avioflow 0.7.3

High-performance audio decoding, encoding and resampling powered by FFmpeg
docs.rs failed to build avioflow-0.7.3
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

avioflow (Rust)

Rust bindings for AvioFlow, a high-performance audio library built on FFmpeg.

The crate wraps the same C++ core as the Python, Node.js, Java and WebAssembly bindings, reached through the C ABI the core exports (avioflow/include/avioflow-c-api.h). FFmpeg is linked statically, so there is nothing to install or locate at runtime.

Install

[dependencies]
avioflow = "0.7"

Building the crate compiles the native core, so the host needs a C++17 compiler and CMake 3.20 or newer. The first build downloads a prebuilt FFmpeg package and takes a few minutes; later builds are cached.

Conventions

  • Sample layout is always planar float: samples[channel][sample], with every channel the same length. A ragged input is rejected rather than truncated.
  • Fallible calls return Result<T, Error>. [Error::kind] classifies the failure and [Error::message] carries the text from the native layer.
  • Handles are Send but not Sync. Move a decoder, encoder or resampler between threads freely; sharing one across threads needs external synchronization.
  • Options use builders. Unset fields keep the source format rather than applying a default.

Decoding

AudioDecoder

Method Purpose
new(&StreamOptions) -> Result<Self> Create a decoder.
load_file(&str) -> Result<Metadata> Open a file path, URL or device.
load_buffer(&[u8]) -> Result<Metadata> Open complete file bytes in memory.
feed(&[u8]) -> Result<()> Push encoded bytes for streaming decode.
flush() -> Result<()> Mark streaming input complete.
get_samples() -> Result<Vec<Vec<f32>>> Decode everything remaining.
get_samples_range(f64, Option<f64>) -> Result<Vec<Vec<f32>>> Decode [start, stop) in seconds.
get_frame() -> Result<Option<Frame<'_>>> Decode one frame without copying.
is_finished() -> Result<bool> Whether the stream is exhausted.
metadata() -> Result<Metadata> Current stream metadata.

StreamOptions

Builder Effect when unset
output_sample_rate(i32) Preserves the source rate.
output_num_channels(i32) Preserves the source channel count.
input_sample_rate(i32) Required for raw PCM streaming.
input_channels(i32) Required for raw PCM streaming.
input_format(&str) Required to use feed.

Offline decoding

use avioflow::{AudioDecoder, StreamOptions};

# fn main() -> Result<(), avioflow::Error> {
let mut decoder = AudioDecoder::new(&StreamOptions::new().output_sample_rate(16000))?;
let metadata = decoder.load_file("audio.mp3")?;
let samples = decoder.get_samples()?;

println!("{:.1}s, {} Hz", metadata.duration, metadata.sample_rate);
println!("{} channels x {} samples", samples.len(), samples[0].len());
# Ok(())
# }

load_file reports the source stream. The resampler is not configured until the first frame is decoded, so when output_sample_rate is set, the new rate appears in metadata() after decoding rather than in the value load_file returns.

Time-range decoding

# use avioflow::{AudioDecoder, StreamOptions};
# fn main() -> Result<(), avioflow::Error> {
# let mut decoder = AudioDecoder::new(&StreamOptions::new())?;
# decoder.load_file("audio.mp3")?;
// Seconds 10.3 through 20.3, exclusive of the end
let window = decoder.get_samples_range(10.3, Some(20.3))?;

// From 30s to the end
let tail = decoder.get_samples_range(30.0, None)?;
# Ok(())
# }

Each call seeks independently, so one decoder can serve many ranges. Range decoding requires offline mode; in stream mode only (0.0, None) is valid.

Frame-by-frame, zero-copy

