From ca53e90b1866372f35c1f4e88b400e063b4e8d63 Mon Sep 17 00:00:00 2001 From: Aleksander Helgaker Date: Wed, 11 Nov 2026 14:02:37 +0100 Subject: [PATCH] craft-audio: MP1/MP2 decoder (#448) FilmCraft's MPEG audio Layer I/II decoder as `craft_audio::mpa` behind `mpa-decode`, kept over symphonia's: the same quality where both decode, and it also decodes the MP2 files ffmpeg writes by default (mono at 384 kbit/s), which symphonia rejects. `TrackDecoder` and the native read path take MP1/MP2 in MP4, Matroska and raw streams; symphonia keeps MP3 only, so its `mp1` and `mp2` features are dropped. Co-authored-by: Claude Opus 5.5 Co-authored-by: Dylan M. Taylor --- .github/workflows/audio.yml | 2 +- README.md | 4 +- crates/audio/Cargo.toml | 13 +- crates/audio/README.md | 25 +- crates/audio/src/layout.rs | 2 +- crates/audio/src/lib.rs | 30 +- crates/audio/src/mpa/mod.rs | 432 +++++++++++++++ crates/audio/src/mpa/tables.rs | 203 +++++++ crates/audio/src/mpa/tests.rs | 154 +++++ crates/audio/src/read/mod.rs | 12 +- crates/audio/src/read/native.rs | 36 +- crates/audio/src/track.rs | 90 ++- crates/audio/tests/codecs/main.rs | 1 + crates/audio/tests/codecs/mpa_oracle.rs | 586 ++++++++++++++++++++ crates/audio/tests/codecs/routing_oracle.rs | 58 +- crates/audio/tests/codecs/testkit.rs | 9 + 16 files changed, 1598 insertions(+), 59 deletions(-) create mode 100644 crates/audio/src/mpa/mod.rs create mode 100644 crates/audio/src/mpa/tables.rs create mode 100644 crates/audio/src/mpa/tests.rs create mode 100644 crates/audio/tests/codecs/mpa_oracle.rs diff --git a/.github/workflows/audio.yml b/.github/workflows/audio.yml index d406b37e..c6c3215e 100644 --- a/.github/workflows/audio.yml +++ b/.github/workflows/audio.yml @@ -39,7 +39,7 @@ jobs: # Each feature on its own, so code that needs another feature fails here. - name: Clippy, each feature alone run: | - for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode ac3-decode serde; do + for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode ac3-decode mpa-decode serde; do echo "::group::features: ${f:-none}" cargo clippy -p craft-audio --all-targets --locked --no-default-features ${f:+--features "$f"} -- -D warnings echo "::endgroup::" diff --git a/README.md b/README.md index a2fe3915..3f27d580 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ craft- = { git = "https://github.com/storytold/craft-libs", rev = "`. -- `decode` tries native decoders first. AAC, Opus and AC-3 no longer go through symphonia, except AAC in +- `decode` tries native decoders first. AAC, Opus, AC-3 and MP2 no longer go through symphonia, except AAC in CAF. MP4 and Matroska files decode to presentation length. A packet that fails is concealed, not dropped. A failed source read is `AudioError::Io`. diff --git a/crates/audio/src/layout.rs b/crates/audio/src/layout.rs index 19fdc95e..a1da0b6c 100644 --- a/crates/audio/src/layout.rs +++ b/crates/audio/src/layout.rs @@ -145,7 +145,7 @@ impl TryFrom for ChannelLayout { } /// A channel count as the API holds it, saturating at `u16::MAX`. -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "pcm-encode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode", feature = "pcm-encode"))] pub(crate) fn count(n: usize) -> u16 { u16::try_from(n).unwrap_or(u16::MAX) } diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index a699b988..36940b02 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -8,11 +8,11 @@ //! `wasm32-unknown-unknown`. //! //! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`] -//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus, AC-3/E-AC-3 and PCM in -//! MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and -//! [`TrackDecoder`]; everything else (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, -//! µ-law) through `symphonia`, which is to be replaced by native decoders. Feature -//! `decode-native` is the same without symphonia. +//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM +//! in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and +//! [`TrackDecoder`]; everything else (MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) +//! through `symphonia`, which is to be replaced by native decoders. Feature `decode-native` is +//! the same without symphonia. //! - [`TrackDecoder`] (any decoder feature): one track's packets from any demuxer to planar //! `f32` in WAV channel order. //! - [`encode`], [`FileEncoder`]: WAV/BWF/RF64 and AIFF/AIFF-C (feature `pcm-encode`, which also @@ -21,7 +21,7 @@ //! `lossless-encode` and `aac-encode`. //! - [`peaks`] (feature `peaks`): min/max/RMS pyramid for waveform drawing. //! - Codec modules for packet-level use, each with its own features: [`aac`] (`aac-decode`, -//! `aac-encode`), [`opus`] (`opus-decode`), [`ac3`] (`ac3-decode`). +//! `aac-encode`), [`opus`] (`opus-decode`), [`ac3`] (`ac3-decode`), [`mpa`] (`mpa-decode`). //! //! The shared types below build with no features. @@ -31,15 +31,17 @@ pub mod aac; pub mod ac3; mod buffer; mod layout; +#[cfg(feature = "mpa-decode")] +pub mod mpa; #[cfg(feature = "opus-decode")] pub mod opus; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] mod pcm; #[cfg(feature = "peaks")] pub mod peaks; #[cfg(feature = "decode-native")] mod read; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] mod track; #[cfg(any(feature = "decode-native", feature = "pcm-encode"))] mod wav; @@ -56,7 +58,7 @@ pub use layout::{ChannelLayout, Speaker}; pub use read::SharedSource; #[cfg(feature = "decode-native")] pub use read::{DEFAULT_MAX_SAMPLES, DecodeOptions, Decoder, TrackInfo, decode, decode_with, detect_container, probe, probe_with, tracks, tracks_of}; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] pub use track::{LOOKAHEAD_PACKETS, TrackDecoder}; #[cfg(feature = "pcm-encode")] pub use write::wav_float_header; @@ -123,6 +125,16 @@ impl From for AudioError { } } +#[cfg(feature = "mpa-decode")] +impl From for AudioError { + fn from(e: mpa::Error) -> Self { + match e { + mpa::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()), + _ => AudioError::Malformed(e.to_string()), + } + } +} + /// The file formats [`encode`] writes. Each needs its feature: WAV and AIFF `pcm-encode`, FLAC /// `flac-encode`; asking for one that is not built returns [`AudioError::Unsupported`]. #[non_exhaustive] diff --git a/crates/audio/src/mpa/mod.rs b/crates/audio/src/mpa/mod.rs new file mode 100644 index 00000000..a8cd8e9b --- /dev/null +++ b/crates/audio/src/mpa/mod.rs @@ -0,0 +1,432 @@ +//! 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). +//! +//! From FilmCraft's `filmcraft-mpa` (storytold/filmcraft@8fda2f2). Public items keep their names, so +//! FilmCraft can re-export this module. + +// Index loops follow the specification's pseudo-code (FilmCraft allows this lint workspace-wide). +#![allow(clippy::needless_range_loop)] + +mod tables; + +use craft_media::bitstream::BitReader; +use tables::AllocTable; + +/// Errors from header parsing and decoding. +#[non_exhaustive] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + /// The data does not start with a frame sync word. + #[error("not an MPEG audio frame")] + NoSync, + /// A valid stream uses a feature this decoder does not implement (Layer III, free format). + #[error("unsupported MPEG audio: {0}")] + Unsupported(&'static str), + /// Reserved or forbidden header values, or a truncated or inconsistent frame. + #[error("invalid MPEG audio frame: {0}")] + Invalid(&'static str), +} + +/// Result with this module's [`Error`]. +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 { + /// Two channels coded independently. + Stereo, + /// Intensity stereo above the bound given by `mode_extension` (Layers I and II). + JointStereo, + /// Two independent mono channels. + DualChannel, + /// One channel. + Mono, +} + +/// The 32-bit frame header (§2.4.1.3, §2.4.2.3). +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Header { + /// MPEG version. + 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, + /// Sampling rate in Hz. + pub sample_rate: u32, + /// `padding_bit`: the frame has one extra slot. + pub padding: bool, + /// Channel mode. + pub mode: Mode, + /// `mode_extension` (0-3); in joint stereo it selects the intensity stereo bound. + 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. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub struct Frame { + /// The frame's header. + 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; 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. +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 { + /// A decoder with empty synthesis filter state. + 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); + // 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; tables::SUBBANDS]; 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; tables::SUBBANDS]; 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]; tables::SUBBANDS]; 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/audio/src/mpa/tables.rs b/crates/audio/src/mpa/tables.rs new file mode 100644 index 00000000..11d56c5a --- /dev/null +++ b/crates/audio/src/mpa/tables.rs @@ -0,0 +1,203 @@ +//! 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]; + +/// 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 { + /// 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`): at most [`SUBBANDS`] (checked at compile time). + pub(crate) const 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. 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 { + 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/audio/src/mpa/tests.rs b/crates/audio/src/mpa/tests.rs new file mode 100644 index 00000000..490d7259 --- /dev/null +++ b/crates/audio/src/mpa/tests.rs @@ -0,0 +1,154 @@ +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/audio/src/read/mod.rs b/crates/audio/src/read/mod.rs index 88d3fcea..56aa942b 100644 --- a/crates/audio/src/read/mod.rs +++ b/crates/audio/src/read/mod.rs @@ -183,13 +183,13 @@ enum Inner<'a> { /// A file's audio, decoded block by block: the streaming counterpart of [`decode`], with seeking /// and track choice. /// -/// Native formats (WAV, AIFF, AAC, Opus, AC-3/E-AC-3 and PCM in MP4/MOV, Matroska/WebM and Ogg, and -/// raw ADTS and AC-3 streams) read their sample data from the source packet by packet. The -/// container's packet index is held in memory (tens of bytes per packet), and opening an Ogg or -/// Matroska file or a raw +/// Native formats (WAV, AIFF, AAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM in MP4/MOV, Matroska/WebM and +/// Ogg, and raw ADTS, AC-3 and MPEG audio streams) read their sample data from the source packet by +/// packet. The container's packet index is held in memory (tens of bytes per packet), and opening +/// an Ogg or Matroska file or a raw /// stream reads through the whole file to build it; WAV and AIFF need no index. Formats symphonia -/// decodes (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) read the whole source into -/// memory first; their decoded audio still streams. +/// decodes (MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) read the whole source into memory +/// first; their decoded audio still streams. /// /// ```no_run /// # fn demo() -> craft_audio::Result<()> { diff --git a/crates/audio/src/read/native.rs b/crates/audio/src/read/native.rs index 60f7c690..ed7ef2d7 100644 --- a/crates/audio/src/read/native.rs +++ b/crates/audio/src/read/native.rs @@ -1,7 +1,7 @@ //! The native decode path: craft-media's demuxers (ISO BMFF / QuickTime, Matroska / WebM), raw -//! ADTS and AC-3 streams, and WAV / AIFF PCM, feeding [`TrackDecoder`]. -//! [`Native::open`] answers `None` for a stream with no native codec (MP1/MP2/MP3, FLAC, ALAC, -//! Vorbis, ...) or a container these demuxers do not read; the caller then uses symphonia. +//! ADTS, AC-3 and MPEG audio streams, and WAV / AIFF PCM, feeding [`TrackDecoder`]. +//! [`Native::open`] answers `None` for a stream with no native codec (MP3, FLAC, ALAC, Vorbis, +//! ...) or a container these demuxers do not read; the caller then uses symphonia. //! //! Every container is a [`Packets`] source over a [`Src`], read packet by packet, so a decode //! streams. Timing follows the containers: MP4 edit lists (AAC priming, Opus pre-skip), Matroska @@ -13,7 +13,7 @@ use super::id3v2_len; use super::source::{Src, io_error}; use super::wav::{self, WavParse}; use crate::track::{TrackDecoder, adts_format_of, adts_frame_count, adts_stream_header}; -use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac, ac3}; +use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac, ac3, mpa}; use craft_media::{isobmff, matroska, ogg}; /// Corrupt packets tolerated before a stream that has produced nothing is an error. @@ -369,7 +369,8 @@ impl<'a> Native<'a> { Some(c @ (Container::Matroska | Container::WebM)) if body.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) => open_mkv(src, c, track), Some(Container::Adts) => single(open_adts(src, at)), Some(Container::Ac3) => single(open_ac3(src, at)), - None => single(open_ac3(src, at).or_else(|| open_adts(src, at))), + Some(Container::MpegAudio) => single(open_mpa(src, at)), + None => single(open_ac3(src, at).or_else(|| open_adts(src, at)).or_else(|| open_mpa(src, at))), _ => None, } } @@ -699,6 +700,31 @@ fn open_adts<'a>(src: &Src<'a>, at: u64) -> Option>> { Some(Ok(n)) } +/// A raw MPEG audio layer I or II stream. +fn open_mpa<'a>(src: &Src<'a>, at: u64) -> Option>> { + let frames = match raw_frames(src, at, |rest| { + mpa::parse_header(rest).ok().filter(|h| h.layer == 1 || h.layer == 2).map(|h| (h.frame_bytes, h.samples() as u32)) + }) { + Ok(frames) => frames?, + Err(e) => return Some(Err(e)), + }; + let total = frames.iter().map(|&(_, _, f)| u64::from(f)).sum(); + let lookahead = match raw_lookahead(src, &frames, 1) { + Ok(lookahead) => lookahead, + Err(e) => return Some(Err(e)), + }; + native( + Container::MpegAudio, + Packets::Raw { frames, src: src.clone(), next: 0 }, + &CodecParams::new(CodecId::Mp2), + &lookahead, + 0, + None, + Some(total), + ) + .map(Ok) +} + /// A raw AC-3 or E-AC-3 stream; E-AC-3 dependent substreams decode to nothing and count no frames. fn open_ac3<'a>(src: &Src<'a>, at: u64) -> Option>> { let frames = match raw_frames(src, at, |rest| { diff --git a/crates/audio/src/track.rs b/crates/audio/src/track.rs index a9eac30a..434b53a9 100644 --- a/crates/audio/src/track.rs +++ b/crates/audio/src/track.rs @@ -6,6 +6,8 @@ use crate::aac; #[cfg(feature = "ac3-decode")] use crate::ac3; +#[cfg(feature = "mpa-decode")] +use crate::mpa; #[cfg(feature = "opus-decode")] use crate::opus; use crate::pcm::{PcmKind, deinterleave}; @@ -32,6 +34,8 @@ enum Inner { dec: Box, order: Option<&'static [usize]>, }, + #[cfg(feature = "mpa-decode")] + Mpa(Box), Pcm { kind: PcmKind, channels: usize, @@ -43,9 +47,9 @@ enum Inner { /// /// It takes the track's [`CodecParams`] (from `isobmff::Track::codec_params`, /// `matroska::Track::codec_params`, `ogg::Stream::codec_params`, or built by hand for another -/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus, AC-3/E-AC-3 and PCM. -/// Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: every decoded -/// sample is returned. +/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus, AC-3/E-AC-3, MP1/MP2 +/// and PCM. Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: every +/// decoded sample is returned. /// /// ```no_run /// # fn demo(params: &craft_audio::CodecParams, packets: &[Vec]) -> craft_audio::Result<()> { @@ -87,9 +91,9 @@ impl std::fmt::Debug for TrackDecoder { impl TrackDecoder { /// A decoder for the track `params` describes. `lookahead` holds the track's first packets /// (up to [`LOOKAHEAD_PACKETS`]; empty when they are not at hand): HE-AAC signalled only - /// inside the stream is found in them, and AC-3 takes its rate and channel layout from the - /// first frame. Without them those are taken from `params` and set - /// from the first decoded packet. + /// inside the stream is found in them, and AC-3 and MPEG audio take their rate and channel + /// layout (MPEG audio its layer too) from the first frame. Without them those are taken from + /// `params` and set from the first decoded packet. /// /// Streams without codec configuration are accepted where the packets carry it: AAC with /// empty `extradata` takes its configuration from the ADTS headers of the lookahead (MPEG-TS, @@ -103,8 +107,9 @@ impl TrackDecoder { pub fn new(params: &CodecParams, lookahead: &[&[u8]]) -> Result { let rate = params.sample_rate.unwrap_or(0); let channels = usize::from(params.channels.unwrap_or(0)); - // AC-3 reads its parameters from the first frame; AAC reads the lookahead; Opus needs none. - #[cfg_attr(not(feature = "ac3-decode"), allow(unused_variables))] + // AC-3 and MPEG audio read their parameters from the first frame; AAC reads the lookahead; + // Opus needs none. + #[cfg_attr(not(any(feature = "ac3-decode", feature = "mpa-decode")), allow(unused_variables))] let first = lookahead.first().copied().unwrap_or_default(); let (inner, codec, sample_rate, layout, sample_format) = match params.codec { #[cfg(feature = "aac-decode")] @@ -169,6 +174,23 @@ impl TrackDecoder { let dec = opus::Decoder::from_head(head, 48_000)?; (Inner::Opus { dec: Box::new(dec), order }, CodecId::Opus, 48_000, layout, SampleFormat::Compressed) } + #[cfg(feature = "mpa-decode")] + CodecId::Mp1 | CodecId::Mp2 | CodecId::Mp3 => match mpa::parse_header(first) { + Ok(h) if h.layer == 3 => return Err(unsupported(CodecId::Mp3)), + Ok(h) => { + let codec = if h.layer == 1 { CodecId::Mp1 } else { CodecId::Mp2 }; + (Inner::Mpa(Box::default()), codec, h.sample_rate, mpa_layout(&h), SampleFormat::Compressed) + } + // Without a first frame, a container that names layer III is left to symphonia. + Err(_) if params.codec == CodecId::Mp3 => return Err(unsupported(CodecId::Mp3)), + Err(_) => ( + Inner::Mpa(Box::default()), + params.codec, + rate, + ChannelLayout::default_for(crate::layout::count(channels)), + SampleFormat::Compressed, + ), + }, CodecId::Pcm => { let pcm = params.pcm.ok_or_else(|| AudioError::Malformed("PCM track without a sample format".into()))?; let (kind, sample_format) = pcm_kind(pcm.bits, pcm.float, pcm.big_endian, !pcm.signed) @@ -184,7 +206,7 @@ impl TrackDecoder { } /// The codec, refined from the stream: [`CodecId::HeAac`] for AAC with spectral band - /// replication, `Eac3` for Dolby Digital Plus. + /// replication, `Eac3` for Dolby Digital Plus, `Mp1`/`Mp2` by layer. pub fn codec(&self) -> CodecId { self.codec } @@ -209,7 +231,7 @@ impl TrackDecoder { self.sample_format } - /// Decode one packet. An MP4 or Matroska packet may hold several AC-3 frames; + /// Decode one packet. An MP4 or Matroska packet may hold several AC-3 or MPEG audio frames; /// an E-AC-3 dependent substream decodes to no frames. A packet that fails is an error for /// every codec: call [`TrackDecoder::conceal`] for audio of its length, so what follows keeps /// its timing. @@ -243,6 +265,25 @@ impl TrackDecoder { } reorder(out, *order) } + #[cfg(feature = "mpa-decode")] + Inner::Mpa(d) => { + let mut out: Vec> = Vec::new(); + let mut p = 0; + while let Some(rest) = packet.get(p..).filter(|r| !r.is_empty()) { + let f = match d.decode(rest) { + Ok(f) => f, + Err(e) if out.is_empty() => return Err(e.into()), + Err(_) => break, + }; + p += f.header.frame_bytes.max(1); + if self.last_frames == 0 { + self.sample_rate = f.header.sample_rate; + self.layout = mpa_layout(&f.header); + } + append(&mut out, f.channels)?; + } + out + } #[cfg(feature = "ac3-decode")] Inner::Ac3(d, shape) => { let mut out: Vec> = Vec::new(); @@ -320,6 +361,8 @@ impl TrackDecoder { } #[cfg(feature = "ac3-decode")] Inner::Ac3(..) => Some(ac3_packet_frames(data)), + #[cfg(feature = "mpa-decode")] + Inner::Mpa(_) => mpa::parse_header(data).ok().map(|h| h.samples()), Inner::Pcm { kind, channels } => Some(data.len() / (kind.bytes() * *channels).max(1)), }; let frames = frames.filter(|&n| n > 0).unwrap_or(self.last_frames); @@ -329,8 +372,8 @@ impl TrackDecoder { /// The frames every packet decodes to, for codecs whose packets all have one length: AAC /// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this) and /// AC-3 / E-AC-3 (the frames of the last decoded packet, else of the first packet's primary - /// syncframes, else 1536); `None` for Opus and PCM, and for AC-3 / E-AC-3 whose packets read so - /// far differ in length. + /// syncframes, else 1536) and MP1/MP2 (one frame per packet); `None` for Opus and PCM, and for + /// AC-3 / E-AC-3 whose packets read so far differ in length. pub fn frames_per_packet(&self) -> Option { match &self.inner { #[cfg(feature = "aac-decode")] @@ -344,15 +387,17 @@ impl TrackDecoder { }), #[cfg(feature = "opus-decode")] Inner::Opus { .. } => None, + #[cfg(feature = "mpa-decode")] + Inner::Mpa(_) => Some(if self.codec == CodecId::Mp1 { 384 } else { 1152 }), Inner::Pcm { .. } => None, } } /// Frames to decode before a seek target and drop, so the decoder has settled when the target - /// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 frame (its filter bank - /// overlaps one frame), none for PCM. E-AC-3 syncframes of fewer than six blocks may reuse - /// exponents from any syncframe before them, so once one has been read the stream decodes from - /// its start (`usize::MAX`). + /// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 or MPEG audio frame + /// (their filter banks overlap one frame), none for PCM. E-AC-3 syncframes of fewer than six + /// blocks may reuse exponents from any syncframe before them, so once one has been read the + /// stream decodes from its start (`usize::MAX`). pub fn preroll(&self) -> usize { match &self.inner { #[cfg(feature = "aac-decode")] @@ -363,6 +408,8 @@ impl TrackDecoder { Inner::Ac3(..) => AC3_FRAME_SAMPLES, #[cfg(feature = "opus-decode")] Inner::Opus { .. } => 3840, + #[cfg(feature = "mpa-decode")] + Inner::Mpa(_) => self.last_frames.max(1152), Inner::Pcm { .. } => 0, } } @@ -385,6 +432,8 @@ impl TrackDecoder { Inner::Ac3(d, _) => d.reset(), #[cfg(feature = "opus-decode")] Inner::Opus { dec, .. } => dec.reset(), + #[cfg(feature = "mpa-decode")] + Inner::Mpa(d) => d.reset(), Inner::Pcm { .. } => {} } } @@ -395,6 +444,7 @@ fn unsupported(codec: CodecId) -> AudioError { CodecId::Aac | CodecId::HeAac => Some("aac-decode"), CodecId::Ac3 | CodecId::Eac3 => Some("ac3-decode"), CodecId::Opus => Some("opus-decode"), + CodecId::Mp1 | CodecId::Mp2 => Some("mpa-decode"), _ => None, }; match feature { @@ -419,7 +469,7 @@ fn reorder(mut channels: Vec>, order: Option<&[usize]>) -> Vec /// Append `more` to `out` channel by channel. A packet whose frames differ in channel count is /// malformed: one buffer cannot hold them. -#[cfg(any(feature = "aac-decode", feature = "ac3-decode"))] +#[cfg(any(feature = "aac-decode", feature = "ac3-decode", feature = "mpa-decode"))] fn append(out: &mut Vec>, more: Vec>) -> Result<()> { if out.is_empty() { *out = more; @@ -599,6 +649,12 @@ pub(crate) fn aac_layout(channel_config: u8, channels: usize) -> ChannelLayout { } } +#[cfg(feature = "mpa-decode")] +/// MPEG audio: mono, or stereo (also for dual channel and joint stereo). +pub(crate) fn mpa_layout(h: &mpa::Header) -> ChannelLayout { + if h.channels() == 1 { ChannelLayout::MONO } else { ChannelLayout::STEREO } +} + #[cfg(feature = "ac3-decode")] /// The speakers of an AC-3 / E-AC-3 program (A/52 Table 5.8) in the decoder's WAV order: fronts, /// LFE, surrounds. Dual mono (1+1) has no speaker assignment. diff --git a/crates/audio/tests/codecs/main.rs b/crates/audio/tests/codecs/main.rs index 02932b10..3f6bea5f 100644 --- a/crates/audio/tests/codecs/main.rs +++ b/crates/audio/tests/codecs/main.rs @@ -10,6 +10,7 @@ mod testkit; mod aac; mod ac3_oracle; mod concealment; +mod mpa_oracle; mod opus; mod routing_oracle; mod track_decoder; diff --git a/crates/audio/tests/codecs/mpa_oracle.rs b/crates/audio/tests/codecs/mpa_oracle.rs new file mode 100644 index 00000000..53f80c7f --- /dev/null +++ b/crates/audio/tests/codecs/mpa_oracle.rs @@ -0,0 +1,586 @@ +//! 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 craft_audio::mpa::{Decoder, Mode, parse_header}; +use craft_media::bitstream::BitWriter; + +/// 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 { + crate::testkit::oracle::find_tool(name) +} + +fn make_encoded(ff: &Path, s: &Encoded) -> Option { + let out = crate::testkit::fixtures_dir("mpa").join(format!("{}.mp2", s.name)); + if s.twolame { + let twolame = tool("twolame")?; + return crate::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 + }); + } + crate::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 = 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 = require_ffmpeg!(); + for (k, &(name, layer, lsf, br, sr, mode, ext)) in SYNTHETIC.iter().enumerate() { + let out = crate::testkit::fixtures_dir("mpa").join(format!("{name}.mp{layer}")); + let f = crate::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 = 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) = crate::testkit::ffmpeg_or_skip("mpa fixtures") else { return }; + for s in ENCODED { + let out = crate::testkit::fixtures_dir("mpa").join(format!("{}.mp2", s.name)); + crate::testkit::fixtures::generate_and_report(&format!("mpa/{}", s.name), &[out], || make_encoded(&ff, s)); + } +} diff --git a/crates/audio/tests/codecs/routing_oracle.rs b/crates/audio/tests/codecs/routing_oracle.rs index 664a7e98..c35a8900 100644 --- a/crates/audio/tests/codecs/routing_oracle.rs +++ b/crates/audio/tests/codecs/routing_oracle.rs @@ -214,6 +214,59 @@ const CASES: &[Case] = &[ frames: Some(240_000), min_snr: 25.0, }, + Case { + name: "mp2.mp2", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "mp2"], + container: Container::MpegAudio, + codec: CodecId::Mp2, + layout: ChannelLayout::STEREO, + channels: 2, + frames: None, + min_snr: 80.0, + }, + // ffmpeg's default for mono MP2 is 384 kbit/s, which ISO/IEC 11172-3 does not allow. + Case { + name: "mp2-mono.mp2", + surround: false, + mono: true, + rate: 48_000, + args: &["-c:a", "mp2"], + container: Container::MpegAudio, + codec: CodecId::Mp2, + layout: ChannelLayout::MONO, + channels: 1, + frames: None, + min_snr: 80.0, + }, + Case { + name: "mp2.mkv", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "mp2"], + container: Container::Matroska, + codec: CodecId::Mp2, + layout: ChannelLayout::STEREO, + channels: 2, + frames: Some(240_000), + min_snr: 80.0, + }, + Case { + name: "mp2.mp4", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "mp2"], + container: Container::Mp4, + codec: CodecId::Mp2, + layout: ChannelLayout::STEREO, + channels: 2, + frames: Some(240_000), + min_snr: 80.0, + }, Case { name: "pcm-s24be.mov", surround: false, @@ -288,6 +341,7 @@ fn reference_lead(name: &str) -> usize { match name { "aac.mkv" => 1024, "eac3-5.1.mkv" => 256, + "mp2.mkv" => 481, _ => 0, } } @@ -315,6 +369,8 @@ fn early_end(name: &str) -> usize { match name { "ac3.mp4" => 288, "eac3-5.1.mkv" => 240, + "mp2.mkv" => 480, + "mp2.mp4" => 528, _ => 0, } } @@ -437,7 +493,7 @@ fn track_decoder_on_demuxed_packets_matches_decode() { use craft_audio::TrackDecoder; use craft_media::isobmff; let ff = require_ffmpeg!(); - for name in ["aac.m4a", "aac-5.1.m4a", "opus.mp4", "pcm-s24be.mov"] { + for name in ["aac.m4a", "aac-5.1.m4a", "opus.mp4", "mp2.mp4", "pcm-s24be.mov"] { let c = CASES.iter().find(|c| c.name == name).unwrap(); let Some(path) = fixture(&ff, c) else { continue }; let bytes = std::fs::read(&path).unwrap(); diff --git a/crates/audio/tests/codecs/testkit.rs b/crates/audio/tests/codecs/testkit.rs index 1959fd3a..ba2c9279 100644 --- a/crates/audio/tests/codecs/testkit.rs +++ b/crates/audio/tests/codecs/testkit.rs @@ -38,6 +38,15 @@ pub fn fixtures_dir(sub: &str) -> PathBuf { d } +pub mod oracle { + use std::path::PathBuf; + + /// A tool by name, silently. + pub fn find_tool(name: &str) -> Option { + crate::common::tool(name) + } +} + pub mod fixtures { use super::*;