A few samples
to get started.

A practical guide to audiobox 0.5. Start with an audio buffer, compose the operations you need, and write the result.

Quick start

Add the crate to your project. Rust 1.85 or newer is required.

Terminal
cargo add audiobox

Or add audiobox = "0.5" under [dependencies] in Cargo.toml. The crate has no dependencies. Optional features add modules for games: mixer, synth and sfx; see Game audio.

This complete program reads a WAV, trims it, normalizes its peak and writes FLAC:

main.rs
use audiobox::{EncodeOptions, NormalizeOptions,
    read_audio_file, write_audio_file};

fn main() -> audiobox::Result<()> {
    let original = read_audio_file("recording.wav")?;
    let ready = original
        .cut(0.0, 10.0)?
        .to_mono()
        .normalize(NormalizeOptions::default())?;

    write_audio_file("ready.flac", &ready,
        &EncodeOptions::default())?;
    Ok(())
}

Try the same operations on demo audio or your own clip in the browser sandbox.

The audio model

Audio stores planar f32 samples: one Vec<f32> per channel. A frame is one sample from each channel at the same time. All channels must have equal lengths, the sample rate must be valid, and samples must be finite.

Samples conventionally range from −1.0 to 1.0, but intermediate processing may exceed that range. Integer encoding clips to the output format's range. Use gain, normalization or a limiter to control levels before exporting.

Construct and inspect
use audiobox::Audio;

fn main() -> audiobox::Result<()> {
    let audio = Audio::new(vec![vec![0.0; 48_000]], 48_000)?;
    assert_eq!(audio.channels(), 1);
    assert_eq!(audio.frames(), 48_000);
    assert_eq!(audio.duration(), 1.0);

    let channel: &[f32] = audio.channel_data(0)?;
    let interleaved: Vec<f32> = audio.to_interleaved();
    let restored = Audio::from_interleaved(&interleaved, 1, 48_000)?;
    assert_eq!(channel.len(), restored.frames());
    Ok(())
}

Transforms return a new Audio; the original remains available. Getters such as all_channels() and channel_data() expose immutable slices.

Decode & encode

Use decode(&bytes) for automatic container detection. sniff(&bytes) returns an Option<AudioFormat> without decoding the samples. File helpers wrap the same codecs with filesystem I/O.

Bytes in, bytes out
use audiobox::{Audio, AudioFormat, EncodeOptions, SampleFormat,
    decode, encode, encode_with_options, sniff};

fn main() -> audiobox::Result<()> {
    let audio = Audio::new(vec![vec![0.0; 4_800]], 48_000)?;
    let wav = encode(&audio, AudioFormat::Wav)?;
    assert_eq!(sniff(&wav), Some(AudioFormat::Wav));
    let restored = decode(&wav)?;

    let flac = encode_with_options(&restored, AudioFormat::Flac,
        &EncodeOptions {
            sample_format: SampleFormat::S24,
            compression_level: 5,
            ..EncodeOptions::default()
        })?;
    assert!(!flac.is_empty());
    Ok(())
}
FunctionUse it for
decodeDetect a container and decode its samples.
decode_with_optionsSpecify a format or apply allocation, duration and channel limits.
encodeEncode with defaults: PCM16 where applicable, 192 kbps for MP3, level 5 for FLAC.
encode_with_optionsChoose sample format, byte order, compression or supported metadata.
read_audio_fileRead a complete audio file.
write_audio_fileEncode and write a complete audio file; the output extension chooses the format.

For raw PCM, choose AudioFormat::Raw when encoding, and use pcm::decode_pcm or the streaming decoder with an explicit sample format, byte order, channel count and rate when reading.

Editing & channels

Time arguments accept seconds as f64 or an explicit TimePosition. Negative positions count from the end. Positions are rounded to the nearest frame and clamped to the buffer.

Exact frames or seconds
use audiobox::{Audio, TimePosition};

fn main() -> audiobox::Result<()> {
    let audio = Audio::silence(3.0, 2, 48_000)?;
    let first_second = audio.cut(
        TimePosition::Frames(0),
        TimePosition::Frames(48_000),
    )?;
    let last_second = audio.cut(-1.0, audio.duration())?;
    let joined = first_second.concat(&last_second)?;
    let reversed = joined.reverse().to_mono();
    assert_eq!(reversed.channels(), 1);
    Ok(())
}
MethodBehavior
cut(from, to)Keep the selected interval.
remove(from, to)Remove an interval and join the remaining audio.
concat(&other)Join matching sample rates and channel counts.
pad(before, after)Add silence around a buffer.
reverse()Reverse frames in every channel.
mix(&overlay, offset, gain, extend)Mix at an offset in frames; extend the output when requested.
crossfade_to(&other, duration)Overlap two matching buffers with a crossfade.
to_mono() / to_stereo()Convert to one or two channels.
to_channels(count)Convert to a requested channel count.
map_channels(&mapping)Reorder or duplicate channels by index.
pan(position)Apply stereo panning from −1.0 (left) to 1.0 (right).
split() / Audio::merge(&parts)Separate channels or combine matching buffers into channels.