# use avioflow::{AudioDecoder, StreamOptions};
# fn main() -> Result<(), avioflow::Error> {
# let mut decoder = AudioDecoder::new(&StreamOptions::new())?;
# decoder.load_file("audio.mp3")?;
let mut total = 0;
while let Some(frame) = decoder.get_frame()? {
    total += frame.num_samples();
    if let Some(left) = frame.channel(0) {
        // `left` borrows decoder-owned memory; copy it to keep it
        let _peak = left.iter().fold(0.0f32, |a, s| a.max(s.abs()));
    }
}
# Ok(())
# }

Frame borrows the decoder, so the compiler prevents holding a frame across the next decode call that would invalidate its buffers.

Streaming decode

use avioflow::{AudioDecoder, StreamOptions};

# fn main() -> Result<(), avioflow::Error> {
# let chunks: Vec<Vec<u8>> = Vec::new();
let mut decoder = AudioDecoder::new(&StreamOptions::new().input_format("mp3"))?;

for chunk in &chunks {
    decoder.feed(chunk)?;
    while let Some(frame) = decoder.get_frame()? {
        let _ = frame.num_samples();
    }
}

decoder.flush()?; // then drain the remaining frames
while let Some(frame) = decoder.get_frame()? {
    let _ = frame.num_samples();
}
# Ok(())
# }

For raw PCM input, also set input_sample_rate and input_channels.

Resampling

Two entry points: [resample] for a buffer held in full, [AudioResampler] for audio arriving in chunks.

One-shot

use avioflow::resample;

# fn main() -> Result<(), avioflow::Error> {
# let samples: Vec<Vec<f32>> = vec![vec![0.0; 44100], vec![0.0; 44100]];
let downsampled = resample(&samples, 44100, 16000, None)?;
let mono = resample(&samples, 44100, 16000, Some(1))?;
# Ok(())
# }

Chunked

use avioflow::{AudioResampler, ResampleOptions};

# fn main() -> Result<(), avioflow::Error> {
# let chunks: Vec<Vec<Vec<f32>>> = Vec::new();
let mut resampler = AudioResampler::new(&ResampleOptions::new(44100, 16000))?;
let mut output: Vec<Vec<f32>> = Vec::new();

for chunk in &chunks {
    append(&mut output, resampler.process(chunk)?);
}
append(&mut output, resampler.flush()?);

fn append(output: &mut Vec<Vec<f32>>, part: Vec<Vec<f32>>) {
    if output.is_empty() {
        output.resize(part.len(), Vec::new());
    }
    for (channel, data) in output.iter_mut().zip(part) {
        channel.extend(data);
    }
}
# Ok(())
# }

flush() is not optional. The resampler holds back the last few milliseconds to keep filter continuity, and skipping the flush discards them. Filter state carries across process calls, so chunked output matches a one-shot conversion sample for sample — calling resample per chunk instead would introduce a discontinuity at every boundary.

output_num_channels() returns 0 until the first process call reveals the input channel count. The channel count must not change between calls.

Encoding

One-shot

use avioflow::{save_audio, WriteOptions};

# fn main() -> Result<(), avioflow::Error> {
# let samples: Vec<Vec<f32>> = vec![vec![0.0; 16000]];
save_audio("out.wav", &samples, &WriteOptions::new()
    .container_format("wav")
    .codec_name("pcm_s16le")
    .sample_rate(16000)
    .num_channels(1))?;
# Ok(())
# }

Reusable encoder

use avioflow::{AudioEncoder, WriteOptions};

# fn main() -> Result<(), avioflow::Error> {
# let parts: Vec<Vec<Vec<f32>>> = Vec::new();
let mut encoder = AudioEncoder::new(&WriteOptions::new()
    .container_format("flac")
    .codec_name("flac")
    .sample_rate(44100))?;

for (index, part) in parts.iter().enumerate() {
    encoder.save(&format!("part{index}.flac"), part)?;
}
# Ok(())
# }

WriteOptions

Builder Common values
codec_name(&str) "pcm_s16le", "flac", "aac", "libmp3lame", "libopus"
container_format(&str) "wav", "flac", "mp4", "ogg", "adts"
sample_format(&str) "s16", "s32", "flt", "fltp"
sample_rate(i32) 8000, 16000, 44100, 48000
num_channels(i32) 1, 2
bit_rate(i64) 128000, 192000, 320000
overwrite(bool) Defaults to true

