Snapcast.Flac (Snapcast v0.4.6)

Copy Markdown View Source

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/1 peels off fLaC + the metadata blocks (through the last-metadata-block flag) — the CodecHeader payload.
  • take_frame/2 peels 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

frame_header(bin)

Parse a FLAC frame header at the front of bin.

Returns {:ok, blocksize, header_len} (sample count + header byte length), :need_more, or :invalid.

streaminfo_only(arg)

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).

take_frame(buf, eof? \\ false)

Peel one whole FLAC audio frame off the front of buf (which must start at a frame boundary).

  • {:ok, frame, blocksize, rest}frame is a whole frame decoding to blocksize samples; rest is the remaining bytes.
  • :incomplete — the next frame boundary is not yet visible; supply more bytes.
  • {:error, :not_frame_aligned}buf does not start with a valid frame header.

eof? true means no more bytes will arrive, so the trailing bytes are the final frame.

take_header(data)

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.