Parse a timestamp with "01:23.5".parse::<TimePosition>()?, then pass the result to an edit method. Other accepted forms include seconds and "250ms".

All Audio methods

Signal processing

Gain and filters work on floating-point samples. Resampling uses a windowed sinc filter. Fades support linear, equal-power and exponential curves.

A small processing chain
use audiobox::{Audio, FadeCurve, FilterOptions, FilterType,
    LimiterOptions, NormalizeOptions, NormalizeUnit};

fn main() -> audiobox::Result<()> {
    let audio = Audio::silence(2.0, 2, 44_100)?;
    let processed = audio
        .remove_dc_offset()
        .filter(FilterOptions::new(FilterType::Highpass, 80.0))?
        .gain_db(-3.0)?
        .fade(0.1, 0.3, FadeCurve::EqualPower)?
        .resample(48_000)?;

    let normalized = processed.normalize(NormalizeOptions {
        to: -16.0,
        unit: NormalizeUnit::Lufs,
        peak_ceiling_db: -1.0,
    })?;
    let limited = normalized.limit(LimiterOptions::default())?;
    assert_eq!(limited.sample_rate(), 48_000);
    Ok(())
}
MethodBehavior
gain(linear) / gain_db(db)Apply linear or decibel gain.
normalize(options)Normalize peak, true peak or integrated loudness. The default is peak −1 dBFS.
normalize_with_report(options)Return the normalized audio and a report of measured level, gain and ceiling restriction.
filter(options)Low-pass, high-pass, band-pass, notch, peaking, shelving or all-pass biquad.
limit(options)Lookahead limiter with configurable ceiling and release.
remove_dc_offset()Remove each channel's DC component.
detect_silence(options)Return silent frame ranges.
trim_silence(options)Trim silence at the edges.
speed(factor)Change playback speed and pitch together.
tempo(factor)Change duration with WSOLA time stretching.
pitch(semitones)Shift pitch while preserving duration approximately.

Filters require a positive frequency below Nyquist (half the sample rate). Stereo panning and channel edits have their own shape checks. Invalid arguments return an error.

Analysis

Inspect peak, RMS, an oversampled true-peak estimate, integrated loudness, waveforms and frequency content. Level methods return decibels; silent audio may return negative infinity.

Waveform and spectrum
use audiobox::{Audio, analyze};

fn main() -> audiobox::Result<()> {
    let audio = Audio::silence(1.0, 1, 48_000)?;
    let waveform = audio.waveform(256)?;
    let spectrum = analyze::spectrum(audio.channel_data(0)?,
        audio.sample_rate(), 2048)?;
    let loudness = audio.loudness();

    println!("Peak: {} dBFS", audio.peak_db());
    println!("RMS: {} dBFS", audio.rms_db());
    println!("Integrated: {} LUFS", loudness.integrated);
    println!("Buckets: {}, bins: {}",
        waveform.min.len(), spectrum.frequencies.len());
    Ok(())
}

waveform(buckets) returns min/max envelopes across channels. analyze::spectrum and analyze::spectrogram use an FFT size that must be a supported power of two. A spectrogram also takes a positive hop size in frames.

Integrated loudness uses K-weighting and gating. The range field is a legacy 400 ms block percentile estimate, not certified EBU R128 loudness range. The true-peak method is also an estimate.

Streaming

stream::Decoder decodes WAV, FLAC, MP3, Ogg Vorbis or Opus held in memory a chunk at a time, so music plays without decoding it whole. Its output equals decode sample for sample, seek and rewind move anywhere, and its codec workspace is bounded independently of duration. read allocates nothing, so an audio thread can call it.

Decode as it plays
use audiobox::stream::Decoder;

fn main() -> audiobox::Result<()> {
    let bytes = std::fs::read("music.mp3")?;
    let mut music = Decoder::new(bytes)?;
    println!("{} Hz, {} channels", music.sample_rate(), music.channels());

    let mut buffer = vec![0.0f32; 1024 * music.channels()];
    loop {
        let frames = music.read(&mut buffer)?; // Interleaved frames; 0 at the end.
        if frames == 0 {
            break;
        }
    }
    music.rewind()?; // Loop without decoding anything again.
    music.seek(44_100)?; // Or move to any frame.
    Ok(())
}

