Skip to content

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 into contiguous ranges of at most 8 channels; each range becomes one slice.
  • FLAC encoding — each slice is encoded as an independent standard FLAC (fLaC) stream.

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.

Each slice starts with 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.. 7
Slice 1: first_channel = 8, channel_count = 8 -> channels 8..15
Slice 2: first_channel = 16, channel_count = 6 -> channels 16..21

Slices may appear in any order. Readers must place channels using first_channel rather than the slice’s position in the file.

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. Encoded integer inputs keep their native 8, 16, 24, or 32-bit signed two’s-complement depth. Floating-point inputs are quantized to signed 32-bit samples before FLAC encoding.

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.

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.

A conforming reader must reject inputs that violate any of the following:

  • Container header reads less than 8 bytes.
  • magic != 0x4366586D.
  • total_channels == 0.
  • slice_count == 0.
  • A slice header reads less than 8 bytes.
  • first_channel ≥ total_channels.
  • first_channel + channel_count > total_channels.
  • channel_count is 0 or greater than 8.
  • 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_size bytes from the stream.
  • A slice payload is not a valid standard FLAC (fLaC) bitstream.
  • A slice’s STREAMINFO bit depth is not 8, 16, 24, or 32.
  • 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 the first slice’s in the file.

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

Single-call encoder that builds a multiplex from per-channel (planar) int32 sample buffers. Allocates the result on the heap; caller frees with free. Returns NULL on invalid arguments, oversized slice payloads, 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 sufficient
typedef 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 && bits_per_sample != 32) {
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;
}
// Slice payload size travels in a 32-bit wire field
if (slice_buffer.length > UINT32_MAX) {
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_channel for predictability. The format does not require this ordering; conforming readers must use first_channel to 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_finish rewrites the slice’s STREAMINFO metadata 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 using FLAC__stream_encoder_init_file does not need explicit seek/tell callbacks.
  • Input samples are passed as sign-extended int32 values regardless of bits_per_sample, per the libFLAC FLAC__stream_encoder_process convention.
  • Encoding 32-bit samples requires libFLAC 1.4.0 or newer; older libFLAC releases cap the encoder at 24 bits per sample.