FLAC Multiplex (`mXfC`) Format
Container for multichannel signal data partitioned into standard FLAC streams of at most 8 channels, matching FLAC’s native per-stream channel limit. A multiplex is an 8-byte container header followed by a sequence of channel slices, each consisting of an 8-byte slice header and an independent standard FLAC bitstream.
The multiplex has two structural layers:
- Partitioning — channels are split channel-major into slices of at most 8 channels each, matching FLAC’s per-stream channel limit.
- FLAC encoding — each slice is encoded as an independent standard FLAC
(
fLaC) stream.
Wire format
Section titled “Wire format”All multi-byte fields are little-endian. The container header is packed (no padding) and must be exactly 8 bytes:
| Off | Size | Field | C type | Constraint |
|---|---|---|---|---|
| 0 | 4 | magic | uint32 | 0x4366586D (ASCII m X f C) |
| 4 | 2 | total_channels | uint16 | ≥ 1 |
| 6 | 2 | slice_count | uint16 | ≥ 1 |
slice_count slice blocks follow the container header back-to-back. Each
slice block is an 8-byte slice header followed by a variable-length FLAC
bitstream, defined in Slice layout.
From the no-overlap and full-coverage rules in Validation,
⌈total_channels / 8⌉ ≤ slice_count ≤ total_channels.
Slice layout
Section titled “Slice layout”Each slice is preceded by an 8-byte slice header:
| Off | Size | Field | C type | Constraint |
|---|---|---|---|---|
| 0 | 2 | first_channel | uint16 | < total_channels |
| 2 | 2 | channel_count | uint16 | 1 ≤ channel_count ≤ 8; first_channel + channel_count ≤ total_channels |
| 4 | 4 | payload_size | uint32 | byte size of the FLAC bitstream that follows |
Immediately after the slice header, payload_size bytes of a standard
FLAC (fLaC) bitstream follow. The next slice header begins at the byte
right after the bitstream.
The multiplex’s total_channels channels are partitioned into contiguous
non-overlapping channel ranges; each slice carries the FLAC encoding of one
such range. For 22 channels:
total_channels = 22, slice_count = 3
Slice 0: first_channel = 0, channel_count = 8 -> channels 0.. 7Slice 1: first_channel = 8, channel_count = 8 -> channels 8..15Slice 2: first_channel = 16, channel_count = 6 -> channels 16..21Slices may appear in any order. Readers must place channels using
first_channel rather than the slice’s position in the file.
Sample format and bit depth
Section titled “Sample format and bit depth”The multiplex headers do not carry sample format or bit depth — these are
properties of each slice’s FLAC bitstream and are read from the FLAC
STREAMINFO metadata block during slice decoding. FLAC restricts samples to
8, 16, or 24 bits per sample in signed two’s-complement; the multiplex
inherits this constraint.
All slices in a single multiplex must agree on sample format, sample rate, and sample count after FLAC decoding (see Validation). The multiplex itself does not duplicate these values in any header.
Compression
Section titled “Compression”The only compression knob is the FLAC compression level applied independently to each slice when it is FLAC-encoded:
| Value | Meaning |
|---|---|
0 | fastest, least compression |
8 | slowest, best compression |
The level is a property of the encoder, not of the wire format — different slices in the same multiplex may have been produced with different levels and still decode to identical samples. FLAC is lossless; the level affects size and CPU cost only.
Validation
Section titled “Validation”A conforming reader must reject inputs that violate any of the following:
- Container header reads less than 8 bytes.
magic != 0x4366586D.- A slice header reads less than 8 bytes.
first_channel ≥ total_channels.first_channel + channel_count > total_channels.channel_countis0or greater than8.- Any channel index appears in more than one slice’s range.
- The union of slice ranges does not cover every index in
[0, total_channels)exactly once. - A slice payload reads fewer than
payload_sizebytes from the stream. - A slice payload is not a valid standard FLAC (
fLaC) bitstream. - A decoded slice’s actual channel count does not match its slice-header
channel_count. - Any slice’s decoded sample format, sample rate, or sample count differs
from slice
0’s.
The validation order is at the reader’s discretion, but cheap structural checks (magic, header sizes, per-slice index arithmetic) should run before payload-decode checks (FLAC bitstream validity, sample-format consistency).
Reference encoder (ISO C17)
Section titled “Reference encoder (ISO C17)”Single-call encoder that builds a multiplex from one or more channel-major
sample buffers. Allocates the result on the heap; caller frees with free.
Returns NULL on invalid arguments, libFLAC failure, or allocation failure.
Compile: cc -std=c17 -O3 fmux_encode.c -lFLAC -o fmux_encode
#include <stdbool.h>#include <stdint.h>#include <stdlib.h>#include <string.h>#include <FLAC/stream_encoder.h>
#define FMUX_MAGIC 0x4366586Du#define FMUX_MAX_CHANNELS_PER_SLICE 8
#pragma pack(push, 1)typedef struct { uint32_t magic; uint16_t total_channels; uint16_t slice_count;} FmuxContainerHeader;
typedef struct { uint16_t first_channel; uint16_t channel_count; uint32_t payload_size;} FmuxSliceHeader;#pragma pack(pop)
// Growable byte buffer with an explicit write cursor; libFLAC patches STREAMINFO// via seek/tell on finish, so a simple append-only buffer is not sufficienttypedef struct { uint8_t* data; size_t length; size_t capacity; size_t position;} ByteBuffer;
static bool buffer_write(ByteBuffer* buffer, const uint8_t* bytes, size_t count) { const size_t required = buffer->position + count;
if (required > buffer->capacity) { size_t new_capacity = buffer->capacity ? buffer->capacity : 4096;
while (new_capacity < required) { new_capacity *= 2; }
uint8_t* new_data = (uint8_t*)realloc(buffer->data, new_capacity);
if (!new_data) { return false; }
buffer->data = new_data; buffer->capacity = new_capacity; }
memcpy(buffer->data + buffer->position, bytes, count); buffer->position += count;
if (buffer->position > buffer->length) { buffer->length = buffer->position; }
return true;}
static FLAC__StreamEncoderWriteStatus write_callback(const FLAC__StreamEncoder* encoder, const FLAC__byte bytes[], size_t count, uint32_t samples, uint32_t current_frame, void* client_data) { (void)encoder; (void)samples; (void)current_frame;
return buffer_write((ByteBuffer*)client_data, bytes, count) ? FLAC__STREAM_ENCODER_WRITE_STATUS_OK : FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR;}
static FLAC__StreamEncoderSeekStatus seek_callback(const FLAC__StreamEncoder* encoder, FLAC__uint64 absolute_offset, void* client_data) { (void)encoder;
ByteBuffer* slice_buffer = (ByteBuffer*)client_data;
if (absolute_offset > slice_buffer->length) { return FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR; }
slice_buffer->position = (size_t)absolute_offset; return FLAC__STREAM_ENCODER_SEEK_STATUS_OK;}
static FLAC__StreamEncoderTellStatus tell_callback(const FLAC__StreamEncoder* encoder, FLAC__uint64* absolute_offset, void* client_data) { (void)encoder;
*absolute_offset = (FLAC__uint64)((const ByteBuffer*)client_data)->position; return FLAC__STREAM_ENCODER_TELL_STATUS_OK;}
static bool encode_one_slice(const int32_t* const* slice_channels, uint16_t channels_in_slice, uint32_t samples_count, uint32_t sample_rate, uint8_t bits_per_sample, uint8_t compression_level, ByteBuffer* slice_buffer) { FLAC__StreamEncoder* encoder = FLAC__stream_encoder_new();
if (!encoder) { return false; }
FLAC__stream_encoder_set_channels(encoder, channels_in_slice); FLAC__stream_encoder_set_bits_per_sample(encoder, bits_per_sample); FLAC__stream_encoder_set_sample_rate(encoder, sample_rate); FLAC__stream_encoder_set_total_samples_estimate(encoder, samples_count); FLAC__stream_encoder_set_compression_level(encoder, compression_level); FLAC__stream_encoder_set_verify(encoder, false);
const FLAC__StreamEncoderInitStatus init_status = FLAC__stream_encoder_init_stream( encoder, write_callback, seek_callback, tell_callback, NULL, slice_buffer);
if (init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) { FLAC__stream_encoder_delete(encoder); return false; }
const bool ok = FLAC__stream_encoder_process(encoder, slice_channels, samples_count) && FLAC__stream_encoder_finish(encoder);
FLAC__stream_encoder_delete(encoder); return ok;}
uint8_t* fmux_encode(const int32_t* const* input_channels, uint16_t channels_count, uint32_t samples_count, uint32_t sample_rate, uint8_t bits_per_sample, uint8_t compression_level, size_t* output_size) { if (!input_channels || !output_size) { return NULL; } if (!channels_count) { return NULL; } if (bits_per_sample != 8 && bits_per_sample != 16 && bits_per_sample != 24) { return NULL; } if (compression_level > 8) { return NULL; } if (!samples_count || !sample_rate) { return NULL; }
const uint16_t slice_count = (channels_count + FMUX_MAX_CHANNELS_PER_SLICE - 1) / FMUX_MAX_CHANNELS_PER_SLICE;
// Emit container header into the growing output buffer ByteBuffer output = {0};
const FmuxContainerHeader container_header = { .magic = FMUX_MAGIC, .total_channels = channels_count, .slice_count = slice_count, };
if (!buffer_write(&output, (const uint8_t*)&container_header, sizeof(container_header))) { free(output.data); return NULL; }
// Encode each slice and append (slice header + FLAC bitstream) to the multiplex const int32_t* slice_ptrs[FMUX_MAX_CHANNELS_PER_SLICE];
for (uint16_t slice_index = 0; slice_index < slice_count; ++slice_index) { const uint16_t channels_start = (uint16_t)(slice_index * FMUX_MAX_CHANNELS_PER_SLICE); const uint16_t channels_left = (uint16_t)(channels_count - channels_start); const uint16_t channels_in_slice = channels_left < FMUX_MAX_CHANNELS_PER_SLICE ? channels_left : FMUX_MAX_CHANNELS_PER_SLICE;
for (uint16_t channel = 0; channel < channels_in_slice; ++channel) { slice_ptrs[channel] = input_channels[channels_start + channel]; }
ByteBuffer slice_buffer = {0};
if (!encode_one_slice(slice_ptrs, channels_in_slice, samples_count, sample_rate, bits_per_sample, compression_level, &slice_buffer)) { free(slice_buffer.data); free(output.data); return NULL; }
const FmuxSliceHeader slice_header = { .first_channel = channels_start, .channel_count = channels_in_slice, .payload_size = (uint32_t)slice_buffer.length, };
if (!buffer_write(&output, (const uint8_t*)&slice_header, sizeof(slice_header)) || !buffer_write(&output, slice_buffer.data, slice_buffer.length)) { free(slice_buffer.data); free(output.data); return NULL; }
free(slice_buffer.data); }
*output_size = output.length; return output.data;}- The encoder produces a host-byte-order container and slice headers. On big-endian platforms the multi-byte fields must be byte-swapped to little-endian before writing the bytes out. The reader assumes little-endian.
- The encoder emits slices in ascending order of
first_channelfor predictability. The format does not require this ordering; conforming readers must usefirst_channelto place channels regardless of slice position in the file. - Each slice payload is a standard FLAC bitstream per https://xiph.org/flac/format.html. Readers delegate slice payload parsing to a FLAC decoder rather than re-implementing FLAC parsing.
FLAC__stream_encoder_finishrewrites the slice’sSTREAMINFOmetadata block in-place by seeking back to the start of the bitstream; the in-memory seek/tell callbacks above support this. A file-backed encoder usingFLAC__stream_encoder_init_filedoes not need explicit seek/tell callbacks.