Skip to content

Reading and Writing Audio Files with KFR

KFR provides decoder and encoder classes for reading and writing audio files across several common containers and codecs. This lets you load audio into memory for processing (filtering, resampling, analysis) and save the result back to disk. Include <kfr/audio.hpp> to use these facilities.

Supported Formats

KFR reads WAV (including RF64 and BW64 for files over 4 GB), W64, AIFF, CAF, FLAC, and MP3 (decode-only), and writes WAV (with automatic RF64 fallback for large files), W64, AIFF, CAF, and FLAC. CAF additionally supports the ALAC codec when built with the ALAC library. On Windows, an optional Media Foundation decoder extends support to additional formats. FLAC and ALAC support depend on external libraries enabled at build time via CMake. See Audio Format Support for the full breakdown of codecs, bit depths, and container capabilities.

Key Concepts

  • Audio data storage: Samples are stored in audio_data, a template over the sample layout: audio_data_interleaved (channels interleaved in a single buffer) or audio_data_planar (one buffer per channel). The sample type is always fbase (float or double, depending on how KFR was built).
  • Error handling: I/O functions return expected<T, audiofile_error>. Check the result before using it — either test it directly (if (!result) ...) or call .error() to get the failure reason. Unchecked errors typically surface later as an empty buffer or a crash when dereferencing *result.
  • File paths: The file_path type depends on the build configuration:
    • std::filesystem::path, if the KFR_USE_STD_FILESYSTEM CMake option is enabled.
  • Otherwise, std::wstring on Windows or std::string on Linux/macOS.
  • On Windows without std::filesystem, overloads that accept a UTF-8 encoded std::string are also provided; they convert to std::wstring internally.
  • Working with large files: Read and write in chunks rather than loading an entire file into memory (see Sample rate conversion for a chunked-processing example). audio_data also has constructors that wrap caller-supplied buffers instead of allocating, which avoids a copy when integrating with existing audio pipelines.

Reading Audio into audio_data

To read an audio file, create an audio_decoder, open the file to retrieve its format, then read the samples into an audio_data buffer.

Step-by-Step Process

  1. Create a decoder with create_decoder_for_file(const file_path &, const audio_decoding_options &). It's selected from the file extension, or the header when the extension is missing or ambiguous; it returns nullptr if no matching decoder is available.
  2. Call [unknown symbol] to retrieve the audiofile_format (sample rate, channel count, bit depth, and so on).
  3. Read samples with .read_all() (whole file into a newly allocated buffer), .read(size_t) (up to a fixed number of frames into a newly allocated buffer), or .read_to(const audio_data_interleaved &) (into a buffer you already own). The first two return audio_data_interleaved, while read_to returns the number of frames read; use .read_all_planar() if you want a planar buffer directly.
  4. Convert to planar afterward, if needed, by constructing an audio_data_planar from the interleaved result.

Check the result of every step — .create_decoder_for_file(const file_path &, const audio_decoding_options &) can return nullptr, and [unknown symbol]/.read_all() return expected<T, audiofile_error>.

Example: Reading a Stereo WAV File

#include <kfr/audio.hpp>
#include <iostream>


int main() {
    kfr::file_path path = KFR_FILEPATH("input.wav");

    auto decoder = kfr::create_decoder_for_file(path);
    if (!decoder) {
        std::cerr << "Error: no decoder available for this file" << std::endl;
        return 1;
    }

    auto format = decoder->open(path);
    if (!format) {
        std::cerr << "Error opening file: " << kfr::to_string(format.error()) << std::endl;
        return 1;
    }

    std::cout << "Sample rate: " << format->sample_rate << ", Channels: " << format->channels << std::endl;

    auto data = decoder->read_all();  // Reads the entire file into audio_data_interleaved
    if (!data) {
        std::cerr << "Error reading data: " << kfr::to_string(data.error()) << std::endl;
        return 1;
    }

    // data->size is the frame count, data->channels is the channel count
    // Convert to planar if needed: kfr::audio_data_planar planar = *data;

    return 0;
}

