From 39afd2f6c3c8929af2b00ad3f6798753da06d6a9 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 00:48:45 +0200 Subject: [PATCH 1/3] #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 --- Cargo.lock | 10 + Cargo.toml | 1 + crates/codecs/Cargo.toml | 1 + crates/codecs/src/audio.rs | 143 +++++++- crates/codecs/tests/mpeg_oracle.rs | 42 +++ crates/mpa/Cargo.toml | 17 + crates/mpa/README.md | 76 ++++ crates/mpa/src/lib.rs | 409 +++++++++++++++++++++ crates/mpa/src/tables.rs | 189 ++++++++++ crates/mpa/src/tests.rs | 151 ++++++++ crates/mpa/tests/oracle.rs | 572 +++++++++++++++++++++++++++++ docs/architecture.md | 3 +- docs/codec-parity.md | 2 +- docs/gaps.md | 9 +- xtask/src/main.rs | 2 + 15 files changed, 1619 insertions(+), 8 deletions(-) create mode 100644 crates/mpa/Cargo.toml create mode 100644 crates/mpa/README.md create mode 100644 crates/mpa/src/lib.rs create mode 100644 crates/mpa/src/tables.rs create mode 100644 crates/mpa/src/tests.rs create mode 100644 crates/mpa/tests/oracle.rs diff --git a/Cargo.lock b/Cargo.lock index 5529201a..f70d5b16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1819,6 +1819,7 @@ dependencies = [ "filmcraft-isobmff", "filmcraft-matroska", "filmcraft-media", + "filmcraft-mpa", "filmcraft-mpeg2v", "filmcraft-mpegts", "filmcraft-mxf", @@ -2075,6 +2076,15 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "filmcraft-mpa" +version = "0.5.0" +dependencies = [ + "filmcraft-bitstream", + "filmcraft-testkit", + "thiserror 2.0.21", +] + [[package]] name = "filmcraft-mpeg2v" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 1d306cff..46a03f1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ filmcraft-cfb = { path = "crates/cfb" } filmcraft-mpegts = { path = "crates/mpegts" } filmcraft-mpeg2v = { path = "crates/mpeg2v" } filmcraft-ac3 = { path = "crates/ac3" } +filmcraft-mpa = { path = "crates/mpa" } filmcraft-ogg = { path = "crates/ogg" } filmcraft-h264 = { path = "crates/h264" } filmcraft-hevc = { path = "crates/hevc" } diff --git a/crates/codecs/Cargo.toml b/crates/codecs/Cargo.toml index 3a490e2c..ffaeabbc 100644 --- a/crates/codecs/Cargo.toml +++ b/crates/codecs/Cargo.toml @@ -20,6 +20,7 @@ filmcraft-mxf = { workspace = true } filmcraft-mpegts = { workspace = true } filmcraft-mpeg2v = { workspace = true } filmcraft-ac3 = { workspace = true } +filmcraft-mpa = { workspace = true } filmcraft-ogg = { workspace = true } filmcraft-bitstream = { workspace = true } filmcraft-h264 = { workspace = true } diff --git a/crates/codecs/src/audio.rs b/crates/codecs/src/audio.rs index 63351636..bc937efa 100644 --- a/crates/codecs/src/audio.rs +++ b/crates/codecs/src/audio.rs @@ -27,6 +27,8 @@ enum Inner { Symphonia(Box), /// Our AC-3 and E-AC-3 decoder (ATSC A/52, Annex E). Ac3(Box), + /// Our MPEG-1/2 audio Layer I and II decoder (ISO/IEC 11172-3, 13818-3). + Mpa(Box), } /// Opus always decodes at 48 kHz (RFC 7845 §5.1: the input rate in the header is informational). @@ -124,11 +126,12 @@ impl PacketDecoder { let order = if dec.head().mapping_family == 1 { vorbis_to_wav_order(channels) } else { None }; Ok(Self { inner: Inner::Opus { dec: Box::new(dec), order }, channels }) } - /// MPEG-1/2 audio layer I, II or III (symphonia). + /// MPEG-1/2 audio layer I, II (our decoder: symphonia rejects the bit rate / mode + /// combinations ISO/IEC 11172-3 does not allow, such as ffmpeg's default 384 kbit/s mono MP2) + /// or III (symphonia). pub fn mpeg_audio(layer: u8, sample_rate: u32) -> Result { let codec = match layer { - 1 => symphonia::core::codecs::CODEC_TYPE_MP1, - 2 => symphonia::core::codecs::CODEC_TYPE_MP2, + 1 | 2 => return Ok(Self { inner: Inner::Mpa(Box::default()), channels: 0 }), _ => CODEC_TYPE_MP3, }; Self::new(codec, sample_rate, None) @@ -214,6 +217,31 @@ impl PacketDecoder { self.channels = out.len(); return Ok(out); } + Inner::Mpa(d) => { + // a packet holds one frame or several; a damaged first frame is an error, damage + // after a good frame ends the packet + let mut out: Vec> = Vec::new(); + let mut p = 0; + while let Some(rest) = data.get(p..).filter(|r| !r.is_empty()) { + let f = match d.decode(rest) { + Ok(f) => f, + Err(e) if out.is_empty() => return Err(CodecError::Decode(e.to_string())), + Err(_) => break, + }; + p += f.header.frame_bytes; + if out.is_empty() { + out = f.channels; + } else if out.len() == f.channels.len() { + for (o, c) in out.iter_mut().zip(f.channels) { + o.extend(c); + } + } else { + break; + } + } + self.channels = out.len(); + return Ok(out); + } }; let pkt = Packet::new_from_slice(0, ts, 0, data); let buf = dec.decode(&pkt).map_err(|e| CodecError::Decode(e.to_string()))?; @@ -242,6 +270,7 @@ impl PacketDecoder { up.reset(); } } + Inner::Mpa(d) => d.reset(), Inner::Opus { dec, .. } => dec.reset(), Inner::Symphonia(d) => d.reset(), Inner::Ac3(d) => d.reset(), @@ -741,6 +770,22 @@ pub struct AudioFileSource { impl AudioFileSource { pub fn decode(name: &str, bytes: Arc<[u8]>) -> Result { let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase(); + if matches!(ext.as_str(), "mp2" | "mp3") + && let Some((layer, rate, samples)) = decode_mpeg_layer12(&bytes) + { + let frames = samples.first().map_or(0, Vec::len); + let info = MediaInfo { + name: name.to_string(), + kind: MediaKind::AudioOnly, + duration: Tick::from_units(frames as i64, rate as i64), + video: None, + audio_streams: vec![AudioStreamInfo { sample_rate: rate, channels: samples.len() as u32, codec: format!("MP{layer}"), bits_per_sample: None }], + container: ext.to_uppercase(), + start_timecode: None, + file_size: Some(bytes.len() as u64), + }; + return Ok(Self { info, rate, samples }); + } let mss = MediaSourceStream::new(Box::new(std::io::Cursor::new(bytes.clone())), Default::default()); let mut hint = Hint::new(); hint.with_extension(&ext); @@ -796,6 +841,49 @@ impl AudioFileSource { } } +/// A raw MPEG audio Layer I / II stream (`.mp2`) decoded with our decoder: (layer, sample rate, +/// planar samples). Frames follow each other from the first frame header (after an ID3v2 tag); +/// damaged data is skipped up to the next frame header of the same layer and rate, and a frame +/// with another channel count keeps its duration as silence. `None` when the stream does not start +/// with a Layer I / II frame followed by another frame (or the end), e.g. MP3. +fn decode_mpeg_layer12(bytes: &[u8]) -> Option<(u8, u32, Vec>)> { + let mut p = match bytes { + // ID3v2: 10-byte header, a syncsafe size, and a 10-byte footer when flagged + [b'I', b'D', b'3', _, _, flags, s0, s1, s2, s3, ..] => { + let size = [*s0, *s1, *s2, *s3].iter().fold(0usize, |a, &b| (a << 7) | (b & 0x7F) as usize); + 10 + size + if flags & 0x10 != 0 { 10 } else { 0 } + } + _ => 0, + }; + let frame_at = |p: usize| bytes.get(p..).and_then(|b| filmcraft_mpa::parse_header(b).ok()).filter(|h| h.frame_bytes > 0); + let first = frame_at(p)?; + let next = p.saturating_add(first.frame_bytes); + if first.layer == 3 || (next < bytes.len() && frame_at(next).is_none_or(|h| h.layer != first.layer || h.sample_rate != first.sample_rate)) { + return None; + } + let mut dec = filmcraft_mpa::Decoder::new(); + let mut out: Vec> = vec![Vec::new(); first.channels()]; + while let Some(rest) = bytes.get(p..).filter(|r| r.len() >= 4) { + match dec.decode(rest) { + Ok(f) if f.header.layer == first.layer && f.header.sample_rate == first.sample_rate => { + let n = f.header.samples(); + if f.channels.len() == out.len() { + for (o, c) in out.iter_mut().zip(f.channels) { + o.extend(c); + } + } else { + for o in out.iter_mut() { + o.resize(o.len() + n, 0.0); + } + } + p += f.header.frame_bytes; + } + _ => p += 1, + } + } + out.first().is_some_and(|c| !c.is_empty()).then_some((first.layer, first.sample_rate, out)) +} + impl MediaSource for AudioFileSource { fn info(&self) -> &MediaInfo { &self.info @@ -816,6 +904,55 @@ pub fn opener(name: &str, bytes: Arc<[u8]>) -> Option Vec { + let h = [0xFF, 0xFD, bitrate_index << 4 | 0x04, mode << 6]; + let mut f = h.to_vec(); + f.resize(filmcraft_mpa::parse_header(&h).unwrap().frame_bytes, 0); + f + } + + /// #809: mono MP2 at 384 kbit/s (ffmpeg's default for mono) is outside ISO/IEC 11172-3's + /// allowed combinations, and symphonia rejected every frame, so the track played as silence. + #[test] + fn mono_mp2_at_384k_decodes() { + let mut d = PacketDecoder::mpeg_audio(2, 48_000).unwrap(); + let out = d.decode(&silent_frame(14, 3), 0).unwrap(); + assert_eq!((out.len(), out[0].len(), d.channels), (1, 1152, 1)); + // a packet of two stereo frames decodes to both + let two = [silent_frame(10, 0), silent_frame(10, 0)].concat(); + let out = d.decode(&two, 1152).unwrap(); + assert_eq!((out.len(), out[0].len()), (2, 2304)); + // damage: an error for the first frame, the good frames before it otherwise + assert!(d.decode(&[0u8; 8], 0).is_err()); + assert!(d.decode(&silent_frame(10, 0)[..100], 0).is_err()); + let partial = [silent_frame(10, 0), vec![0xFF, 0xFD, 0xA4]].concat(); + assert_eq!(d.decode(&partial, 0).unwrap()[0].len(), 1152); + d.reset(); + } + + /// Raw `.mp2` files: after an ID3v2 tag, frame after frame, skipping damage. + #[test] + fn raw_streams_resync_after_damage() { + let mono = silent_frame(14, 3); + let file = [&b"ID3\x04\0\0\0\0\0\x05abcde"[..], &mono, &mono, &[0xFF, 0xFD, 0x13, 0x37, 0x42], &mono].concat(); + let (layer, rate, samples) = decode_mpeg_layer12(&file).unwrap(); + assert_eq!((layer, rate, samples.len(), samples[0].len()), (2, 48_000, 1, 3 * 1152)); + let src = AudioFileSource::decode("x.mp2", file.clone().into()).unwrap(); + assert_eq!(src.info().audio().map(|a| (a.channels, a.codec.as_str())), Some((1, "MP2"))); + // MP3, junk and truncated data are not ours (symphonia gets them) + assert!(decode_mpeg_layer12(&[0xFF, 0xFB, 0x90, 0x44, 0, 0]).is_none()); + for n in 0..40 { + assert!(decode_mpeg_layer12(&file[..n]).is_none(), "{n}"); + } + assert!(decode_mpeg_layer12(&[&mono[..], &[0x12; 50]].concat()).is_none()); + } +} + #[cfg(test)] mod he_aac_tests { use super::*; diff --git a/crates/codecs/tests/mpeg_oracle.rs b/crates/codecs/tests/mpeg_oracle.rs index d6a7042a..90cc6bd4 100644 --- a/crates/codecs/tests/mpeg_oracle.rs +++ b/crates/codecs/tests/mpeg_oracle.rs @@ -16,6 +16,9 @@ use filmcraft_media::{FrameRequest, MediaError, MediaSource}; const NOISE: &[&str] = &["-f", "lavfi", "-i", "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000"]; +/// A mono tone. +const MONO: &[&str] = &["-f", "lavfi", "-i", "sine=frequency=1000:sample_rate=48000"]; + fn spec(name: &str) -> Vec { let cat = |p: &[&[&str]]| p.concat().into_iter().map(String::from).collect::>(); match name { @@ -142,6 +145,12 @@ fn spec(name: &str) -> Vec { cat(&[&["-f", "lavfi", "-i", "testsrc2=size=320x240:rate=25"], NOISE, &["-t", "0.6", "-c:v", "mpeg2video", "-bf", "2", "-g", "6", "-c:a", "mp2"]]) } "audio.mp2" => cat(&[NOISE, &["-t", "1", "-c:a", "mp2", "-b:a", "224k", "-f", "mp2"]]), + // mono MP2 at ffmpeg's default bit rate, 384 kbit/s, which ISO/IEC 11172-3 does not allow + "audio_mono.mp2" => cat(&[MONO, &["-t", "1", "-c:a", "mp2", "-f", "mp2"]]), + "ts_mpeg2_mono_mp2.ts" => { + cat(&[&["-f", "lavfi", "-i", "testsrc2=size=320x240:rate=25"], MONO, &["-t", "1", "-c:v", "mpeg2video", "-c:a", "mp2", "-f", "mpegts"]]) + } + "mkv_mono_mp2.mkv" => cat(&[MONO, &["-t", "1", "-c:a", "mp2"]]), "es_mpeg2.m2v" => { cat(&[&["-f", "lavfi", "-i", "testsrc2=size=352x288:rate=25"], &["-t", "1", "-c:v", "mpeg2video", "-bf", "2", "-g", "9", "-f", "mpeg2video"]]) } @@ -164,6 +173,9 @@ pub const ALL: &[&str] = &[ "mp4_mpeg2.mp4", "mkv_mpeg2.mkv", "audio.mp2", + "audio_mono.mp2", + "ts_mpeg2_mono_mp2.ts", + "mkv_mono_mp2.mkv", "es_mpeg2.m2v", ]; @@ -515,6 +527,36 @@ fn standalone_mp2_file() { assert!(worst <= 2e-4, "{worst}"); } +/// #809: mono MP2 at 384 kbit/s, ffmpeg's default, played as silence in every container because +/// symphonia rejects the bit rate / mode combinations ISO/IEC 11172-3 does not allow. +#[test] +fn mono_mp2_at_ffmpegs_default_bit_rate() { + let ff = filmcraft_testkit::require_ffmpeg!(); + let f = make(&ff, "audio_mono.mp2"); + assert_eq!(std::fs::read(&f).unwrap()[2] >> 4, 14, "384 kbit/s"); + let worst = check_audio(&ff, &make(&ff, "ts_mpeg2_mono_mp2.ts"), 2e-4); + println!("ts_mpeg2_mono_mp2.ts: max error {worst}"); + // Matroska: ffmpeg trims the 481-sample encoder delay; we trim `CodecDelay` rounded to the + // millisecond (#790), so allow a lag of a sample or two there + for (name, max_lag) in [("audio_mono.mp2", 0i64), ("mkv_mono_mp2.mkv", 2)] { + let f = make(&ff, name); + let src = filmcraft_codecs::open_bytes(name, bytes(&f)).unwrap(); + let a = src.info().audio().cloned().unwrap(); + assert_eq!((a.sample_rate, a.channels), (48_000, 1), "{name}"); + let want = ffmpeg_audio_f32(&ff, &f, &[]); + let n = want.len() - 4; + let worst = (-max_lag..=max_lag) + .map(|lag| { + let buf = src.audio(2 + lag, n, 48_000).unwrap(); + want[2..].iter().zip(&buf.channels[0]).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max) + }) + .fold(f32::INFINITY, f32::min); + let peak = want.iter().fold(0f32, |m, x| m.max(x.abs())); + println!("{name}: max error {worst}, peak {peak}"); + assert!(peak > 0.1 && worst <= 2e-4, "{name}: peak {peak}, max error {worst}"); + } +} + #[test] fn truncated_and_corrupt_files_never_panic() { let ff = filmcraft_testkit::require_ffmpeg!(); diff --git a/crates/mpa/Cargo.toml b/crates/mpa/Cargo.toml new file mode 100644 index 00000000..8abf08c2 --- /dev/null +++ b/crates/mpa/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "filmcraft-mpa" +description = "Clean-room MPEG-1/2 Audio Layer I and Layer II (MP1 / MP2) decoder" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +filmcraft-bitstream = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +filmcraft-testkit = { workspace = true } diff --git a/crates/mpa/README.md b/crates/mpa/README.md new file mode 100644 index 00000000..caa1afde --- /dev/null +++ b/crates/mpa/README.md @@ -0,0 +1,76 @@ +# 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. diff --git a/crates/mpa/src/lib.rs b/crates/mpa/src/lib.rs new file mode 100644 index 00000000..4dd32814 --- /dev/null +++ b/crates/mpa/src/lib.rs @@ -0,0 +1,409 @@ +//! Clean-room MPEG-1 / MPEG-2 Audio Layer I and Layer II decoder (MP1 / MP2), written from +//! ISO/IEC 11172-3 (§2.4 bit stream syntax and decoding, Annex B tables) and ISO/IEC 13818-3 +//! (the lower sampling frequencies 16 / 22.05 / 24 kHz). +//! +//! - Layer I and Layer II; mono, stereo, dual channel and joint (intensity) stereo; every bit rate +//! and sampling frequency of MPEG-1, MPEG-2 and the unofficial MPEG-2.5 (8 / 11.025 / 12 kHz). +//! - Lenient where encoders are: Layer II bit rate / mode combinations that ISO/IEC 11172-3 does not +//! allow (mono above 192 kbit/s, which is ffmpeg's default for mono, and stereo below 64 kbit/s) +//! decode with the allocation table their bit rate per channel selects (see [`Header`]). +//! - Output: planar f32 at full scale ±1, 384 (Layer I) or 1152 (Layer II) samples per channel. +//! +//! Not decoded: Layer III (use an MP3 decoder; [`parse_header`] still reads its headers), the free +//! format (bit rate index 0, whose frame length is only known from the next sync word) and the +//! CRC (read, not checked, like most decoders). + +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))] + +mod tables; + +use filmcraft_bitstream::BitReader; +use tables::AllocTable; + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + #[error("not an MPEG audio frame")] + NoSync, + #[error("unsupported MPEG audio: {0}")] + Unsupported(&'static str), + #[error("invalid MPEG audio frame: {0}")] + Invalid(&'static str), +} + +pub type Result = std::result::Result; + +/// MPEG audio version (`ID` bit, and the MPEG-2.5 extension of the sync word). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Version { + /// ISO/IEC 11172-3: 32 / 44.1 / 48 kHz. + Mpeg1, + /// ISO/IEC 13818-3 lower sampling frequencies: 16 / 22.05 / 24 kHz. + Mpeg2, + /// The unofficial extension to 8 / 11.025 / 12 kHz. + Mpeg25, +} + +/// Channel mode (`mode`, §2.4.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mode { + Stereo, + /// Intensity stereo above the bound given by `mode_extension` (Layers I and II). + JointStereo, + /// Two independent mono channels. + DualChannel, + Mono, +} + +/// The 32-bit frame header (§2.4.1.3, §2.4.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Header { + pub version: Version, + /// 1, 2 or 3. + pub layer: u8, + /// A 16-bit CRC follows the header. + pub crc: bool, + /// 0 for the free format. + pub bitrate_kbps: u32, + pub sample_rate: u32, + pub padding: bool, + pub mode: Mode, + pub mode_extension: u8, + /// Bytes in the frame, header included (0 for the free format). + pub frame_bytes: usize, +} + +impl Header { + /// Output channels: 1 for mono, else 2. + pub fn channels(&self) -> usize { + if self.mode == Mode::Mono { 1 } else { 2 } + } + /// Samples per channel the frame decodes to. + pub fn samples(&self) -> usize { + match (self.layer, self.version) { + (1, _) => 384, + (3, Version::Mpeg2 | Version::Mpeg25) => 576, + _ => 1152, + } + } + /// The first subband coded as intensity stereo (`bound`, §2.4.2.3): `mode_extension` 0-3 gives + /// 4, 8, 12 or 16 in joint stereo; otherwise every subband is coded per channel. + fn bound(&self) -> usize { + if self.mode == Mode::JointStereo { 4 * (self.mode_extension as usize + 1) } else { 32 } + } +} + +/// Parse the frame header at the start of `b`. +pub fn parse_header(b: &[u8]) -> Result
{ + let [0xFF, b1, b2, b3, ..] = *b else { + return Err(if b.len() < 4 && b.first().is_none_or(|&x| x == 0xFF) { Error::Invalid("truncated header") } else { Error::NoSync }); + }; + if b1 & 0xE0 != 0xE0 { + return Err(Error::NoSync); + } + let version = match (b1 >> 3) & 3 { + 3 => Version::Mpeg1, + 2 => Version::Mpeg2, + 0 => Version::Mpeg25, + _ => return Err(Error::Invalid("reserved version")), + }; + let layer = match (b1 >> 1) & 3 { + 3 => 1, + 2 => 2, + 1 => 3, + _ => return Err(Error::Invalid("reserved layer")), + }; + let crc = b1 & 1 == 0; + let bitrate_index = (b2 >> 4) as usize; + let rate_index = ((b2 >> 2) & 3) as usize; + let padding = (b2 >> 1) & 1 == 1; + let mode = match b3 >> 6 { + 0 => Mode::Stereo, + 1 => Mode::JointStereo, + 2 => Mode::DualChannel, + _ => Mode::Mono, + }; + let mode_extension = (b3 >> 4) & 3; + let base = *tables::SAMPLE_RATES.get(rate_index).ok_or(Error::Invalid("reserved sampling frequency"))?; + let sample_rate = match version { + Version::Mpeg1 => base, + Version::Mpeg2 => base / 2, + Version::Mpeg25 => base / 4, + }; + let rates = match (version, layer) { + (Version::Mpeg1, 1) => &tables::BITRATES_MPEG1_L1, + (Version::Mpeg1, 2) => &tables::BITRATES_MPEG1_L2, + (Version::Mpeg1, _) => &tables::BITRATES_MPEG1_L3, + (_, 1) => &tables::BITRATES_LSF_L1, + _ => &tables::BITRATES_LSF_L23, + }; + let bitrate_kbps = *rates.get(bitrate_index).ok_or(Error::Invalid("forbidden bit rate index"))?; + // §2.4.3.1: Layer I slots are 4 bytes, 12 slots per 384-sample frame per bit/s over the + // sampling frequency; Layers II / III have 1-byte slots, 144 per 1152 samples (72 for the + // 576-sample Layer III frames of MPEG-2). + let bits_per_second = u64::from(bitrate_kbps) * 1000; + let rate = u64::from(sample_rate); + let pad = u64::from(padding); + let frame_bytes = if bitrate_kbps == 0 { + 0 + } else if layer == 1 { + (12 * bits_per_second / rate + pad) * 4 + } else { + let slots = if layer == 3 && version != Version::Mpeg1 { 72 } else { 144 }; + slots * bits_per_second / rate + pad + }; + Ok(Header { version, layer, crc, bitrate_kbps, sample_rate, padding, mode, mode_extension, frame_bytes: frame_bytes as usize }) +} + +/// One decoded frame. +#[derive(Clone, Debug)] +pub struct Frame { + pub header: Header, + /// Planar samples, [`Header::channels`] × [`Header::samples`]. + pub channels: Vec>, +} + +/// Subband samples of one frame: `[channel][time slot][subband]`, up to 36 time slots. +type Subbands = [[[f32; 32]; 36]; 2]; + +/// A Layer I / Layer II decoder. It keeps the synthesis filter state between frames, so frames +/// must be fed in order; call [`Decoder::reset`] before decoding from another position. +pub struct Decoder { + /// The synthesis filter's `V` vector per channel (§2.4.3.2, Figure A.2). + v: [[f32; 1024]; 2], + sb: Box, +} + +impl Default for Decoder { + fn default() -> Self { + Self::new() + } +} + +impl Decoder { + pub fn new() -> Self { + Self { v: [[0.0; 1024]; 2], sb: Box::new([[[0.0; 32]; 36]; 2]) } + } + + /// Forget the filter history. + pub fn reset(&mut self) { + self.v = [[0.0; 1024]; 2]; + } + + /// Decode the frame at the start of `frame` (which may run on into the next frames; only + /// [`Header::frame_bytes`] are read). + pub fn decode(&mut self, frame: &[u8]) -> Result { + let header = parse_header(frame)?; + if header.layer == 3 { + return Err(Error::Unsupported("Layer III")); + } + if header.frame_bytes == 0 { + return Err(Error::Unsupported("free-format bit rate")); + } + let data = frame.get(..header.frame_bytes).ok_or(Error::Invalid("truncated frame"))?; + let mut r = BitReader::new(data); + r.skip(32 + if header.crc { 16 } else { 0 }).map_err(|_| Error::Invalid("truncated frame"))?; + let nch = header.channels(); + for ch in self.sb.iter_mut() { + for slot in ch.iter_mut() { + *slot = [0.0; 32]; + } + } + let slots = if header.layer == 1 { + layer1(&mut r, &header, &mut self.sb)?; + 12 + } else { + layer2(&mut r, &header, &mut self.sb)?; + 36 + }; + let window = tables::window(); + let matrix = tables::matrix(); + let mut channels = vec![vec![0f32; slots * 32]; nch]; + for (ch, out) in channels.iter_mut().enumerate() { + let (Some(v), Some(sb)) = (self.v.get_mut(ch), self.sb.get(ch)) else { continue }; + for (slot, o) in sb.iter().take(slots).zip(out.as_chunks_mut::<32>().0) { + synthesize(v, matrix, window, slot, o); + } + } + Ok(Frame { header, channels }) + } +} + +fn read(r: &mut BitReader, n: u32) -> Result { + r.read_bits(n).map_err(|_| Error::Invalid("frame data runs past the frame")) +} + +/// Requantize an `steps`-level code `v` (§2.4.3.3.4 / §2.4.3.4.4): the fractional value with +/// the MSB inverted, `C · (s''' + D)`, which for every class is `(2v + 1 - steps) / steps`. +fn requantize(v: u32, steps: u32) -> f32 { + (2.0 * v as f32 + 1.0 - steps as f32) / steps as f32 +} + +/// Layer I audio data (§2.4.1.5): allocation, scalefactors and 12 samples per subband. +fn layer1(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> { + let nch = h.channels(); + let bound = if nch == 1 { 32 } else { h.bound() }; + let mut alloc = [[0u32; 32]; 2]; + for sb in 0..32 { + if sb < bound { + for ch in 0..nch { + alloc[ch][sb] = read(r, 4)?; + } + } else { + let a = read(r, 4)?; + alloc[0][sb] = a; + alloc[1][sb] = a; + } + } + if alloc.iter().flatten().any(|&a| a == 15) { + return Err(Error::Invalid("forbidden Layer I allocation")); + } + let mut scf = [[0f32; 32]; 2]; + for sb in 0..32 { + for ch in 0..nch { + if alloc[ch][sb] != 0 { + scf[ch][sb] = tables::scalefactor(read(r, 6)?); + } + } + } + for s in 0..12 { + for sb in 0..32 { + if sb < bound { + for ch in 0..nch { + let a = alloc[ch][sb]; + if a != 0 { + let nb = a + 1; + out[ch][s][sb] = requantize(read(r, nb)?, (1 << nb) - 1) * scf[ch][sb]; + } + } + } else if alloc[0][sb] != 0 { + let nb = alloc[0][sb] + 1; + let q = requantize(read(r, nb)?, (1 << nb) - 1); + for ch in 0..nch { + out[ch][s][sb] = q * scf[ch][sb]; + } + } + } + } + Ok(()) +} + +/// Read the three samples of one granule in a subband with `steps` quantization steps. +fn granule(r: &mut BitReader, steps: u16) -> Result<[f32; 3]> { + let steps = u32::from(steps); + let mut q = [0u32; 3]; + if let Some(bits) = tables::grouping(steps as u16) { + // three samples in one codeword: c = s0 + steps·s1 + steps²·s2 (§2.4.3.3.4); what is left + // of an invalid codeword beyond steps³ - 1 is ignored + let mut c = read(r, bits)?; + for s in q.iter_mut() { + *s = c % steps; + c /= steps; + } + } else { + let nb = (steps + 1).trailing_zeros(); + for s in q.iter_mut() { + *s = read(r, nb)?.min(steps - 1); + } + } + Ok(q.map(|v| requantize(v, steps))) +} + +/// Layer II audio data (§2.4.1.6): allocation, scalefactor selection, scalefactors and 12 +/// granules of three samples per subband. +fn layer2(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> { + let nch = h.channels(); + let per_channel = h.bitrate_kbps / nch as u32; + let table = AllocTable::select(h.version != Version::Mpeg1, h.sample_rate, per_channel); + let sblimit = table.sblimit(); + let bound = if nch == 1 { sblimit } else { h.bound().min(sblimit) }; + let mut steps = [[0u16; 32]; 2]; + for sb in 0..sblimit { + let row = table.row(sb); + let nbal = row.len().trailing_zeros(); + let pick = |r: &mut BitReader| -> Result { Ok(row.get(read(r, nbal)? as usize).copied().unwrap_or(0)) }; + if sb < bound { + for ch in 0..nch { + steps[ch][sb] = pick(r)?; + } + } else { + let s = pick(r)?; + steps[0][sb] = s; + steps[1][sb] = s; + } + } + let mut scfsi = [[0u32; 32]; 2]; + for sb in 0..sblimit { + for ch in 0..nch { + if steps[ch][sb] != 0 { + scfsi[ch][sb] = read(r, 2)?; + } + } + } + // the scalefactor for each third of the frame (4 granules each) + let mut scf = [[[0f32; 3]; 32]; 2]; + for sb in 0..sblimit { + for ch in 0..nch { + if steps[ch][sb] == 0 { + continue; + } + let mut s = || -> Result { Ok(tables::scalefactor(read(r, 6)?)) }; + scf[ch][sb] = match scfsi[ch][sb] { + 0 => [s()?, s()?, s()?], + 1 => { + let (a, b) = (s()?, s()?); + [a, a, b] + } + 2 => { + let a = s()?; + [a, a, a] + } + _ => { + let (a, b) = (s()?, s()?); + [a, b, b] + } + }; + } + } + for gr in 0..12 { + let part = gr / 4; + for sb in 0..sblimit { + if sb < bound { + for ch in 0..nch { + if steps[ch][sb] != 0 { + let q = granule(r, steps[ch][sb])?; + for (k, v) in q.iter().enumerate() { + out[ch][3 * gr + k][sb] = v * scf[ch][sb][part]; + } + } + } + } else if steps[0][sb] != 0 { + let q = granule(r, steps[0][sb])?; + for ch in 0..nch { + for (k, v) in q.iter().enumerate() { + out[ch][3 * gr + k][sb] = v * scf[ch][sb][part]; + } + } + } + } + } + Ok(()) +} + +/// The synthesis subband filter (§2.4.3.2, Figure A.2) for one time slot: 32 subband samples in, +/// 32 PCM samples out. +fn synthesize(v: &mut [f32; 1024], n: &[[f32; 32]; 64], d: &[f32; 512], s: &[f32; 32], out: &mut [f32; 32]) { + v.copy_within(0..960, 64); + for (vi, row) in v.iter_mut().zip(n.iter()) { + *vi = row.iter().zip(s.iter()).map(|(a, b)| a * b).sum(); + } + for (j, o) in out.iter_mut().enumerate() { + let mut acc = 0f32; + for i in 0..8 { + acc += v[i * 128 + j] * d[i * 64 + j] + v[i * 128 + 96 + j] * d[i * 64 + 32 + j]; + } + *o = acc; + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/mpa/src/tables.rs b/crates/mpa/src/tables.rs new file mode 100644 index 00000000..4ca94756 --- /dev/null +++ b/crates/mpa/src/tables.rs @@ -0,0 +1,189 @@ +//! Tables from ISO/IEC 11172-3 Annex B and ISO/IEC 13818-3 Annex B. + +use std::sync::OnceLock; + +/// Bit rates in kbit/s by `bitrate_index` (ISO/IEC 11172-3 §2.4.2.3; ISO/IEC 13818-3 §2.4.2.3 for +/// the lower sampling frequencies). Index 0 is the free format, index 15 is forbidden. +pub(crate) const BITRATES_MPEG1_L1: [u32; 15] = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448]; +pub(crate) const BITRATES_MPEG1_L2: [u32; 15] = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384]; +pub(crate) const BITRATES_MPEG1_L3: [u32; 15] = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]; +pub(crate) const BITRATES_LSF_L1: [u32; 15] = [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256]; +pub(crate) const BITRATES_LSF_L23: [u32; 15] = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; + +/// Sampling frequencies of MPEG-1 by `sampling_frequency` (3 is reserved); MPEG-2 halves them, +/// the unofficial MPEG-2.5 quarters them. +pub(crate) const SAMPLE_RATES: [u32; 3] = [44_100, 48_000, 32_000]; + +/// Coefficients `D[0..=256]` of the synthesis window (Table 3-B.3), in units of 2⁻¹⁶: the table's +/// nine-decimal values are exact multiples of 1/65536. The rest of the window follows from the +/// symmetry of the prototype filter, see [`window`]. +const WINDOW_HALF: [i32; 257] = [ + 0, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -3, -3, -4, -4, -5, // + -5, -6, -7, -7, -8, -9, -10, -11, -13, -14, -16, -17, -19, -21, -24, -26, // + -29, -31, -35, -38, -41, -45, -49, -53, -58, -63, -68, -73, -79, -85, -91, -97, // + -104, -111, -117, -125, -132, -139, -147, -154, -161, -169, -176, -183, -190, -196, -202, -208, // + 213, 218, 222, 225, 227, 228, 228, 227, 224, 221, 215, 208, 200, 189, 177, 163, // + 146, 127, 106, 83, 57, 29, -2, -36, -72, -111, -153, -197, -244, -294, -347, -401, // + -459, -519, -581, -645, -711, -779, -848, -919, -991, -1064, -1137, -1210, -1283, -1356, -1428, -1498, // + -1567, -1634, -1698, -1759, -1817, -1870, -1919, -1962, -2001, -2032, -2057, -2075, -2085, -2087, -2080, -2063, // + 2037, 2000, 1952, 1893, 1822, 1739, 1644, 1535, 1414, 1280, 1131, 970, 794, 605, 402, 185, // + -45, -288, -545, -814, -1095, -1388, -1692, -2006, -2330, -2663, -3004, -3351, -3705, -4063, -4425, -4788, // + -5153, -5517, -5879, -6237, -6589, -6935, -7271, -7597, -7910, -8209, -8491, -8755, -8998, -9219, -9416, -9585, // + -9727, -9838, -9916, -9959, -9966, -9935, -9863, -9750, -9592, -9389, -9139, -8840, -8492, -8092, -7640, -7134, // + 6574, 5959, 5288, 4561, 3776, 2935, 2037, 1082, 70, -998, -2122, -3300, -4533, -5818, -7154, -8540, // + -9975, -11455, -12980, -14548, -16155, -17799, -19478, -21189, -22929, -24694, -26482, -28289, -30112, -31947, -33791, -35640, // + -37489, -39336, -41176, -43006, -44821, -46617, -48390, -50137, -51853, -53534, -55178, -56778, -58333, -59838, -61289, -62684, // + -64019, -65290, -66494, -67629, -68692, -69679, -70590, -71420, -72169, -72835, -73415, -73908, -74313, -74630, -74856, -74992, // + 75038, +]; + +/// `D[i]` for `i` in `0..=256`, in units of 2⁻¹⁶. +pub(crate) fn window_half(i: usize) -> Option { + WINDOW_HALF.get(i).copied() +} + +/// The 512 synthesis window coefficients `D[i]` (Table 3-B.3). The table stores the symmetric +/// prototype `h[i] = h[512 - i]` with every other block of 64 negated, so for `i > 256` +/// `D[i] = -D[512 - i]`, except at multiples of 64 where both halves share the block parity. +pub(crate) fn window() -> &'static [f32; 512] { + static W: OnceLock<[f32; 512]> = OnceLock::new(); + W.get_or_init(|| { + let mut w = [0f32; 512]; + for (i, d) in w.iter_mut().enumerate() { + let (j, sign) = if i <= 256 { (i, 1.0) } else { (512 - i, if i % 64 == 0 { 1.0 } else { -1.0 }) }; + *d = sign * window_half(j).unwrap_or(0) as f32 / 65_536.0; + } + w + }) +} + +/// The matrixing coefficients `N[i][k] = cos((16 + i)(2k + 1)π / 64)` of the synthesis subband +/// filter (§2.4.3.2, Figure A.2), `i` in `0..64`, `k` in `0..32`. +pub(crate) fn matrix() -> &'static [[f32; 32]; 64] { + static N: OnceLock<[[f32; 32]; 64]> = OnceLock::new(); + N.get_or_init(|| { + let mut n = [[0f32; 32]; 64]; + for (i, row) in n.iter_mut().enumerate() { + for (k, c) in row.iter_mut().enumerate() { + *c = (((16 + i) * (2 * k + 1)) as f64 * std::f64::consts::PI / 64.0).cos() as f32; + } + } + n + }) +} + +/// Scalefactors (Table 3-B.1): `2^(1 - index / 3)`. Index 63 is not used by encoders; it continues +/// the progression rather than failing the frame. +pub(crate) fn scalefactor(index: u32) -> f32 { + static S: OnceLock<[f32; 64]> = OnceLock::new(); + let s = S.get_or_init(|| { + let mut s = [0f32; 64]; + for (i, v) in s.iter_mut().enumerate() { + *v = 2f64.powf(1.0 - i as f64 / 3.0) as f32; + } + s + }); + s.get(index as usize).copied().unwrap_or(0.0) +} + +// Layer II possible quantizations per subband (Tables 3-B.2a-d; ISO/IEC 13818-3 Table B.1): the +// number of quantization steps for each value of the subband's allocation field, 0 meaning +// nothing is transmitted. A row has 2^nbal entries. + +/// 3-B.2a/b subbands 0-2. +const A_LOW: [u16; 16] = [0, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535]; +/// 3-B.2a/b subbands 3-10. +const A_MID: [u16; 16] = [0, 3, 5, 7, 9, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 65535]; +/// 3-B.2a/b subbands 11-22. +const A_HIGH: [u16; 8] = [0, 3, 5, 7, 9, 15, 31, 65535]; +/// 3-B.2a/b subbands 23-29. +const A_TOP: [u16; 4] = [0, 3, 5, 65535]; +/// 3-B.2c/d subbands 0-1. +const C_LOW: [u16; 16] = [0, 3, 5, 9, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767]; +/// 3-B.2c/d subbands 2-11; 13818-3 B.1 subbands 4-10. +const C_HIGH: [u16; 8] = [0, 3, 5, 9, 15, 31, 63, 127]; +/// 13818-3 B.1 subbands 0-3. +const LSF_LOW: [u16; 16] = [0, 3, 5, 7, 9, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383]; +/// 13818-3 B.1 subbands 11-29. +const LSF_TOP: [u16; 4] = [0, 3, 5, 9]; + +/// A Layer II bit allocation table. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AllocTable { + /// 3-B.2a: 27 subbands. + A, + /// 3-B.2b: 30 subbands. + B, + /// 3-B.2c: 8 subbands. + C, + /// 3-B.2d: 12 subbands. + D, + /// ISO/IEC 13818-3 Table B.1 (lower sampling frequencies): 30 subbands. + Lsf, +} + +impl AllocTable { + /// The table for a Layer II stream (§2.4.3.3.1 and Table 3-B.2): by sampling frequency and bit + /// rate per channel. ISO/IEC 11172-3 lists only the allowed bit rate / mode combinations; + /// encoders also write the others (ffmpeg's default for mono is 384 kbit/s), so the bit rate + /// ranges are taken as open-ended: at least 56 kbit/s per channel at 48 kHz, or 56-80 kbit/s, + /// is table a; 96 kbit/s and up at 44.1 / 32 kHz is table b; 48 kbit/s and below is table c + /// (d at 32 kHz). The free format (`kbps_per_channel` 0) uses a or b like the high rates. + pub(crate) fn select(lsf: bool, sample_rate: u32, kbps_per_channel: u32) -> Self { + if lsf { + return Self::Lsf; + } + let free = kbps_per_channel == 0; + if (sample_rate == 48_000 && (free || kbps_per_channel >= 56)) || (56..=80).contains(&kbps_per_channel) { + Self::A + } else if free || kbps_per_channel >= 96 { + Self::B + } else if sample_rate == 32_000 { + Self::D + } else { + Self::C + } + } + + /// Subbands with an allocation (`sblimit`). + pub(crate) fn sblimit(self) -> usize { + match self { + Self::A => 27, + Self::B | Self::Lsf => 30, + Self::C => 8, + Self::D => 12, + } + } + + /// The quantization steps per allocation value for subband `sb`; its length is 2^nbal. + pub(crate) fn row(self, sb: usize) -> &'static [u16] { + match self { + Self::A | Self::B => match sb { + 0..=2 => &A_LOW, + 3..=10 => &A_MID, + 11..=22 => &A_HIGH, + _ => &A_TOP, + }, + Self::C | Self::D => match sb { + 0..=1 => &C_LOW, + _ => &C_HIGH, + }, + Self::Lsf => match sb { + 0..=3 => &LSF_LOW, + 4..=10 => &C_HIGH, + _ => &LSF_TOP, + }, + } + } +} + +/// How the samples of a quantization class are coded (Table 3-B.4): `Some(bits)` for the grouped +/// classes (three samples in one codeword), `None` for one `log2(steps + 1)`-bit code per sample. +pub(crate) fn grouping(steps: u16) -> Option { + match steps { + 3 => Some(5), + 5 => Some(7), + 9 => Some(10), + _ => None, + } +} diff --git a/crates/mpa/src/tests.rs b/crates/mpa/src/tests.rs new file mode 100644 index 00000000..1f2e20a6 --- /dev/null +++ b/crates/mpa/src/tests.rs @@ -0,0 +1,151 @@ +use super::*; +use tables::window_half; + +/// A frame header: MPEG-1 unless `lsf`, `layer` 1/2, no CRC, `bitrate_index`, `rate_index` +/// (0: 44.1 kHz, 1: 48 kHz, 2: 32 kHz), `mode` 0-3 and `mode_extension`. +fn header(lsf: bool, layer: u8, bitrate_index: u8, rate_index: u8, mode: u8, ext: u8) -> [u8; 4] { + let version = if lsf { 0b10 } else { 0b11 }; + [0xFF, 0xE0 | version << 3 | (4 - layer) << 1 | 1, bitrate_index << 4 | rate_index << 2, mode << 6 | ext << 4] +} + +/// A frame of `header` with all-zero audio data: no subband has an allocation. +fn silent(h: [u8; 4]) -> Vec { + let mut f = h.to_vec(); + f.resize(parse_header(&h).unwrap().frame_bytes, 0); + f +} + +#[test] +fn headers() { + // Layer II 48 kHz stereo 192 kbit/s: 144 * 192000 / 48000 = 576 bytes + let h = parse_header(&header(false, 2, 10, 1, 0, 0)).unwrap(); + assert_eq!((h.version, h.layer, h.bitrate_kbps, h.sample_rate, h.frame_bytes, h.channels(), h.samples()), (Version::Mpeg1, 2, 192, 48_000, 576, 2, 1152)); + // ffmpeg's default for mono: Layer II 384 kbit/s, which ISO/IEC 11172-3 does not allow + let h = parse_header(&header(false, 2, 14, 1, 3, 0)).unwrap(); + assert_eq!((h.bitrate_kbps, h.frame_bytes, h.mode, h.channels()), (384, 1152, Mode::Mono, 1)); + // Layer I 44.1 kHz 384 kbit/s with padding: (12 * 384000 / 44100 + 1) * 4 = 420 + let mut b = header(false, 1, 12, 0, 1, 2); + b[2] |= 2; + let h = parse_header(&b).unwrap(); + assert_eq!((h.layer, h.frame_bytes, h.samples(), h.mode, h.mode_extension, h.bound()), (1, 420, 384, Mode::JointStereo, 2, 12)); + // MPEG-2 Layer II 24 kHz 160 kbit/s: 144 * 160000 / 24000 = 960; Layer I 22.05 kHz 256 kbit/s + let h = parse_header(&header(true, 2, 14, 1, 0, 0)).unwrap(); + assert_eq!((h.version, h.sample_rate, h.frame_bytes), (Version::Mpeg2, 24_000, 960)); + let h = parse_header(&header(true, 1, 14, 0, 0, 0)).unwrap(); + assert_eq!((h.sample_rate, h.bitrate_kbps, h.frame_bytes), (22_050, 256, 4 * (12 * 256_000 / 22_050))); + // MPEG-2.5 and Layer III headers parse too + assert_eq!(parse_header(&[0xFF, 0xE5, 0x10, 0xC0]).unwrap().sample_rate, 11_025); + let h = parse_header(&[0xFF, 0xFB, 0x90, 0x44]).unwrap(); + assert_eq!((h.layer, h.frame_bytes), (3, 417)); +} + +#[test] +fn bad_headers_are_errors() { + assert_eq!(parse_header(&[]), Err(Error::Invalid("truncated header"))); + assert_eq!(parse_header(&[0xFF, 0xFD]), Err(Error::Invalid("truncated header"))); + assert_eq!(parse_header(&[0x00, 0xFD, 0x90, 0x00]), Err(Error::NoSync)); + assert_eq!(parse_header(&[0xFF, 0x1D, 0x90, 0x00]), Err(Error::NoSync)); + assert_eq!(parse_header(&[0xFF, 0xED, 0x90, 0x00]), Err(Error::Invalid("reserved version"))); + assert_eq!(parse_header(&[0xFF, 0xF9, 0x90, 0x00]), Err(Error::Invalid("reserved layer"))); + assert_eq!(parse_header(&[0xFF, 0xFD, 0xF0, 0x00]), Err(Error::Invalid("forbidden bit rate index"))); + assert_eq!(parse_header(&[0xFF, 0xFD, 0x9C, 0x00]), Err(Error::Invalid("reserved sampling frequency"))); + let mut d = Decoder::new(); + assert_eq!(d.decode(&[0xFF, 0xFB, 0x90, 0x44]).unwrap_err(), Error::Unsupported("Layer III")); + assert_eq!(d.decode(&[0xFF, 0xFD, 0x04, 0x00, 0, 0]).unwrap_err(), Error::Unsupported("free-format bit rate")); + assert_eq!(d.decode(&silent(header(false, 2, 10, 1, 0, 0))[..100]).unwrap_err(), Error::Invalid("truncated frame")); +} + +#[test] +fn silent_frames_decode_to_silence() { + let mut d = Decoder::new(); + for (h, ch, n) in [ + (header(false, 2, 14, 1, 3, 0), 1, 1152), + (header(false, 2, 1, 2, 0, 0), 2, 1152), + (header(true, 2, 8, 0, 1, 3), 2, 1152), + (header(false, 1, 6, 0, 2, 0), 2, 384), + (header(false, 1, 4, 1, 3, 0), 1, 384), + ] { + let f = d.decode(&silent(h)).unwrap(); + assert_eq!(f.channels.len(), ch, "{h:?}"); + assert!(f.channels.iter().all(|c| c.len() == n && c.iter().all(|&s| s == 0.0)), "{h:?}"); + } +} + +/// Table 3-B.3 as typed in must be the smooth prototype filter: with every other block of 64 +/// negated back, its third differences stay within the rounding of the 2⁻¹⁶ units (a mistyped +/// coefficient shows up as a spike of hundreds). +#[test] +fn synthesis_window_is_the_smooth_prototype() { + let h: Vec = (0..=256).map(|i| window_half(i).unwrap() as f64 * if (i / 64) % 2 == 1 { -1.0 } else { 1.0 }).collect(); + let d1: Vec = h.windows(2).map(|w| w[1] - w[0]).collect(); + let d2: Vec = d1.windows(2).map(|w| w[1] - w[0]).collect(); + let worst = d2.windows(2).map(|w| (w[1] - w[0]).abs()).fold(0.0, f64::max); + assert!(worst <= 8.0, "{worst}"); + assert_eq!(window_half(257), None); + let w = tables::window(); + // symmetry of the full window and the table's endpoints + assert_eq!((w[0], w[256], w[511], w[448]), (0.0, 75_038.0 / 65_536.0, 1.0 / 65_536.0, w[64])); + assert_eq!(w[300], -w[212]); +} + +#[test] +fn layer2_allocation_tables() { + use tables::AllocTable as T; + for t in [T::A, T::B, T::C, T::D, T::Lsf] { + for sb in 0..t.sblimit() { + let row = t.row(sb); + assert!(row.len().is_power_of_two() && row[0] == 0, "{t:?} {sb}"); + } + } + // the allowed combinations (Table 3-B.2) and the ones encoders write anyway + assert_eq!(T::select(false, 48_000, 192), T::A); + assert_eq!(T::select(false, 44_100, 64), T::A); + assert_eq!(T::select(false, 44_100, 96), T::B); + assert_eq!(T::select(false, 32_000, 192), T::B); + assert_eq!(T::select(false, 48_000, 32), T::C); + assert_eq!(T::select(false, 44_100, 48), T::C); + assert_eq!(T::select(false, 32_000, 48), T::D); + assert_eq!(T::select(false, 48_000, 384), T::A); + assert_eq!(T::select(false, 44_100, 384), T::B); + assert_eq!(T::select(false, 32_000, 16), T::D); + assert_eq!(T::select(true, 24_000, 160), T::Lsf); +} + +#[test] +fn requantization_is_symmetric() { + assert_eq!([0, 1, 2].map(|v| requantize(v, 3)), [-2.0 / 3.0, 0.0, 2.0 / 3.0]); + assert_eq!(requantize(4, 9), 0.0); + assert_eq!(requantize(0, 65_535), -65_534.0 / 65_535.0); +} + +/// Random audio data behind valid headers, and damaged headers: errors or noise, never a panic. +#[test] +fn random_frames_never_panic() { + let mut x = 0x1234_5678u32; + let mut rnd = move || { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x + }; + let mut d = Decoder::new(); + for k in 0..4000 { + let mut h = header(k % 3 == 0, 1 + (k % 2) as u8, 1 + (rnd() % 14) as u8, (rnd() % 3) as u8, (rnd() % 4) as u8, (rnd() % 4) as u8); + if k % 5 == 0 { + h[1] &= 0xFE; // CRC present + } + if k % 7 == 0 { + h[2] ^= rnd() as u8; + h[3] ^= rnd() as u8; + } + let len = parse_header(&h).map(|p| p.frame_bytes).unwrap_or(64); + let mut f = h.to_vec(); + f.extend((0..len.saturating_sub(4) + (rnd() % 8) as usize).map(|_| rnd() as u8)); + if k % 11 == 0 { + f.truncate(rnd() as usize % f.len()); + } + if let Ok(fr) = d.decode(&f) { + assert!(fr.channels.iter().flatten().all(|s| s.is_finite())); + } + } +} diff --git a/crates/mpa/tests/oracle.rs b/crates/mpa/tests/oracle.rs new file mode 100644 index 00000000..57ad6544 --- /dev/null +++ b/crates/mpa/tests/oracle.rs @@ -0,0 +1,572 @@ +//! Layer I and Layer II against ffmpeg's decoder as the reference. +//! +//! - Encoded streams: ffmpeg's MP2 encoder (mono, stereo, MPEG-1 and MPEG-2 sampling frequencies, +//! every Layer II allocation table, and bit rate / mode combinations ISO/IEC 11172-3 does not +//! allow but ffmpeg writes: its default for mono is 384 kbit/s); twolame, when installed, for +//! joint stereo and dual channel. +//! - Synthetic streams: valid random frames for what no installed encoder writes: Layer I (mono, +//! stereo, dual channel, joint stereo at every bound) and Layer II joint stereo / dual channel +//! with every allocation table. ffmpeg decodes them as it would any stream. + +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use filmcraft_bitstream::BitWriter; +use filmcraft_mpa::{Decoder, Mode, parse_header}; + +/// A stream from an encoder. +struct Encoded { + name: &'static str, + input: &'static str, + /// Encoder arguments after the input (ffmpeg) or flags (twolame). + enc: &'static [&'static str], + rate: u32, + channels: usize, + /// The bit rate the frames must carry (proves which combination is covered). + kbps: u32, + mode: Mode, + twolame: bool, +} + +const TONES: &str = "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t)"; + +const ENCODED: &[Encoded] = &[ + // ffmpeg's default for mono: 384 kbit/s, which ISO/IEC 11172-3 does not allow (table a) + Encoded { + name: "mono_48k_default", + input: "sine=frequency=1000:sample_rate=48000", + enc: &[], + rate: 48_000, + channels: 1, + kbps: 384, + mode: Mode::Mono, + twolame: false, + }, + Encoded { + name: "stereo_48k_192k", + input: TONES, + enc: &["-ar", "48000", "-b:a", "192k"], + rate: 48_000, + channels: 2, + kbps: 192, + mode: Mode::Stereo, + twolame: false, + }, + // table b + Encoded { + name: "stereo_44k_256k", + input: TONES, + enc: &["-ar", "44100", "-b:a", "256k"], + rate: 44_100, + channels: 2, + kbps: 256, + mode: Mode::Stereo, + twolame: false, + }, + // table b, mono above 192 kbit/s (not allowed) + Encoded { + name: "mono_44k_320k", + input: "anoisesrc=color=pink:sample_rate=44100:amplitude=0.3:seed=5", + enc: &["-b:a", "320k"], + rate: 44_100, + channels: 1, + kbps: 320, + mode: Mode::Mono, + twolame: false, + }, + // table c, and stereo below 64 kbit/s (not allowed) + Encoded { + name: "stereo_48k_32k", + input: TONES, + enc: &["-ar", "48000", "-b:a", "32k"], + rate: 48_000, + channels: 2, + kbps: 32, + mode: Mode::Stereo, + twolame: false, + }, + Encoded { + name: "stereo_44k_96k", + input: TONES, + enc: &["-ar", "44100", "-b:a", "96k"], + rate: 44_100, + channels: 2, + kbps: 96, + mode: Mode::Stereo, + twolame: false, + }, + // table d + Encoded { + name: "mono_32k_48k", + input: "sine=frequency=440:sample_rate=32000", + enc: &["-b:a", "48k"], + rate: 32_000, + channels: 1, + kbps: 48, + mode: Mode::Mono, + twolame: false, + }, + // table a at 32 kHz + Encoded { + name: "stereo_32k_128k", + input: TONES, + enc: &["-ar", "32000", "-b:a", "128k"], + rate: 32_000, + channels: 2, + kbps: 128, + mode: Mode::Stereo, + twolame: false, + }, + // MPEG-2 lower sampling frequencies (ISO/IEC 13818-3 table) + Encoded { + name: "lsf_stereo_24k_160k", + input: TONES, + enc: &["-ar", "24000", "-b:a", "160k"], + rate: 24_000, + channels: 2, + kbps: 160, + mode: Mode::Stereo, + twolame: false, + }, + Encoded { + name: "lsf_mono_22k_64k", + input: "sine=frequency=700:sample_rate=22050", + enc: &["-b:a", "64k"], + rate: 22_050, + channels: 1, + kbps: 64, + mode: Mode::Mono, + twolame: false, + }, + Encoded { + name: "lsf_stereo_16k_32k", + input: TONES, + enc: &["-ar", "16000", "-b:a", "32k"], + rate: 16_000, + channels: 2, + kbps: 32, + mode: Mode::Stereo, + twolame: false, + }, + // twolame: joint stereo (intensity coding above the bound) and dual channel + Encoded { + name: "twolame_joint_48k_128k", + input: TONES, + enc: &["-b", "128", "-m", "j"], + rate: 48_000, + channels: 2, + kbps: 128, + mode: Mode::JointStereo, + twolame: true, + }, + Encoded { + name: "twolame_joint_44k_64k", + input: TONES, + enc: &["-b", "64", "-m", "j"], + rate: 44_100, + channels: 2, + kbps: 64, + mode: Mode::JointStereo, + twolame: true, + }, + Encoded { + name: "twolame_dual_44k_192k", + input: TONES, + enc: &["-b", "192", "-m", "d"], + rate: 44_100, + channels: 2, + kbps: 192, + mode: Mode::DualChannel, + twolame: true, + }, +]; + +fn tool(name: &str) -> Option { + filmcraft_testkit::oracle::find_tool(name) +} + +fn make_encoded(ff: &Path, s: &Encoded) -> Option { + let out = filmcraft_testkit::fixtures_dir("mpa").join(format!("{}.mp2", s.name)); + if s.twolame { + let twolame = tool("twolame")?; + return filmcraft_testkit::fixtures::generate(&out, |tmp| { + let wav = tmp.with_extension("wav"); + let made = Command::new(ff) + .args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i", &format!("{}:s={}", s.input, s.rate), "-t", "1", "-c:a", "pcm_s16le"]) + .arg(&wav) + .stdin(Stdio::null()) + .status() + .is_ok_and(|x| x.success()) + && Command::new(twolame).arg("--quiet").args(s.enc).arg(&wav).arg(tmp).stdin(Stdio::null()).status().is_ok_and(|x| x.success()); + let _ = std::fs::remove_file(&wav); + made + }); + } + filmcraft_testkit::fixtures::generate(&out, |tmp| { + Command::new(ff) + .args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i", s.input, "-t", "1", "-c:a", "mp2"]) + .args(s.enc) + .args(["-f", "mp2"]) + .arg(tmp) + .stdin(Stdio::null()) + .status() + .is_ok_and(|x| x.success()) + }) +} + +/// The frames of a raw stream (an ID3 tag or garbage before the first frame is not expected). +fn frames(data: &[u8]) -> Vec<&[u8]> { + let mut out = Vec::new(); + let mut p = 0; + while let Some(h) = data.get(p..).and_then(|b| parse_header(b).ok()) { + let Some(f) = data.get(p..p + h.frame_bytes).filter(|_| h.frame_bytes > 0) else { break }; + out.push(f); + p += h.frame_bytes; + } + out +} + +/// ffmpeg's decode of `file`, interleaved f32. +fn ffmpeg_decode(ff: &Path, file: &Path) -> Vec { + let o = Command::new(ff).args(["-v", "error", "-f", "mp3", "-i"]).arg(file).args(["-f", "f32le", "-"]).output().unwrap(); + assert!(o.status.success(), "{}: {}", file.display(), String::from_utf8_lossy(&o.stderr)); + o.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect() +} + +/// Decode `file` with our decoder and return the worst channel's SNR against ffmpeg (dB). +fn snr_vs_ffmpeg(ff: &Path, file: &Path, rate: u32, channels: usize) -> f64 { + let data = std::fs::read(file).unwrap(); + let fr = frames(&data); + assert!(fr.len() > 10, "{}: {} frames", file.display(), fr.len()); + let want = ffmpeg_decode(ff, file); + let n = want.len() / channels; + // ffmpeg drops the first frame of a stream whose second frame changes the channel mode + // (twolame starts in joint stereo and switches to stereo): start there too + let first = if first_frame_mode_changes(&data) { 1 } else { 0 }; + let mut dec = Decoder::new(); + let mut ours: Vec> = vec![Vec::new(); channels]; + for f in &fr[first..] { + let d = dec.decode(f).unwrap_or_else(|e| panic!("{}: {e}", file.display())); + assert_eq!((d.header.sample_rate, d.channels.len()), (rate, channels), "{}", file.display()); + for (o, c) in ours.iter_mut().zip(d.channels) { + o.extend(c); + } + } + assert_eq!(n, ours[0].len(), "{}: sample count", file.display()); + let mut worst = f64::INFINITY; + for (c, ours) in ours.iter().enumerate() { + let (mut sig, mut err) = (0f64, 0f64); + for (i, &a) in ours.iter().enumerate() { + let b = want[i * channels + c] as f64; + sig += b * b; + err += (a as f64 - b) * (a as f64 - b); + } + worst = worst.min(10.0 * (sig / err.max(1e-30)).log10()); + } + worst +} + +fn first_frame_mode_changes(data: &[u8]) -> bool { + let f = frames(data); + f.len() > 1 && parse_header(f[0]).unwrap().mode != parse_header(f[1]).unwrap().mode +} + +/// The minimum SNR against ffmpeg's float decoder. ffmpeg's own fixed-point decoder agrees with +/// it to ~77 dB (the error is at the 16-bit LSB, ~-96 dBFS); we measure 73-84 dB. A wrong table +/// entry or a misread field costs tens of dB. +const MIN_SNR: f64 = 70.0; + +#[test] +fn encoded_streams_match_ffmpeg() { + let ff = filmcraft_testkit::require_ffmpeg!(); + for s in ENCODED { + let Some(f) = make_encoded(&ff, s) else { + assert!(s.twolame, "could not generate {}", s.name); + eprintln!("{}: twolame not installed, skipped", s.name); + continue; + }; + let data = std::fs::read(&f).unwrap(); + let h = parse_header(&data).unwrap(); + assert_eq!((h.layer, h.bitrate_kbps, h.mode), (2, s.kbps, s.mode), "{}", s.name); + let snr = snr_vs_ffmpeg(&ff, &f, s.rate, s.channels); + println!("{}: {} Hz, {} kbit/s, {:?}: SNR vs ffmpeg {snr:.1} dB", s.name, s.rate, s.kbps, s.mode); + assert!(snr >= MIN_SNR, "{}: SNR {snr:.1} dB < {MIN_SNR}", s.name); + } +} + +struct Rng(u32); +impl Rng { + fn next(&mut self) -> u32 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 17; + self.0 ^= self.0 << 5; + self.0 + } + fn below(&mut self, n: u32) -> u32 { + self.next() % n + } +} + +/// Header bytes (no CRC): `lsf` MPEG-2, `layer` 1/2, the indices, `mode` 0-3, `ext`. +fn header(lsf: bool, layer: u8, bitrate_index: u8, rate_index: u8, mode: u8, ext: u8) -> [u8; 4] { + let version = if lsf { 0b10 } else { 0b11 }; + [0xFF, 0xE0 | version << 3 | (4 - layer) << 1 | 1, bitrate_index << 4 | rate_index << 2, mode << 6 | ext << 4] +} + +/// A valid random Layer I frame: random allocations (as many as the frame holds), scalefactors +/// and samples. +fn random_layer1(rng: &mut Rng, h: [u8; 4]) -> Vec { + let hd = parse_header(&h).unwrap(); + let nch = hd.channels(); + let bound = if hd.mode == Mode::JointStereo { 4 * (hd.mode_extension as usize + 1) } else { 32 }; + let mut alloc = [[0u32; 32]; 2]; + for sb in 0..32 { + for ch in 0..nch { + if (ch == 0 || sb < bound) && rng.below(3) != 0 { + alloc[ch][sb] = 1 + rng.below(14); + } + } + if sb >= bound { + alloc[1][sb] = alloc[0][sb]; + } + } + let bits = |alloc: &[[u32; 32]; 2]| { + let mut n = 32 + 4 * (32 + bound.min(32) * (nch - 1)); + for sb in 0..32 { + for ch in 0..nch { + if alloc[ch][sb] != 0 { + n += 6; + if ch == 0 || sb < bound { + n += 12 * (alloc[ch][sb] as usize + 1); + } + } + } + } + n + }; + // drop allocations from the top until the frame holds them + let mut sb = 31; + while bits(&alloc) > hd.frame_bytes * 8 { + alloc[0][sb] = 0; + alloc[1][sb] = 0; + sb -= 1; + } + let mut w = BitWriter::new(); + w.write_bytes(&h); + for sb in 0..32 { + for ch in 0..if sb < bound { nch } else { 1 } { + w.write_bits(alloc[ch][sb], 4); + } + } + let mut scf = || 10 + rng.below(40); + let mut scfs = vec![]; + for sb in 0..32 { + for ch in 0..nch { + if alloc[ch][sb] != 0 { + scfs.push(scf()); + } + } + } + for s in scfs { + w.write_bits(s, 6); + } + for _ in 0..12 { + for sb in 0..32 { + for ch in 0..if sb < bound { nch } else { 1 } { + let nb = alloc[ch][sb] + 1; + if alloc[ch][sb] != 0 { + // any code but the forbidden all-ones + w.write_bits(rng.below((1 << nb) - 1), nb); + } + } + } + } + let mut f = w.finish(); + f.resize(hd.frame_bytes, 0); + f +} + +/// Allocation field widths (nbal) per subband of the Layer II table the test frames use, by the +/// same selection as the decoder; every table's allocation value 1 is the 3-step class. +fn nbal(lsf: bool, rate: u32, kbps_per_channel: u32) -> Vec { + let rep = |v: &[(u32, usize)]| v.iter().flat_map(|&(n, k)| std::iter::repeat_n(n, k)).collect::>(); + if lsf { + rep(&[(4, 4), (3, 7), (2, 19)]) + } else if (rate == 48_000 && kbps_per_channel >= 56) || (56..=80).contains(&kbps_per_channel) { + rep(&[(4, 11), (3, 12), (2, 4)]) + } else if kbps_per_channel >= 96 { + rep(&[(4, 11), (3, 12), (2, 7)]) + } else if rate == 32_000 { + rep(&[(4, 2), (3, 10)]) + } else { + rep(&[(4, 2), (3, 6)]) + } +} + +/// A valid random Layer II frame using only the 3-step class (allocation value 1, grouped). +fn random_layer2(rng: &mut Rng, h: [u8; 4]) -> Vec { + let hd = parse_header(&h).unwrap(); + let nch = hd.channels(); + let nbal = nbal(h[1] & 0x08 == 0, hd.sample_rate, hd.bitrate_kbps / nch as u32); + let sblimit = nbal.len(); + let bound = if hd.mode == Mode::JointStereo { (4 * (hd.mode_extension as usize + 1)).min(sblimit) } else { sblimit }; + let mut alloc = [[false; 32]; 2]; + let mut scfsi = [[0u32; 32]; 2]; + for sb in 0..sblimit { + for ch in 0..nch { + alloc[ch][sb] = if ch == 1 && sb >= bound { alloc[0][sb] } else { rng.below(3) != 0 }; + scfsi[ch][sb] = rng.below(4); + } + } + let scf_bits = |s: u32| { + if s == 0 { + 18 + } else if s == 2 { + 6 + } else { + 12 + } + }; + let bits = |alloc: &[[bool; 32]; 2]| { + let mut n = 32; + for sb in 0..sblimit { + n += nbal[sb] as usize * if sb < bound { nch } else { 1 }; + for ch in 0..nch { + if alloc[ch][sb] { + n += 2 + scf_bits(scfsi[ch][sb]); + if ch == 0 || sb < bound { + n += 12 * 5; + } + } + } + } + n + }; + let mut sb = sblimit - 1; + while bits(&alloc) > hd.frame_bytes * 8 { + alloc[0][sb] = false; + alloc[1][sb] = false; + sb -= 1; + } + let mut w = BitWriter::new(); + w.write_bytes(&h); + for sb in 0..sblimit { + for ch in 0..if sb < bound { nch } else { 1 } { + w.write_bits(alloc[ch][sb] as u32, nbal[sb]); + } + } + for sb in 0..sblimit { + for ch in 0..nch { + if alloc[ch][sb] { + w.write_bits(scfsi[ch][sb], 2); + } + } + } + for sb in 0..sblimit { + for ch in 0..nch { + if alloc[ch][sb] { + for _ in 0..scf_bits(scfsi[ch][sb]) / 6 { + w.write_bits(10 + rng.below(40), 6); + } + } + } + } + for _ in 0..12 { + for sb in 0..sblimit { + for ch in 0..if sb < bound { nch } else { 1 } { + if alloc[ch][sb] { + w.write_bits(rng.below(27), 5); + } + } + } + } + let mut f = w.finish(); + f.resize(hd.frame_bytes, 0); + f +} + +/// (name, layer, MPEG-2, bitrate index, sampling frequency index, mode, mode extension) +const SYNTHETIC: &[(&str, u8, bool, u8, u8, u8, u8)] = &[ + ("l1_mono_44k_192k", 1, false, 6, 0, 3, 0), + ("l1_stereo_48k_384k", 1, false, 12, 1, 0, 0), + ("l1_dual_32k_256k", 1, false, 8, 2, 2, 0), + ("l1_joint4_48k_192k", 1, false, 6, 1, 1, 0), + ("l1_joint8_44k_320k", 1, false, 10, 0, 1, 1), + ("l1_joint12_32k_128k", 1, false, 4, 2, 1, 2), + ("l1_joint16_48k_448k", 1, false, 14, 1, 1, 3), + ("l1_lsf_stereo_24k_128k", 1, true, 8, 1, 0, 0), + ("l1_lsf_joint_16k_64k", 1, true, 4, 2, 1, 1), + ("l2_joint4_48k_192k", 2, false, 10, 1, 1, 0), + ("l2_joint8_44k_256k", 2, false, 12, 0, 1, 1), + ("l2_joint12_48k_64k", 2, false, 4, 1, 1, 2), + ("l2_joint16_32k_96k", 2, false, 6, 2, 1, 3), + ("l2_dual_44k_160k", 2, false, 9, 0, 2, 0), + ("l2_dual_32k_384k", 2, false, 14, 2, 2, 0), + ("l2_lsf_joint_22k_96k", 2, true, 10, 0, 1, 1), + ("l2_lsf_mono_24k_160k", 2, true, 14, 1, 3, 0), +]; + +#[test] +fn synthetic_streams_match_ffmpeg() { + let ff = filmcraft_testkit::require_ffmpeg!(); + for (k, &(name, layer, lsf, br, sr, mode, ext)) in SYNTHETIC.iter().enumerate() { + let out = filmcraft_testkit::fixtures_dir("mpa").join(format!("{name}.mp{layer}")); + let f = filmcraft_testkit::fixtures::generate(&out, |tmp| { + let mut rng = Rng(0x9E37_79B9 ^ k as u32); + let h = header(lsf, layer, br, sr, mode, ext); + let data: Vec = (0..40).flat_map(|_| if layer == 1 { random_layer1(&mut rng, h) } else { random_layer2(&mut rng, h) }).collect(); + std::fs::write(tmp, data).is_ok() + }) + .unwrap(); + let h = parse_header(&std::fs::read(&f).unwrap()).unwrap(); + let snr = snr_vs_ffmpeg(&ff, &f, h.sample_rate, h.channels()); + println!("{name}: SNR vs ffmpeg {snr:.1} dB"); + assert!(snr >= MIN_SNR, "{name}: SNR {snr:.1} dB < {MIN_SNR}"); + } +} + +/// Damaged frames (bit flips, truncation, scrambled headers) give errors or noise, never a panic. +#[test] +fn damaged_frames_never_panic() { + let ff = filmcraft_testkit::require_ffmpeg!(); + let mut rng = Rng(0x00C0_FFEE); + let mut streams: Vec> = ENCODED.iter().filter(|s| !s.twolame).take(5).map(|s| std::fs::read(make_encoded(&ff, s).unwrap()).unwrap()).collect(); + let mut synth = Rng(7); + streams.push((0..8).flat_map(|_| random_layer1(&mut synth, header(false, 1, 12, 1, 1, 1))).collect()); + for data in &streams { + let mut dec = Decoder::new(); + for fr in frames(data).into_iter().take(8) { + for k in 0..300 { + let mut m = fr.to_vec(); + match k % 3 { + 0 => { + for _ in 0..1 + rng.below(8) { + let bit = rng.below(m.len() as u32 * 8) as usize; + m[bit / 8] ^= 0x80 >> (bit % 8); + } + } + 1 => m.truncate(rng.below(m.len() as u32) as usize), + _ => { + // keep the sync word; scramble version, layer, rates and mode + m[1] ^= rng.next() as u8 & 0x1F; + m[2] ^= rng.next() as u8; + m[3] ^= rng.next() as u8; + } + } + if let Ok(d) = dec.decode(&m) { + assert!(d.channels.iter().flatten().all(|s| s.is_finite())); + } + } + } + } +} + +#[test] +#[ignore] +fn generate_fixtures() { + let Some(ff) = filmcraft_testkit::ffmpeg_or_skip("mpa fixtures") else { return }; + for s in ENCODED { + let out = filmcraft_testkit::fixtures_dir("mpa").join(format!("{}.mp2", s.name)); + filmcraft_testkit::fixtures::generate_and_report(&format!("mpa/{}", s.name), &[out], || make_encoded(&ff, s)); + } +} diff --git a/docs/architecture.md b/docs/architecture.md index ed1486af..97fc7209 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -29,7 +29,7 @@ Design principles: L2 edit · codecs · interchange · captions · speech · tts · tts-text L1 frame · media · project · audio-dsp · text L0 foundation: time · geom · color · bitstream · testkit (dev-dependency only) - codecs/containers: isobmff · matroska · mxf · cfb · mpegts · ogg · h264 · h264enc · hevc · vp9 · av1 · mpeg2v · prores · dnx · apv · aac · ac3 · opus + codecs/containers: isobmff · matroska · mxf · cfb · mpegts · ogg · h264 · h264enc · hevc · vp9 · av1 · mpeg2v · prores · dnx · apv · aac · ac3 · mpa · opus ``` Crates are named `filmcraft-` (`crates/time` is `filmcraft-time`). The apps are `filmcraft` @@ -56,6 +56,7 @@ and `filmcraft-cli`. | `mpeg2v` | L0 | MPEG-2 video (Main / 4:2:2 profile; frame and field pictures, dual prime) and MPEG-1 video decoder ([README](../crates/mpeg2v/README.md)) | | `aac` | L0 | AAC-LC decoder and encoder | | `ac3` | L0 | AC-3 (ATSC A/52) decoder ([README](../crates/ac3/README.md)) | +| `mpa` | L0 | MPEG-1/2 Audio Layer I and II (MP1 / MP2) decoder ([README](../crates/mpa/README.md)) | | `testkit` | L0 | test-only helpers, used only as a dev-dependency: ffmpeg/ffprobe discovery, fixture dirs, golden images ([testing.md](testing.md)) | | `frame` | L1 | `VideoFrame` (planar YUV / RGBA8 / linear RGBA f32, colour metadata), `AudioBuffer` | | `media` | L1 | `MediaSource` trait, probing/openers, frame cache, generators, stills, image sequences, WAV / Broadcast WAV | diff --git a/docs/codec-parity.md b/docs/codec-parity.md index ba0da95b..ecbf6d72 100644 --- a/docs/codec-parity.md +++ b/docs/codec-parity.md @@ -42,7 +42,7 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed | AAC (LC, HE, LATM) | decode + encode | own decoder (LC, HE core), LATM | own LC encoder | loudness oracle vs ffmpeg | 90% | 3–5 h | | AC-3 | decode (licensed) | own decoder (ATSC A/52) | none | conformance | 80% | — | | E-AC-3 (Dolby Digital Plus) | decode | own decoder (ATSC A/52 Annex E, #647): independent substream, 1–6 blocks, spectral extension; **no** adaptive hybrid transform, enhanced coupling or 7.1 dependent substreams (plays the 5.1 core) | none | SNR vs ffmpeg 65–91 dB (raw, MP4, MOV, Matroska, TS) | 75% | 10–20 h | -| MP3 / MP2 | decode + MP3 encode | decode (symphonia, MPL-2.0; own MP2) | **none** | — | 60% | 10–20 h (MP3 encoder) | +| MP3 / MP2 | decode + MP3 encode | MP3: symphonia (MPL-2.0); MP1 / MP2: own decoder (ISO/IEC 11172-3 / 13818-3, #809), also the bit rate / mode combinations ffmpeg writes by default (mono 384 kbit/s) | **none** | MP1 / MP2: SNR vs ffmpeg 73–84 dB | 65% | 10–20 h (MP3 encoder) | | Opus | — (not a Premiere import format) | own decoder, RFC 8251 range-exact | none | conformance vectors | ahead | — | | FLAC, ALAC, Vorbis | partial | decode (symphonia); FLAC in MP4 silent (#603) | none | — | 70% | 3–6 h | | WMA | decode on Windows | none | none | — | 0% | 10–20 h | diff --git a/docs/gaps.md b/docs/gaps.md index ab25b8af..fca996d6 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -156,9 +156,12 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement** ## G10. Container and codec gaps on import - **Have:** E-AC-3 (Dolby Digital Plus) in MP4, MOV, Matroska and MPEG-TS (#647): independent - substream, 1–6 blocks per syncframe, spectral extension. -- **Missing:** E-AC-3 adaptive hybrid transform, enhanced coupling and the extra channels of 7.1 - dependent substreams (played as the 5.1 core), AVI (#598; listed as an extension but no + substream, 1–6 blocks per syncframe, spectral extension. MP1 / MP2 (MPEG audio Layer I / II): + own decoder (#809) in raw files, MPEG-TS / PS and Matroska, including the bit rate / mode + combinations ISO/IEC 11172-3 disallows but ffmpeg writes (its mono default is 384 kbit/s). +- **Missing:** free-format MPEG audio, MP1 in Matroska (`A_MPEG/L1`), E-AC-3 adaptive hybrid + transform, enhanced coupling and the extra channels of 7.1 dependent substreams (played as the + 5.1 core), AVI (#598; listed as an extension but no demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345, #383), JPEG 2000 / JPEG XS MXF, Cinema DNG; stills: PSD, EXR, DPX, Targa, HEIF / HEIC, Radiance HDR, AI / EPS; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1ea276af..ab3955ac 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -39,6 +39,7 @@ const LAYERS: &[(&str, u8)] = &[ ("mpegts", 0), ("mpeg2v", 0), ("ac3", 0), + ("mpa", 0), ("ogg", 0), ("riff", 0), ("h264", 0), @@ -406,6 +407,7 @@ const FIXTURE_GENERATORS: &[(&str, &str)] = &[ ("codecs", "mpeg_oracle"), ("mpeg2v", "oracle"), ("ac3", "oracle"), + ("mpa", "oracle"), ("codecs", "ogg_oracle"), ]; From 482783e538304fd9a482c6ff6bacaf34cffa71c1 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 03:00:46 +0200 Subject: [PATCH 2/3] #809: Cargo.lock picks up the 0.6.0 workspace version for filmcraft-mpa Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b2e6fcca..e1632562 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2078,7 +2078,7 @@ dependencies = [ [[package]] name = "filmcraft-mpa" -version = "0.5.0" +version = "0.6.0" dependencies = [ "filmcraft-bitstream", "filmcraft-testkit", From a147e2551e085e55f99cac850f67d70def4ac26d Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 08:56:31 +0200 Subject: [PATCH 3/3] #809: MP2 Matroska oracle robust to older ffmpeg; bounded ID3 skip and subbands ffmpeg's CLI handles Matroska priming differently by version: 9.x trims the 481-sample CodecDelay, 2023-24 static builds keep the priming samples, so comparing from sample 2 put the priming ramp against our tone (max error = peak 0.125). The .mkv case now searches lags within two samples of 0 (trimmed) and of the stream's own CodecDelay (kept), over the interior of both decodes; the plain .mp2 file stays at lag 0. Review nits: the ID3v2 skip offset uses saturating_add (test with the largest tag size); every Layer II table's sblimit is checked <= 32 at compile time and clamped to SUBBANDS before indexing the per-subband arrays (row() is a match, so it cannot index out of range). Verified with ffmpeg 9.0.2 and the static N-71064 (2024-06) build. Co-Authored-By: Claude Opus 5.5 --- crates/codecs/src/audio.rs | 4 ++- crates/codecs/tests/mpeg_oracle.rs | 39 +++++++++++++++++++++--------- crates/mpa/src/lib.rs | 12 +++++---- crates/mpa/src/tables.rs | 20 ++++++++++++--- 4 files changed, 55 insertions(+), 20 deletions(-) diff --git a/crates/codecs/src/audio.rs b/crates/codecs/src/audio.rs index dab8a49f..dda0a4d1 100644 --- a/crates/codecs/src/audio.rs +++ b/crates/codecs/src/audio.rs @@ -875,7 +875,7 @@ fn decode_mpeg_layer12(bytes: &[u8]) -> Option<(u8, u32, Vec>)> { // ID3v2: 10-byte header, a syncsafe size, and a 10-byte footer when flagged [b'I', b'D', b'3', _, _, flags, s0, s1, s2, s3, ..] => { let size = [*s0, *s1, *s2, *s3].iter().fold(0usize, |a, &b| (a << 7) | (b & 0x7F) as usize); - 10 + size + if flags & 0x10 != 0 { 10 } else { 0 } + size.saturating_add(if flags & 0x10 != 0 { 20 } else { 10 }) } _ => 0, }; @@ -974,6 +974,8 @@ mod mpeg_layer12_tests { assert!(decode_mpeg_layer12(&file[..n]).is_none(), "{n}"); } assert!(decode_mpeg_layer12(&[&mono[..], &[0x12; 50]].concat()).is_none()); + // an ID3v2 tag claiming the largest size (with a footer) points past the end + assert!(decode_mpeg_layer12(&[&b"ID3\x04\0\x10\x7F\x7F\x7F\x7F"[..], &mono].concat()).is_none()); } } diff --git a/crates/codecs/tests/mpeg_oracle.rs b/crates/codecs/tests/mpeg_oracle.rs index 90cc6bd4..e6d5a39d 100644 --- a/crates/codecs/tests/mpeg_oracle.rs +++ b/crates/codecs/tests/mpeg_oracle.rs @@ -536,24 +536,41 @@ fn mono_mp2_at_ffmpegs_default_bit_rate() { assert_eq!(std::fs::read(&f).unwrap()[2] >> 4, 14, "384 kbit/s"); let worst = check_audio(&ff, &make(&ff, "ts_mpeg2_mono_mp2.ts"), 2e-4); println!("ts_mpeg2_mono_mp2.ts: max error {worst}"); - // Matroska: ffmpeg trims the 481-sample encoder delay; we trim `CodecDelay` rounded to the - // millisecond (#790), so allow a lag of a sample or two there - for (name, max_lag) in [("audio_mono.mp2", 0i64), ("mkv_mono_mp2.mkv", 2)] { + // Matroska stores the encoder's 481-sample priming as `CodecDelay`. Whether ffmpeg's CLI trims + // it depends on its version (9.x does; 2023-24 builds keep the priming samples), and until + // #790 we trim it rounded to the millisecond. So for .mkv ffmpeg's decode is searched at + // lags within a sample or two of 0 (trimmed) and of `CodecDelay` (kept). The plain .mp2 file + // has no such ambiguity and must line up exactly. + for name in ["audio_mono.mp2", "mkv_mono_mp2.mkv"] { let f = make(&ff, name); - let src = filmcraft_codecs::open_bytes(name, bytes(&f)).unwrap(); + let file = bytes(&f); + let src = filmcraft_codecs::open_bytes(name, file.clone()).unwrap(); let a = src.info().audio().cloned().unwrap(); assert_eq!((a.sample_rate, a.channels), (48_000, 1), "{name}"); let want = ffmpeg_audio_f32(&ff, &f, &[]); - let n = want.len() - 4; - let worst = (-max_lag..=max_lag) + let (delay, lags): (usize, Vec) = match name.ends_with(".mkv") { + true => { + let mkv = filmcraft_matroska::open(file).unwrap(); + let ns = mkv.tracks.first().map_or(0, |t| t.codec_delay_ns); + let delay = ((ns * 48_000 + 500_000_000) / 1_000_000_000) as usize; + assert!(delay > 0, "{name}: no CodecDelay"); + (delay, (-2..=2).chain(delay as i64 - 2..=delay as i64 + 2).collect()) + } + false => (0, vec![0]), + }; + // ours from sample 2 on, against ffmpeg's from sample 2 + lag (inside both decodes) + let n = want.len() - delay - 6; + let buf = src.audio(0, n, 48_000).unwrap(); + let (worst, lag) = lags + .into_iter() .map(|lag| { - let buf = src.audio(2 + lag, n, 48_000).unwrap(); - want[2..].iter().zip(&buf.channels[0]).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max) + let err = (2..n - 2).map(|i| (want[(i as i64 + lag) as usize] - buf.channels[0][i]).abs()).fold(0f32, f32::max); + (err, lag) }) - .fold(f32::INFINITY, f32::min); + .fold((f32::INFINITY, 0), |m, x| if x.0 < m.0 { x } else { m }); let peak = want.iter().fold(0f32, |m, x| m.max(x.abs())); - println!("{name}: max error {worst}, peak {peak}"); - assert!(peak > 0.1 && worst <= 2e-4, "{name}: peak {peak}, max error {worst}"); + println!("{name}: max error {worst} at lag {lag} (CodecDelay {delay}), peak {peak}"); + assert!(peak > 0.1 && worst <= 2e-4, "{name}: peak {peak}, max error {worst} at lag {lag}"); } } diff --git a/crates/mpa/src/lib.rs b/crates/mpa/src/lib.rs index 4dd32814..9834e529 100644 --- a/crates/mpa/src/lib.rs +++ b/crates/mpa/src/lib.rs @@ -163,7 +163,7 @@ pub struct Frame { } /// Subband samples of one frame: `[channel][time slot][subband]`, up to 36 time slots. -type Subbands = [[[f32; 32]; 36]; 2]; +type Subbands = [[[f32; tables::SUBBANDS]; 36]; 2]; /// A Layer I / Layer II decoder. It keeps the synthesis filter state between frames, so frames /// must be fed in order; call [`Decoder::reset`] before decoding from another position. @@ -314,9 +314,11 @@ fn layer2(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> { let nch = h.channels(); let per_channel = h.bitrate_kbps / nch as u32; let table = AllocTable::select(h.version != Version::Mpeg1, h.sample_rate, per_channel); - let sblimit = table.sblimit(); + // at most 32 for every table (a compile-time check in `tables`); the clamp keeps every + // `[sb]` below in bounds even so + let sblimit = table.sblimit().min(tables::SUBBANDS); let bound = if nch == 1 { sblimit } else { h.bound().min(sblimit) }; - let mut steps = [[0u16; 32]; 2]; + let mut steps = [[0u16; tables::SUBBANDS]; 2]; for sb in 0..sblimit { let row = table.row(sb); let nbal = row.len().trailing_zeros(); @@ -331,7 +333,7 @@ fn layer2(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> { steps[1][sb] = s; } } - let mut scfsi = [[0u32; 32]; 2]; + let mut scfsi = [[0u32; tables::SUBBANDS]; 2]; for sb in 0..sblimit { for ch in 0..nch { if steps[ch][sb] != 0 { @@ -340,7 +342,7 @@ fn layer2(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> { } } // the scalefactor for each third of the frame (4 granules each) - let mut scf = [[[0f32; 3]; 32]; 2]; + let mut scf = [[[0f32; 3]; tables::SUBBANDS]; 2]; for sb in 0..sblimit { for ch in 0..nch { if steps[ch][sb] == 0 { diff --git a/crates/mpa/src/tables.rs b/crates/mpa/src/tables.rs index 4ca94756..11d56c5a 100644 --- a/crates/mpa/src/tables.rs +++ b/crates/mpa/src/tables.rs @@ -107,6 +107,19 @@ const LSF_LOW: [u16; 16] = [0, 3, 5, 7, 9, 15, 31, 63, 127, 255, 511, 1023, 2047 /// 13818-3 B.1 subbands 11-29. const LSF_TOP: [u16; 4] = [0, 3, 5, 9]; +/// Subbands of the polyphase filter bank: the size of the decoder's per-subband arrays. +pub(crate) const SUBBANDS: usize = 32; + +// Every table's `sblimit` fits the per-subband arrays, so `sb < sblimit` indexes them in bounds. +const _: () = { + let tables = [AllocTable::A, AllocTable::B, AllocTable::C, AllocTable::D, AllocTable::Lsf]; + let mut i = 0; + while i < tables.len() { + assert!(tables[i].sblimit() <= SUBBANDS); + i += 1; + } +}; + /// A Layer II bit allocation table. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum AllocTable { @@ -145,8 +158,8 @@ impl AllocTable { } } - /// Subbands with an allocation (`sblimit`). - pub(crate) fn sblimit(self) -> usize { + /// Subbands with an allocation (`sblimit`): at most [`SUBBANDS`] (checked at compile time). + pub(crate) const fn sblimit(self) -> usize { match self { Self::A => 27, Self::B | Self::Lsf => 30, @@ -155,7 +168,8 @@ impl AllocTable { } } - /// The quantization steps per allocation value for subband `sb`; its length is 2^nbal. + /// The quantization steps per allocation value for subband `sb`; its length is 2^nbal. A match, + /// not an index: any `sb` gets a row (subbands past `sblimit` get the table's last one). pub(crate) fn row(self, sb: usize) -> &'static [u16] { match self { Self::A | Self::B => match sb {