Unset fields are inferred from the container and the input samples.

Info and diagnostics

# fn main() -> Result<(), avioflow::Error> {
let decoders = avioflow::supported_decoders()?;       // "mp3", "aac", ...
let encoders = avioflow::supported_encoders()?;       // "pcm_s16le", "flac", ...
let demuxers = avioflow::supported_input_formats()?;  // "mp3", "wav", ...
let muxers = avioflow::supported_output_formats()?;   // "wav", "flac", ...

for device in avioflow::list_audio_devices()? {
    println!("{} ({}) output={}", device.name, device.description, device.is_output);
}

avioflow::set_log_level(Some("warning")); // or None for the default
# Ok(())
# }

Metadata without a full decode:

# use avioflow::{AudioDecoder, StreamOptions};
# fn main() -> Result<(), avioflow::Error> {
let mut decoder = AudioDecoder::new(&StreamOptions::new())?;
let metadata = decoder.load_file("audio.mp3")?;
println!("{} / {} @ {} Hz, {:.1}s",
    metadata.container, metadata.codec, metadata.sample_rate, metadata.duration);
# Ok(())
# }

Metadata also carries num_samples, num_channels, bit_rate and sample_format. Note codec is the FFmpeg decoder name, so an MP3 file reports "mp3float" while container reports "mp3".

Error handling

use avioflow::{AudioDecoder, ErrorKind, StreamOptions};

# fn main() -> Result<(), avioflow::Error> {
let mut decoder = AudioDecoder::new(&StreamOptions::new())?;
match decoder.load_file("missing.mp3") {
    Ok(metadata) => println!("{} Hz", metadata.sample_rate),
    Err(error) => match error.kind() {
        ErrorKind::InvalidArgument => eprintln!("bad argument: {error}"),
        ErrorKind::Runtime => eprintln!("could not decode: {error}"),
        ErrorKind::InvalidString => eprintln!("path contained a NUL byte"),
        ErrorKind::Unknown => eprintln!("unclassified failure: {error}"),
    },
}
# Ok(())
# }

Error implements std::error::Error, so it works with ? and Box<dyn Error>.

Building from a repository checkout

cd rust
cargo test

build.rs configures the repository root with static linkage. To pass extra CMake arguments — for example to reuse an already downloaded FFmpeg package:

AVIOFLOW_CMAKE_ARGS="-DFETCHCONTENT_SOURCE_DIR_FFMPEG_BIN=/path/to/ffmpeg" cargo test

The crate version is derived from the repository's version.txt; run python3 scripts/sync_version.py after changing it rather than editing Cargo.toml directly.

Publishing

The crate compiles the C++ core, but Cargo can only publish files inside rust/, so the native sources must be staged there first:

python3 scripts/vendor_rust_sources.py   # copies sources into rust/native/
cargo publish --manifest-path rust/Cargo.toml

rust/native/ is generated and gitignored; build.rs uses it when present and falls back to the repository root otherwise, so a checkout needs no vendoring to run cargo test.

Verify the package builds as a consumer would before publishing, since a publish is irreversible — a version can be yanked but never replaced:

python3 scripts/vendor_rust_sources.py
cargo package --manifest-path rust/Cargo.toml
tar xzf rust/target/package/avioflow-*.crate -C /tmp
cd /tmp/avioflow-* && cargo build

CI runs this same check on every tag.

Platform support

Verified on Linux x86_64. The macOS and Windows link configuration is implemented but not yet exercised in CI.

avioflow/core/compat/glibc-finite-compat.c supplies the __*_finite math symbols that glibc 2.31 removed, which the bundled libvorbis still refers to. They are compiled in whenever FFmpeg is linked statically on Linux, which is what the crate does; the other bindings link shared FFmpeg, which does not refer to them.

License

MIT