Raw PCM

The PCM decoder buffers incomplete frames between byte chunks; the resampler retains its filter history across chunks.

Decode and resample incrementally
use audiobox::{Endian, Limits, SampleFormat};
use audiobox::stream::{PcmDecoder, Resampler};

fn main() -> audiobox::Result<()> {
    let mut decoder = PcmDecoder::new(SampleFormat::S16,
        Endian::Little, 2, 48_000, Limits::default())?;
    let mut resampler = Resampler::new(48_000, 16_000,
        2, Limits::default())?;
    let pcm = vec![0_u8; 48_000 * 2 * 2];

    for chunk in pcm.chunks(1023) {
        let decoded = decoder.push(chunk)?;
        let output = resampler.push(&decoded)?;
        println!("{} output frames", output.frames());
    }
    decoder.finish()?;          // Reject an incomplete final frame.
    let tail = resampler.finish()?; // Drain the final filter tail.
    println!("{} final frames", tail.frames());
    Ok(())
}

PcmEncoder::new(format, endian) converts an Audio chunk to bytes via push(&audio). The sample rate and channel count must remain consistent within a stream.

Streaming reference

Game audio

Optional features give a game its sound. Enable the ones you use: audiobox = { version = "0.5", features = ["mixer", "synth", "sfx"] }. They add modules, not dependencies.

Version 0.4 adds bus effect chains, synthwave unison/filter/LFO instruments and stereo chords, synthesized drums and tracker instruments. Optional music, events, assets and file-stream features add adaptive music, named text events, cached loading and background disk streaming. The mixer also supports 3D emitters, Doppler and surround. See the complete 0.5 API reference for configuration and bounds.

Version 0.5 adds bounded virtual voices and distance culling, prepared group buses and shared effect sends, exact prepared synth timing and held-note controls. Offline delivery gains deterministic LUFS/true-peak normalization, true-peak limiting, seeded integer dithering and numerical render measurements. Vorbis and Opus import now support whole and incremental decoding with exact replay seeks.

Mixer

The mixer feature splits into a Mixer for the audio callback and a MixerHandle for the game. Voices play shared decoded audio or streams, with gain, constant-power pan, loops and fades, through named buses. At most max_voices play; the lowest priority gives way. render allocates nothing, takes no lock and does not block, and handle.at(frame) schedules changes at exact frames.

Music and effects
use audiobox::mixer::{Fade, Loop, Mixer, MixerConfig, Play};
use audiobox::stream::Decoder;
use std::sync::Arc;

fn main() -> audiobox::Result<()> {
    let (mut mixer, mut handle) = Mixer::new(MixerConfig {
        buses: vec!["music".into(), "effects".into()],
        ..MixerConfig::default() // 48 kHz stereo, 32 voices.
    })?;
    let music = handle.bus("music").unwrap();
    let effects = handle.bus("effects").unwrap();

    // Game thread: stream music, share decoded effects.
    let song = Decoder::new(std::fs::read("music.flac")?)?;
    let looping = Play { bus: music, looping: Loop::Whole, priority: 255, ..Play::default() };
    handle.play(song, &looping)?;
    let jump = Arc::new(audiobox::read_audio_file("jump.wav")?);
    handle.play(jump, &Play { bus: effects, pan: -0.4, ..Play::default() })?;
    handle.set_bus_gain(music, 0.5, Fade::linear(0.5))?;

    // Audio thread: once per device buffer.
    let mut buffer = vec![0.0f32; 2 * 512];
    mixer.render(&mut buffer);

    // Game thread, once per frame: events, and freeing finished voices.
    while let Some(event) = handle.poll_event() {
        println!("{event:?}");
    }
    Ok(())
}

Synthesizer

The synth feature renders text scores in an 8-bit style: pulse, triangle, sawtooth and noise channels with envelopes, vibrato, arpeggios and slides; repeats, patterns and a loop point. Parse errors give a line and a column. Synth renders the same samples chunk by chunk for the mixer.

A song from text
use audiobox::synth::Score;

fn main() -> audiobox::Result<()> {
    let score: Score = "
        tempo 150
        instrument lead wave=pulse duty=25 adsr=5,60,70,40 vibrato=12,160,200
        instrument bass wave=triangle
        channel melody = @lead o5 l8 L [c e g >c< g e]2 c2
        channel bass   = @bass o3 l2 c g c1
    ".parse()?;
    let audio = score.render(44_100)?;
    audiobox::write_audio_file("song.wav", &audio, &Default::default())?;
    Ok(())
}

