Skip to content

Audio files

Keep file work on a control or background thread. Capability queries, preflight, open/close, probing, hashing, decoding, seeking, and writing can allocate or block. None belongs in an audio callback.

  1. Call getAudioFileCapabilities(). If file_io_available is false, stop and report that the package has no usable file provider. The codec rows describe SDK policy; use probeAudioFile() to determine whether a particular file is accepted.
  2. For output, fill an AudioFileWriterConfig and call preflightAudioFileWrite() before creating a destination. The documented procedure uses WAV, 48 kHz, one channel, and AudioSampleFormat::Int16.
  3. Call createAudioFileWriter() and check for a null pointer. Call open(path, config) and check SessionGraphError. The parent directory must already exist. Pass interleaved float samples to writeSamples(buffer, frames), check its Result<size_t>, then call close() to flush data and finalize the header.
  4. Call probeAudioFile(path) when you need metadata without decoding. Check the Result; a missing, unreadable, or corrupt file is an error, not a reason to guess the format.
  5. Call createAudioFileReader() and check for null. Call open(path) on the control/background thread, then inspect sample rate, channel count, frame count, format, and bit depth. Allocate a caller-owned interleaved float buffer and call readSamples(). The result may contain fewer frames at end-of-file; zero frames means EOF.
  6. Close the reader on the control/background thread. Remove temporary output only after the writer is closed and all reads/checks have finished.

IAudioFileReader::open() and close() are explicitly not audio-thread operations. A prepared or worker-fed PCM source can be handed to transport; the callback must not reopen, seek, probe, or write a file.

The writer accepts WAV, AIFF, and FLAC. WAV and AIFF accept Int16, Int24, and Float32; FLAC accepts Int16 and Int24, and rejects Float32. MP3 and OGG are not supported containers. preflightAudioFileWrite() returns:

  • OK when the exact rate/channel/container/encoding tuple is accepted.
  • InvalidParameter for invalid rate or channel counts, an invalid encoding, or backend rejection.
  • NotSupported for an unsupported container.
  • NotReady when the provider is unavailable.

Successful preflight does not guarantee that the destination can be created: open() can still return InternalError for a missing parent directory, permissions, or another filesystem failure. With libsndfile, FLAC sample rates above 655350 Hz are rejected during preflight. Integer output clips input floats outside [-1, 1].

The installed-package example writes four mono PCM16 WAV frames through the required libsndfile provider, closes the writer, probes and reopens the file, checks 48 kHz/mono/four-frame/16-bit metadata, reads the frames, compares the decoded samples, and removes the temporary file:

example.audio-file-smoke · Product 0.9.0
Source revision: b8c8b87540fe585b5c7d5c8f25d193c1a1af93d5
// SPDX-License-Identifier: MIT
#include <treefall/audio_file_capabilities.h>
#include <treefall/audio_file_reader.h>
#include <treefall/audio_file_writer.h>

#include <array>
#include <cmath>
#include <filesystem>
#include <iostream>
#include <stdexcept>

namespace {
void require(bool condition, const char* message) {
  if (!condition) throw std::runtime_error(message);
}
} // namespace

int main() {
  const auto path = std::filesystem::temp_directory_path() / "treefall-sdk-audio-file-smoke.wav";
  try {
    const auto capabilities = orpheus::getAudioFileCapabilities();
    require(capabilities.file_io_available, "audio file I/O capability is unavailable");

    constexpr orpheus::AudioFileWriterConfig config{
        .format = orpheus::AudioFileFormat::WAV,
        .sample_rate = 48000,
        .num_channels = 1,
        .sample_format = orpheus::AudioSampleFormat::Int16,
    };
    require(orpheus::preflightAudioFileWrite(config) == orpheus::SessionGraphError::OK,
            "WAV PCM16 preflight failed");

    const std::array<float, 4> samples{0.0f, 0.25f, -0.5f, 1.0f};
    auto writer = orpheus::createAudioFileWriter();
    require(writer != nullptr, "audio writer is unavailable");
    require(writer->open(path.string(), config) == orpheus::SessionGraphError::OK,
            "audio writer open failed");
    const auto written = writer->writeSamples(samples.data(), samples.size());
    require(written.isOk() && *written == samples.size(), "audio write frame count mismatch");
    require(writer->close() == orpheus::SessionGraphError::OK, "audio writer close failed");
    require(writer->getFramesWritten() == 4, "audio writer metadata frame count mismatch");

    const auto probe = orpheus::probeAudioFile(path.string());
    require(probe.isOk() && probe.value.format == orpheus::AudioFileFormat::WAV &&
                probe.value.sample_rate == 48000 && probe.value.num_channels == 1 &&
                probe.value.duration_samples == 4 && probe.value.bit_depth == 16,
            "audio probe metadata mismatch");

    auto reader = orpheus::createAudioFileReader();
    require(reader != nullptr, "audio reader is unavailable");
    const auto opened = reader->open(path.string());
    require(opened.isOk() && opened.value.format == orpheus::AudioFileFormat::WAV &&
                opened.value.sample_rate == 48000 && opened.value.num_channels == 1 &&
                opened.value.duration_samples == 4 && opened.value.bit_depth == 16,
            "audio reader metadata mismatch");
    std::array<float, 4> decoded{};
    const auto read = reader->readSamples(decoded.data(), decoded.size());
    require(read.isOk() && *read == 4, "audio read frame count mismatch");
    for (std::size_t index = 0; index < decoded.size(); ++index) {
      require(std::abs(decoded[index] - samples[index]) < 1.0e-4f, "audio sample mismatch");
    }
    reader->close();
    std::filesystem::remove(path);

    std::cout << "Treefall libsndfile WAV smoke: 4 frames\n";
    return 0;
  } catch (const std::exception& error) {
    std::error_code ignored;
    std::filesystem::remove(path, ignored);
    std::cerr << "audio file smoke failed: " << error.what() << '\n';
    return 1;
  }
}
Treefall libsndfile WAV smoke: 4 frames
Source and producer-reported verification
Source-relative path
examples/audio-file-smoke.cpp
SHA-256
d743e5183d94c79363ad4cd191745563310427898941f03103348bb2f73db20b
Producer-reported verification
executed
Environment
os=darwin/arm64; cmake=cmake version 4.1.0; compiler=Apple clang version 21.0.0 (clang-2100.0.123.102)
Command
./examples-build/audio-file-smoke

This is a narrow executable check of one provider and tuple, not coverage of every codec or provider. If capability discovery is false, a factory returns null, or an operation returns NotReady, report the unavailable provider and stop. Do not silently substitute another provider.

probeAudioFile() is a header-only operation: successful metadata has an empty file_hash_sha256. IAudioFileReader::open() may calculate the file’s SHA-256 and returns it in metadata. readSamples() returns interleaved frames, while transport playback should consume prepared data rather than issue file calls from the callback.

For a deterministic installed-package composition combining reader, transport, and writer, see offline rendering. The public declarations are in the capability header, reader header, and writer header. The installed composition is exercised by the offline package example.