Files
-rpi-sendspin/third_party/sendspin-go/internal/server/flac_encoder.go

148 lines
4.4 KiB
Go

// ABOUTME: FLAC audio encoder for lossless streaming
// ABOUTME: Wraps mewkiz/flac to encode PCM int32 samples to FLAC frames
package server
import (
"bytes"
"fmt"
"github.com/mewkiz/flac"
"github.com/mewkiz/flac/frame"
"github.com/mewkiz/flac/meta"
)
// FLACEncoder wraps a mewkiz/flac encoder for real-time FLAC frame
// generation. Each Encode call produces one FLAC frame from a block
// of interleaved int32 PCM samples. Prediction analysis is enabled
// so the encoder picks optimal Fixed/LPC prediction per block for
// real compression (not just verbatim framing).
type FLACEncoder struct {
encoder *flac.Encoder
buf *bytes.Buffer
codecHeader []byte
sampleRate int
channels int
bitDepth int
blockSize int
}
// NewFLACEncoder creates a FLAC encoder with prediction analysis enabled.
// blockSize is samples per channel per frame (e.g., 960 for 20ms at 48kHz).
func NewFLACEncoder(sampleRate, channels, bitDepth, blockSize int) (*FLACEncoder, error) {
buf := &bytes.Buffer{}
info := &meta.StreamInfo{
BlockSizeMin: uint16(blockSize),
BlockSizeMax: uint16(blockSize),
SampleRate: uint32(sampleRate),
NChannels: uint8(channels),
BitsPerSample: uint8(bitDepth),
}
enc, err := flac.NewEncoder(buf, info)
if err != nil {
return nil, fmt.Errorf("failed to create FLAC encoder: %w", err)
}
// Prediction analysis is enabled by default in mewkiz/flac v1.0.13.
// Explicit call for clarity: WriteFrame will analyze subframes marked
// PredVerbatim and pick the optimal prediction method (Constant/Fixed).
enc.EnablePredictionAnalysis(true)
// The encoder writes fLaC + STREAMINFO to the buffer immediately.
codecHeader := make([]byte, buf.Len())
copy(codecHeader, buf.Bytes())
buf.Reset()
return &FLACEncoder{
encoder: enc,
buf: buf,
codecHeader: codecHeader,
sampleRate: sampleRate,
channels: channels,
bitDepth: bitDepth,
blockSize: blockSize,
}, nil
}
// CodecHeader returns the FLAC codec header (fLaC + STREAMINFO metadata
// block). Base64-encode this for stream/start messages.
func (e *FLACEncoder) CodecHeader() []byte {
return e.codecHeader
}
// Encode converts a block of interleaved int32 PCM samples into a FLAC
// frame. len(samples) must equal blockSize * channels.
func (e *FLACEncoder) Encode(samples []int32) ([]byte, error) {
expected := e.blockSize * e.channels
if len(samples) != expected {
return nil, fmt.Errorf("expected %d samples (%d x %d), got %d",
expected, e.blockSize, e.channels, len(samples))
}
// Sendspin's AudioSource contract delivers int32 samples right-justified
// to 24 bits (see encodePCM / convertToInt16 in audio_engine.go). When
// the encoder's configured bit depth differs (e.g., 16-bit FLAC for an
// ESP32 that doesn't advertise 24-bit), down-shift so values fit the
// target range — otherwise the encoder emits frames with out-of-range
// samples that decoders reject, the client wedges, and TCP back-pressure
// stalls the server's WebSocket writer.
shift := uint(0)
if e.bitDepth < 24 {
shift = uint(24 - e.bitDepth)
}
// De-interleave into per-channel slices.
subframes := make([]*frame.Subframe, e.channels)
for ch := 0; ch < e.channels; ch++ {
channelSamples := make([]int32, e.blockSize)
for i := 0; i < e.blockSize; i++ {
channelSamples[i] = samples[i*e.channels+ch] >> shift
}
subframes[ch] = &frame.Subframe{
SubHeader: frame.SubHeader{
// PredVerbatim signals the encoder to run prediction analysis
// and pick the optimal method (Constant, Fixed, or Verbatim).
Pred: frame.PredVerbatim,
},
Samples: channelSamples,
NSamples: e.blockSize,
}
}
var channelAssign frame.Channels
switch e.channels {
case 1:
channelAssign = frame.ChannelsMono
case 2:
channelAssign = frame.ChannelsLR
default:
channelAssign = frame.Channels(e.channels)
}
f := &frame.Frame{
Header: frame.Header{
HasFixedBlockSize: true,
BlockSize: uint16(e.blockSize),
SampleRate: uint32(e.sampleRate),
Channels: channelAssign,
BitsPerSample: uint8(e.bitDepth),
},
Subframes: subframes,
}
e.buf.Reset()
if err := e.encoder.WriteFrame(f); err != nil {
return nil, fmt.Errorf("FLAC encode failed: %w", err)
}
result := make([]byte, e.buf.Len())
copy(result, e.buf.Bytes())
return result, nil
}
// Close finalizes the encoder.
func (e *FLACEncoder) Close() error {
return e.encoder.Close()
}