file_path and the KFR_FILEPATH macro adapt to the active build configuration (see Key Concepts above), so this example compiles unchanged whether file_path is std::string, std::wstring, or std::filesystem::path. On Windows without std::filesystem, you can instead pass a std::string directly; matching overloads convert it internally.

Reading Directly into User-Supplied Buffers (No Copy)

audio_data_interleaved and audio_data_planar can wrap existing pointers instead of allocating, which avoids a copy when the destination buffer already exists (for example, a buffer owned by another library).

Example: Reading Interleaved Stereo Audio into a std::vector

std::vector<kfr::fbase> buffer(44100 * 2 * 60);  // 1 minute of 44.1 kHz stereo

kfr::audio_data_interleaved stereo_data(buffer.data(), 2, buffer.size() / 2);  // Wrap the buffer, no copy

auto decoder = kfr::create_decoder_for_file(KFR_FILEPATH("stereo.wav"));
if (!decoder) {
    std::cerr << "Error: no decoder available for this file" << std::endl;
    return;
}
auto format = decoder->open(KFR_FILEPATH("stereo.wav"));
if (format) {
    auto read = decoder->read_to(stereo_data);  // Read directly into the buffer
    if (read) {
        std::cout << "Read " << *read << " frames" << std::endl;
    }
}

For planar data, wrap an array of per-channel pointers instead:

constexpr size_t frames = 44100;
std::vector<kfr::fbase> left(frames);
std::vector<kfr::fbase> right(frames);
std::array<kfr::fbase*, 2> pointers = { left.data(), right.data() };
kfr::audio_data_planar planar(pointers, left.size());  // Wrap two buffers, no copy

Advanced Options

  • Pass audio_decoding_options{ .read_metadata = true } to create_decoder_for_file to load tags (artist, title, and so on) into format->metadata.
  • To seek, call .seek(uint64_t) and pass the position in frames. Seeking isn't always sample-accurate for compressed codecs; call .seek_is_precise() to check before relying on exact positioning.

Writing Audio from audio_data

Writing means creating a audio_encoder for the target container, opening the destination file with the desired audiofile_format, writing one or more chunks of data, and closing the encoder to finalize the file.

Step-by-Step Process

  1. Create an encoder for the target container — either a container-specific factory such as create_wave_encoder(const wave_encoding_options &), or create_encoder_for_container when the container is only known at runtime.
  2. Fill in an audiofile_format (container, codec, bit depth, sample rate, channel count).
  3. Call [unknown symbol] to start writing.
  4. Write one or more chunks of interleaved data with .write(const audio_data_interleaved &).
  5. Call .close() to finalize the file and get the total number of frames written.

Example: Writing Processed Audio to WAV

kfr::audio_data_interleaved data(2, 44100);  // Stereo, 1 second at 44.1 kHz
// Fill data with samples...

kfr::audiofile_format format;
format.container  = kfr::audiofile_container::wave;
format.codec       = kfr::audiofile_codec::lpcm;
format.bit_depth   = 16;  // 16-bit PCM
format.sample_rate = 44100;
format.channels    = 2;

auto encoder = kfr::create_wave_encoder();
auto opened = encoder->open(KFR_FILEPATH("output.wav"), format);
if (!opened) {
    std::cerr << "Error opening file: " << kfr::to_string(opened.error()) << std::endl;
    return 1;
}

auto written = encoder->write(data);
if (!written) {
    std::cerr << "Error writing: " << kfr::to_string(written.error()) << std::endl;
    return 1;
}

auto closed = encoder->close();
if (closed) {
    std::cout << "Wrote " << *closed << " frames" << std::endl;
}

Pass audio_encoding_options{ .dithering = kfr::audio_dithering::triangular } to the encoder factory for better-sounding quantization noise when reducing bit depth (options: none, rectangular, triangular).

.write(const audio_data_interleaved &) only accepts audio_data_interleaved; if your data is planar, convert it first (kfr::audio_data_interleaved interleaved = planar_data;) or use the planar overload of encode_audio_file(const file_path &, const audio_data_interleaved &, const audiofile_format &, audio_decoder *, const audio_encoding_options &) described below.

