rusty_mp3 0.4.1

Pure-Rust MP3 (MPEG-1/2/2.5 Layer III) decoder + encoder, no C/FFI. Decoder bit-exact vs FFmpeg; the first pure-Rust MP3 encoder (CBR/VBR, joint stereo, bit reservoir).
Documentation

rusty_mp3

Remade With Rust By Mata Network License: Apache-2.0

A pure-Rust MP3 (MPEG-1/2/2.5 Audio Layer III) decoder and encoder. Zero dependencies, no C, no FFI, Apache-2.0. MP3's patents expired in 2017 — the format is royalty-free everywhere.

  • Decoder: full MPEG-1/2/2.5 Layer III — bit reservoir, all stereo modes (including mid/side and intensity), alias reduction, hybrid IMDCT, polyphase synthesis. Bit-exact against FFmpeg on our conformance corpus.
  • Encoder: to our knowledge the first pure-Rust MP3 encoder on crates.io — existing options are FFI bindings to LAME. MPEG-1/2/2.5, CBR and VBR, mono/stereo/joint (mid/side) stereo, psychoacoustic model with transient block switching, and a bit reservoir (default-on for MPEG-1 CBR ≤ 256 kbps) that puts it at PEAQ parity with LAME across 96–256 kbps on the content we've measured. Known gap: the reservoir is disabled at 320 kbps and for MPEG-2/2.5 (fixed-frame path is used there instead).

Decode

use rusty_mp3::{Mp3Decoder, Error};

fn main() -> Result<(), Error> {
    let bytes = std::fs::read("input.mp3").expect("read input");

    let mut dec = Mp3Decoder::new();
    dec.push(&bytes); // feed any chunking you like; the decoder frame-syncs
    dec.flush();      // signal end of input

    let mut pcm = Vec::new(); // interleaved f32 in [-1, 1]
    loop {
        match dec.next_frame() {
            Ok(frame) => {
                println!("{} Hz, {} ch", frame.sample_rate, frame.channels);
                pcm.extend_from_slice(&frame.samples);
            }
            Err(Error::Again) => break, // need more input (streaming)
            Err(Error::Eof) => break,   // flushed and fully drained
            Err(e) => return Err(e),
        }
    }
    println!("decoded {} samples", pcm.len());
    Ok(())
}

Encode

use rusty_mp3::{Mp3Encoder, Mp3EncoderConfig, Error};

fn main() -> Result<(), Error> {
    // 2 s of a 440 Hz sine, mono 44.1 kHz.
    let sr = 44100u32;
    let pcm: Vec<f32> = (0..2 * sr)
        .map(|i| 0.5 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sr as f32).sin())
        .collect();

    let mut enc = Mp3Encoder::new(Mp3EncoderConfig {
        bitrate_kbps: 192, // 0 = default (128); snapped to the valid table
        vbr_quality: None, // Some(rusty_mp3::vbr_quality_index(3.0)) for VBR -q:a 3
    });
    enc.push_pcm_f32(&pcm, 1, sr)?; // also: push_pcm_s16 for i16 input
    enc.finish(); // tail padding, reservoir assembly, Xing/Info header

    let mut mp3 = Vec::new();
    while let Ok(packet) = enc.next_packet() {
        mp3.extend_from_slice(&packet);
    }
    std::fs::write("out.mp3", mp3).expect("write output");
    Ok(())
}

The pull calls follow FFmpeg's EAGAIN/EOF drain protocol: Err(Error::Again) means "feed more input", Err(Error::Eof) means the flushed stream is fully drained. Lower-level building blocks (frame-level Mp3Decode/Mp3Encode, header::FrameHeader, bit I/O, ISO tables) are public too.

Performance

Measured on a real 6:53 stereo 44.1 kHz music track (412.9 s, 15,806 frames) at CBR 192 kbps.

0.4.0 caches the psychoacoustic model's FFT twiddle factors — they depend only on the transform size, but were rebuilt on every call, which cost ~1.26 M cos/sin evaluations across the track to produce ten distinct values — and reuses the mid/side scratch across frames instead of reallocating it:

before after
encode CPU (median of 41 pairs) 7,047 ms 6,766 ms
allocations per frame 32.06 21.06
zero-filled allocations / 800 frames 8,000 2

1.045× faster encode, 33/41 paired wins, z = 3.90. The output is byte-identical across the change (same md5 over the full track), so both arms are provably doing the same work rather than one of them doing less. Decode is untouched at 5.04 allocations per frame — its per-block path was already allocation-free.

Method: pinned to one core at High priority, CPU time rather than wall, arms ABBA-interleaved, 41 pairs, with a null arm (the same binary against itself) reading 1.017 as the session's resolution floor. This is a same-binary-family delta, not a cross-implementation ratio.

The allocation counts are reproducible with the bundled instrument, which counts through whichever global allocator the binary sets:

cargo run -p rusty_mp3 --release --example allocaudit -- 800 192

Decode — 0.4.1

Three structural changes, measured on a real 27.5-minute stereo stream encoded by LAME (a decoder benchmarked on its own encoder's output skips paths that encoder never emits, so provenance matters):

brick change effect
bit reader peek(n) loaded eight bytes and shifted, instead of looping once per bit huffman 954 → 706 ms
synthesis V FIFO addressed circularly instead of shifted (a 960-float memmove per pass, ~17.5 GB per track), plus a transposed window loop so the operands are contiguous synthesis 1008 → 857 ms
IMDCT exact kernel symmetry — half the dot products are derived, 648 → 324 MACs per subband imdct share 31.9% → 19.9%

1.185× faster decode overall, 39/41 paired wins, z = 5.78, against a null arm of 1.006. Measured directly rather than by chaining the per-brick ratios, which would have overstated it as 1.233×.

All three are bit-identical, not merely close. The IMDCT one is the surprise: halving the work costs no precision because the symmetries (cos36[17−n][k] == −cos36[n][k], cos36[53−n][k] == +cos36[n][k], and the same pair in the 12-point short-block kernel) hold exactly in the stored f32 tables, and IEEE multiplication and round-to-nearest are sign-symmetric — so a mirrored sum is exactly the negation of the computed one.

Verified byte-identical over a 15-stream corpus spanning joint/true-L-R/mono, MPEG-1 (44.1/48/32 kHz), MPEG-2 (22.05/24 kHz — the 576-sample granule path), MPEG-2.5 (11.025 kHz), 128–320 kbps CBR plus VBR, and four content classes. Short blocks are 15–37% of granules there; mixed blocks are 0% on every stream because LAME never emits them, so that path is gated separately against a dense reference implementation instead of being assumed covered.

cargo run -p rusty_mp3 --release --example decprof -- input.mp3

Part of Remade With Rust

This crate is the standalone MP3 engine of remade_ffmpeg_rs — a ground-up, permissively-licensed Rust rebuild of FFmpeg: a drop-in ffmpeg/ffprobe CLI on pure-Rust codecs, with no copyleft. Also check out our sister project FFAI — media for an AI-first world — and the rest of github.com/remade-with-rust, including the sibling codec crates rusty_h264, rusty_vp9, rusty_aac, rusty-opus, rusty_vorbis, and the rusty-av1-toolkit forks.

About Mata Network

Mata Network builds sovereign, self-hostable infrastructure. Remade With Rust is our open-source home for the permissively-licensed building blocks that work depends on.

License

Apache-2.0. See the workspace LICENSE.