Minimal FLAC bitstream framing for Snapcast's flac transport.
Snapcast does not send a .flac file: it sends the FLAC stream header once (as a
flac CodecHeader) and then the audio split into whole FLAC frames, each in its
own timestamped WireChunk. The client's decoder resets its input on every chunk and
loops process_single until the chunk is drained, so a frame may never span two
chunks — every chunk payload must be a whole number of frames
(client/decoder/flac_decoder.cpp).
This module turns the raw FLAC byte stream produced by ffmpeg into exactly that:
take_header/1peels offfLaC+ the metadata blocks (through the last-metadata-block flag) — the CodecHeader payload.take_frame/2peels off one whole audio frame at a time, reporting its blocksize (sample count) so the caller can stamp each WireChunk on the server clock.
Frame boundaries are found the way libFLAC finds them: scan for the 14-bit frame sync, then accept the candidate only if its frame header's CRC-8 checks out.
Summary
Functions
Parse a FLAC frame header at the front of bin.
Reduce a full FLAC metadata header (fLaC + blocks, as returned by take_header/1)
to just fLaC + the STREAMINFO block, flagged as the last metadata block.
Peel one whole FLAC audio frame off the front of buf (which must start at a frame
boundary).
Split off the FLAC metadata header (fLaC + metadata blocks through the
last-metadata-block) from the front of data.
Functions
Parse a FLAC frame header at the front of bin.
Returns {:ok, blocksize, header_len} (sample count + header byte length),
:need_more, or :invalid.
Reduce a full FLAC metadata header (fLaC + blocks, as returned by take_header/1)
to just fLaC + the STREAMINFO block, flagged as the last metadata block.
Snapcast clients only read STREAMINFO (rate/bits/channels). The trailing
VORBIS_COMMENT/PADDING blocks ffmpeg appends (an 8 KiB padding block by default)
are dead weight on the wire — and worse, the client copies the whole CodecHeader into
libFLAC's fixed-size read buffer in one memcpy (client/decoder/flac_decoder.cpp
read_callback), so an oversized header overflows that buffer and crashes libFLAC with
a heap (sysmalloc) assertion. Some builds have a larger buffer and survive (macOS),
others (Debian/glibc on ARM) abort. Sending only STREAMINFO (~42 bytes) is safe on all
clients and matches what the C++ snapserver effectively does.
STREAMINFO is always the first metadata block per the FLAC spec, so we keep the first block and set its last-metadata-block flag (bit 7 of the type byte).
Peel one whole FLAC audio frame off the front of buf (which must start at a frame
boundary).
{:ok, frame, blocksize, rest}—frameis a whole frame decoding toblocksizesamples;restis the remaining bytes.:incomplete— the next frame boundary is not yet visible; supply more bytes.{:error, :not_frame_aligned}—bufdoes not start with a valid frame header.
eof? true means no more bytes will arrive, so the trailing bytes are the final
frame.
Split off the FLAC metadata header (fLaC + metadata blocks through the
last-metadata-block) from the front of data.
Returns {:ok, header, rest} once the whole header is present, :incomplete if more
bytes are needed, or :error if data is not a FLAC stream.