Working with Raw Audio

Raw audio (a sample stream with no container header or metadata) is supported through dedicated decoder and encoder factories that take the format as an explicit parameter, since there's no header to read it from.

Reading Raw Audio

Use create_raw_decoder(const raw_decoding_options &) with format that specifies sample rate, channel count, bit depth, and codec.

Writing Raw Audio

Use create_raw_encoder(const raw_encoding_options &), then proceed as with any other encoder (open with an audiofile_format, write, close).

Example: Reading Raw 16-bit PCM Stereo

kfr::raw_decoding_options opts;
opts.format.sample_rate = 48000;
opts.format.channels    = 2;
opts.format.bit_depth   = 32;
opts.format.codec       = kfr::audiofile_codec::ieee_float;

auto decoder = kfr::create_raw_decoder(opts);
auto format  = decoder->open(KFR_FILEPATH("raw.pcm"));
auto data    = decoder->read_all();
if (!format || !data) {
    std::cerr << "Error reading raw PCM data" << std::endl;
}

Raw I/O assumes little-endian samples by default; set opts.format.endianness explicitly if the stream is big-endian.

Decoding and Encoding in One Call

For simple cases that don't need explicit control over the decoder or encoder lifetime, use the convenience functions below.

Decoding

decode_audio_file(const file_path &, audiofile_format *, const audio_decoding_options &) opens the file, reads it in full, and returns audio_data_interleaved. Pass a pointer to an audiofile_format if you want the detected format back; pass nullptr (the default) to discard it.

kfr::audiofile_format detected_format;
auto data = kfr::decode_audio_file(KFR_FILEPATH("input.flac"), &detected_format);
if (data) {
    // Process *data
} else {
    std::cerr << "Error: " << kfr::to_string(data.error()) << std::endl;
}

Encoding

encode_audio_file creates the appropriate encoder, opens the file, writes data in one call, and closes it. Overloads accept either audio_data_interleaved or audio_data_planar.

kfr::audio_data_interleaved data(2, 44100, 0.0f);
kfr::audiofile_format format;
format.container  = kfr::audiofile_container::wave;
format.codec      = kfr::audiofile_codec::lpcm;
format.bit_depth  = 16;
format.sample_rate = 44100;
format.channels    = 2;
auto result = kfr::encode_audio_file(KFR_FILEPATH("output.wav"), data, format);
if (!result) {
    std::cerr << "Error: " << kfr::to_string(result.error()) << std::endl;
}

To copy metadata (such as tags) from an existing file while re-encoding, pass the source decoder as the fourth argument: encode_audio_file.

Reading a RIFF Chunk

RIFF-based containers (WAV, W64, RF64, BW64) store auxiliary data in named chunks, such as LIST/INFO for metadata. .has_chunk, .read_chunk, and .read_chunk_bytes give direct access to these chunks; they're implemented for RIFF-family decoders and return audiofile_error::not_implemented (or an empty result) on non-RIFF containers such as FLAC or MP3.

Example: Reading a Custom Chunk

auto decoder = kfr::create_decoder_for_file(KFR_FILEPATH("file.wav"));
if (!decoder) {
    std::cerr << "Error: no decoder available for this file" << std::endl;
    return;
}
auto format = decoder->open(KFR_FILEPATH("file.wav")); // Handle errors as usual
if (!format) {
    std::cerr << "Error opening file: " << kfr::to_string(format.error()) << std::endl;
    return;
}

constexpr char id[4] = { 'L', 'I', 'S', 'T' };
std::span<const std::byte> chunk_id(reinterpret_cast<const std::byte*>(id), 4);

if (decoder->has_chunk(chunk_id)) {
    auto read = decoder->read_chunk_bytes(chunk_id);
    if (read) {
        std::vector<uint8_t> chunk_data = std::move(*read);
        // Process chunk_data
    }
}

For large chunks, use .read_chunk instead: it invokes handler repeatedly with successive buffers rather than accumulating the whole chunk in memory.

This is useful for extracting metadata or format-specific extensions without decoding the audio samples themselves.