Files
filmcraft/crates/mpa/README.md
vangel76andClaude Opus 5.5 39afd2f6c3 #809: own MPEG audio Layer I / II decoder plays MP2 at ffmpeg's default bit rates
Problem: symphonia rejects every Layer II frame whose bit rate / mode
combination ISO/IEC 11172-3 does not allow (mono above 192 kbit/s, stereo
below 64 kbit/s). ffmpeg writes mono MP2 at 384 kbit/s by default, so such
tracks played as silence in MPEG-TS / PS, Matroska and raw .mp2 files.

Fix: new L0 crate filmcraft-mpa, a clean-room Layer I / II decoder written
from ISO/IEC 11172-3 and 13818-3 (all modes incl. joint stereo, MPEG-1/2/2.5
rates, the disallowed combinations decoded with the allocation table their
bit rate per channel selects). PacketDecoder::mpeg_audio uses it for layers
I and II (packets of several frames too); raw .mp2 files decode with it,
resyncing after damage. Oracle tests vs ffmpeg: 73-84 dB SNR on ffmpeg /
twolame streams and synthetic Layer I / joint stereo frames; fuzzed damaged
frames never panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:48:45 +02:00

77 lines
4.4 KiB
Markdown

# filmcraft-mpa
Clean-room MPEG-1 / MPEG-2 Audio Layer I and Layer II decoder (MP1 / MP2) in pure Rust. Layer L0:
depends only on `filmcraft-bitstream` and `thiserror`. Builds for `wasm32-unknown-unknown`; no
`unsafe`. No GPL/LGPL code (FFmpeg, libmad, mpg123, twolame) was consulted; FFmpeg (and twolame,
when installed) are used only as external fixture generators and test oracles.
## Why
symphonia (the bootstrap decoder for MP3 and others) rejects every Layer II frame whose bit rate /
channel mode combination ISO/IEC 11172-3 does not allow: mono above 192 kbit/s and stereo below
64 kbit/s. ffmpeg writes such streams by default (mono MP2 is 384 kbit/s unless asked otherwise),
so those tracks played as silence in every container (#809).
## Specification
ISO/IEC 11172-3:1993: §2.4.1 bit stream syntax (header, Layer I and Layer II audio data),
§2.4.2 semantics, §2.4.3.1 frame length, §2.4.3.2 synthesis subband filter (Figure A.2),
§2.4.3.3 Layer I and §2.4.3.4 Layer II decoding; Annex B: Table 3-B.1 scalefactors (computed as
2^(1 - i/3)), Table 3-B.2a-d Layer II allocation tables, Table 3-B.3 synthesis window (the 257
coefficients `D[0..=256]`, exact multiples of 2⁻¹⁶, the rest from the prototype's symmetry;
a unit test checks the typed table is the smooth prototype), Table 3-B.4 quantization classes.
ISO/IEC 13818-3: the lower sampling frequencies (16 / 22.05 / 24 kHz), their bit rate tables and
the Layer II allocation table B.1 (its rows confirmed against FFmpeg's decode).
## API
```rust
let mut dec = filmcraft_mpa::Decoder::new();
let h = filmcraft_mpa::parse_header(frame)?; // layer, rate, mode, frame size
let out = dec.decode(frame)?; // h.samples() per channel (384 or 1152)
dec.reset(); // before decoding from another position
```
## Decoder
- Layer I and Layer II; mono, stereo, dual channel and joint (intensity) stereo with every bound;
MPEG-1, MPEG-2 lower sampling frequencies and the unofficial MPEG-2.5; with or without CRC.
- Layer II table selection by sampling frequency and bit rate per channel, with the ranges of
Table 3-B.2 taken as open-ended so the disallowed combinations decode the way encoders write
them (mono 384 kbit/s uses table a at 48 kHz, b at 44.1 / 32 kHz; stereo 32 / 48 kbit/s uses
c, or d at 32 kHz).
- Requantization `(2v + 1 - steps) / steps` (the standard's `C · (s''' + D)` for every class),
grouped 3-, 5- and 9-step codewords, scalefactor selection information.
- Output: planar f32 at full scale ±1.
- Errors (forbidden allocation, reserved header fields, data running past the frame) return
`Err`; garbage never panics (unit fuzz test and damaged-frame oracle test).
## Accuracy (vs FFmpeg as an external oracle)
`tests/oracle.rs`, worst channel SNR against FFmpeg's float decoder. FFmpeg's own fixed-point
decoder agrees with its float decoder to ~77 dB (the error sits at the 16-bit LSB); the test
requires 70 dB, and a wrong table entry or misread field costs tens of dB.
| Stream | SNR |
|---|---|
| Layer II mono 48 kHz, FFmpeg's default 384 kbit/s (not allowed by 11172-3) | 77.1 dB |
| Layer II mono 44.1 kHz 320 kbit/s pink noise (not allowed) | 72.9 dB |
| Layer II stereo 48 kHz 32 kbit/s (table c; not allowed) | 83.2 dB |
| Layer II stereo 48 kHz 192 kbit/s, 44.1 kHz 256 / 96 kbit/s, 32 kHz 128 kbit/s | 83.3-83.6 dB |
| Layer II mono 32 kHz 48 kbit/s (table d) | 76.9 dB |
| Layer II MPEG-2: stereo 24 kHz 160 kbit/s, mono 22.05 kHz 64 kbit/s, stereo 16 kHz 32 kbit/s | 76.9-83.5 dB |
| Layer II joint stereo and dual channel from twolame (when installed) | 83.4-83.6 dB |
| Synthetic valid random frames: Layer I mono / stereo / dual / joint stereo at every bound, MPEG-1 and MPEG-2; Layer II joint stereo at every bound and dual channel with every allocation table | 74.1-80.0 dB |
No installed encoder writes Layer I, so Layer I is checked on synthetic frames (random
allocations, scalefactors and samples) that FFmpeg decodes as it would any stream. Through
`filmcraft-codecs` (`tests/mpeg_oracle.rs`), mono 384 kbit/s MP2 in MPEG-TS, Matroska and a raw
`.mp2` file is within 3.1·10⁻⁵ of FFmpeg's decode.
## Gaps
- Layer III is not decoded here (symphonia does it); `parse_header` reads its headers.
- The free format (bit rate index 0) is refused: its frame length is only known from the next
sync word.
- The CRC is read, not checked; ancillary data is skipped.