Sound effects

The sfx feature describes an effect with a few parameters, writes and reads them as text, and draws presets such as a jump, a coin or an explosion from a seed.

Effects from seeds and text
use audiobox::sfx::{Effect, Preset};

fn main() -> audiobox::Result<()> {
    // Draw an effect from a seed; the same seed gives the same sound everywhere.
    let jump = Effect::preset(Preset::Jump, 7);
    println!("{jump}"); // Its text form, to keep in your repository.

    let coin: Effect = "wave=pulse frequency=987.8 sustain=0.05 punch=0.5 decay=0.3
                        arpeggio=5 arpeggio-time=0.07".parse()?;
    audiobox::write_audio_file("coin.wav", &coin.render(44_100)?, &Default::default())?;
    Ok(())
}

Try both in the playground.

Errors & limits

Fallible operations return audiobox::Result<T>. Match the stable ErrorCode to make decisions; the human-readable message supplies context.

Bound an untrusted decode
use audiobox::{DecodeOptions, ErrorCode, Limits,
    decode_with_options};

fn main() {
    let options = DecodeOptions {
        limits: Limits {
            max_duration_seconds: 60.0,
            max_channels: 2,
            max_decoded_bytes: 32 * 1024 * 1024,
            ..Limits::default()
        },
        ..DecodeOptions::default()
    };

    match decode_with_options(b"not an audio file", &options) {
        Ok(audio) => println!("{} frames", audio.frames()),
        Err(error) => match error.code {
            ErrorCode::LimitExceeded => eprintln!("Choose a smaller clip"),
            _ => eprintln!("Could not decode: {error}"),
        },
    }
}

Default limits allow up to 512 MiB of decoded PCM, six hours, 64 channels and sample rates between 1 kHz and 768 kHz. These are allocation safeguards, not a guarantee that every codec supports every rate or channel count. Set stricter limits for your application.

ErrorCode is non-exhaustive. Include a fallback arm when matching it.

Format support

Support is implemented in Rust inside the crate. FFmpeg is used for optional interoperability tests, not by the library at runtime.

FormatDecodeEncode
WAV / RF64 / BW64PCM, IEEE float, G.711, IMA ADPCMWAV PCM, float, G.711
AIFF / AIFF-CPCM, sowt, float, G.711AIFF / AIFF-C where needed
CAFPCM, G.711; 64-bit chunk sizesPCM, G.711
AUPCM, float, G.711PCM, float, G.711
FLACFixed/LPC, Rice, stereo decorrelation, checksumsFixed predictor and Rice coding; levels 0–8
MP3MPEG-1 / 2 / 2.5 Layer III; CBR/VBR; gapless tagsMPEG-1 CBR; mono/stereo at 32, 44.1 or 48 kHz
Ogg Vorbis I1–8 channels; floor 0/1, residue 0/1/2; granule trim; exact replay seeksNot supported
Ogg OpusSILK / CELT / hybrid; 1–8 channels at 48 kHz; pre-skip and replay seeks—

Scroll horizontally to see all format columns.

MP3 frame information is available through mp3::probe_mp3(&bytes), including encoder delay and padding when tags are present.

Release limitations

Version 0.5 is an independent Rust release. Its API and capabilities do not exactly match the TypeScript package.

  • MP3 encoding uses a basic CBR, long-block model. There is no VBR encoder, psychoacoustic tuning, transient block switching or encoder bit reservoir.
  • FLAC level 0 uses verbatim frames; levels 1–8 currently share the same fixed-predictor search.
  • Seeded TPDF dithering is opt-in for integer PCM/FLAC exports. Sony Wave64 and RF64/BW64 writing remain unsupported. Incremental decoding covers WAV, FLAC, MP3 and single-link Ogg Vorbis/Opus; AIFF, CAF and AU use whole import. Ogg seeking replays from the beginning.
  • The mixer does not open audio devices: call its render from the audio callback of the library that does.
  • Structured metadata writing supports MP3 ID3v2.4, FLAC and WAV INFO; unsupported fields or container combinations are errors.
  • Loudness range and true peak are estimates; see the analysis notes.
  • Buffer transforms and decoding are synchronous. Optional assets and file-stream features move loading and native disk reads to worker threads. Filesystem helpers need a native filesystem; the browser sandbox uses byte buffers instead.

The sandbox runs a WebAssembly build of the Rust crate, with a small separate bridge and stricter limits for browser memory. It demonstrates selected operations, not an arbitrary Rust compiler. Read the source or open an issue for a missing capability.