rusty_mp3
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 ;
Encode
use ;
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:
VBR correctness — 0.5.0
If you use -q:a / vbr_quality, upgrade. Every release up to and
including 0.4.1 produced VBR streams that FFmpeg rejects (invalid new backstep -1) and that decode to noise. Three stacked defects:
- the masking thresholds (FFT power domain) were compared directly against quantization noise (MDCT domain), scales ~10⁴ apart — so the gain search saturated at the coarsest setting for 97.5% of granules and every quality setting produced the same ~39 kbps;
- the quality scale was inverted (NMR ≥ 1 means noise at or above the masking threshold, so even the best setting asked for audible noise);
- with those fixed, quality could demand more bits than the largest legal frame holds, and the overflow corrupted the bit-reservoir back-pointer.
Measured on 60 s of real guitar, ours at each -q:a:
-q:a |
kbps | SNR | FFmpeg decode |
|---|---|---|---|
| 0 | 315.1 | 54.47 dB | clean |
| 4 | 300.3 | 47.34 dB | clean |
| 9 | 143.8 | 9.01 dB | clean |
CBR is unaffected and byte-identical across the change.
Two known limits, stated rather than implied: short blocks still take the CBR
budget rather than the quality target (short-block masking thresholds are not
modelled yet), and the ladder sits above LAME's at matched -q:a, which is a
rate-quality calibration question needing PEAQ rather than SNR.
Decode — 0.5.0 (SIMD)
Two AVX kernels in the synthesis filterbank, both bit-identical to their scalar twins (each output owns a lane and accumulates in the original order; separate mul+add, never FMA). Runtime-detected, with the scalar paths kept as oracles and as the fallback.
| kernel | share of the win |
|---|---|
matrixing (matrixing_avx) |
1.162× whole decode, 31/31 pairs, z = 5.57 |
windowing (window_avx) |
1.019×, 23/31 pairs, z = 2.69 |
The gap between those two is the useful part: same stage, same instruction set, same effort, 16.2% versus 1.9%. Auto-vectorization had produced 0 packed ops against 1698 scalar ones in this kernel, but "the stage is hot" was still not enough to aim a kernel — it took the split within the stage.
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.
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.