Merge pull request #819 from vangel76/gap-mp2-decoder

#809: own MPEG audio Layer I / II decoder plays MP2 at ffmpeg's default bit rates
This commit is contained in:
Brandon Thomas 2026-10-11 21:02:50 -04:00
commit 06e5ac6651
15 changed files with 1656 additions and 13 deletions

10
Cargo.lock generated
View File

@ -1830,6 +1830,7 @@ dependencies = [
"filmcraft-isobmff",
"filmcraft-matroska",
"filmcraft-media",
"filmcraft-mpa",
"filmcraft-mpeg2v",
"filmcraft-mpegts",
"filmcraft-mxf",
@ -2088,6 +2089,15 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-mpa"
version = "0.6.0"
dependencies = [
"filmcraft-bitstream",
"filmcraft-testkit",
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-mpeg2v"
version = "0.6.0"

View File

@ -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" }

View File

@ -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 }

View File

@ -21,7 +21,7 @@ enum Inner {
/// AAC whose configuration arrives with the first frame (ADTS / LATM).
AacPending,
/// MPEG audio at this rate whose layer arrives with the first frame (MP4 / QuickTime name the
/// codec, not the layer, and symphonia's decoder reads only the layer it was made for).
/// codec, not the layer; layers I / II go to our decoder, layer III to symphonia's).
MpegPending(u32),
/// Our own Opus decoder (always 48 kHz output; pre-skip is left to container timestamps).
/// `order` maps output channel → decoded channel (Vorbis → WAV/SMPTE order for surround).
@ -30,6 +30,8 @@ enum Inner {
Symphonia(Box<dyn Decoder>),
/// Our AC-3 and E-AC-3 decoder (ATSC A/52, Annex E).
Ac3(Box<filmcraft_ac3::Decoder>),
/// Our MPEG-1/2 audio Layer I and II decoder (ISO/IEC 11172-3, 13818-3).
Mpa(Box<filmcraft_mpa::Decoder>),
}
/// Opus always decodes at 48 kHz (RFC 7845 §5.1: the input rate in the header is informational).
@ -130,11 +132,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<Self> {
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)
@ -231,6 +234,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<f32>> = 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()))?;
@ -259,6 +287,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(),
@ -768,6 +797,22 @@ pub struct AudioFileSource {
impl AudioFileSource {
pub fn decode(name: &str, bytes: Arc<[u8]>) -> Result<Self> {
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);
@ -823,6 +868,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<Vec<f32>>)> {
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);
size.saturating_add(if flags & 0x10 != 0 { 20 } else { 10 })
}
_ => 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<f32>> = 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
@ -843,6 +931,57 @@ pub fn opener(name: &str, bytes: Arc<[u8]>) -> Option<std::result::Result<Shared
Some(AudioFileSource::decode(name, bytes).map(|s| Arc::new(s) as SharedSource).map_err(Into::into))
}
#[cfg(test)]
mod mpeg_layer12_tests {
use super::*;
/// A silent 48 kHz MPEG-1 Layer II frame (all-zero allocations) of `bitrate_index` and `mode`.
fn silent_frame(bitrate_index: u8, mode: u8) -> Vec<u8> {
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());
// 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());
}
}
#[cfg(test)]
mod he_aac_tests {
use super::*;

View File

@ -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<String> {
let cat = |p: &[&[&str]]| p.concat().into_iter().map(String::from).collect::<Vec<_>>();
match name {
@ -142,6 +145,12 @@ fn spec(name: &str) -> Vec<String> {
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,53 @@ 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 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 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 (delay, lags): (usize, Vec<i64>) = 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 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, 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} at lag {lag} (CodecDelay {delay}), peak {peak}");
assert!(peak > 0.1 && worst <= 2e-4, "{name}: peak {peak}, max error {worst} at lag {lag}");
}
}
#[test]
fn truncated_and_corrupt_files_never_panic() {
let ff = filmcraft_testkit::require_ffmpeg!();

17
crates/mpa/Cargo.toml Normal file
View File

@ -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 }

76
crates/mpa/README.md Normal file
View File

@ -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.

411
crates/mpa/src/lib.rs Normal file
View File

@ -0,0 +1,411 @@
//! 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<T> = std::result::Result<T, Error>;
/// 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<Header> {
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<Vec<f32>>,
}
/// 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<Subbands>,
}
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<Frame> {
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<u32> {
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<u16> { 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<f32> { 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;

203
crates/mpa/src/tables.rs Normal file
View File

@ -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<i32> {
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<u32> {
match steps {
3 => Some(5),
5 => Some(7),
9 => Some(10),
_ => None,
}
}

151
crates/mpa/src/tests.rs Normal file
View File

@ -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<u8> {
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<f64> = (0..=256).map(|i| window_half(i).unwrap() as f64 * if (i / 64) % 2 == 1 { -1.0 } else { 1.0 }).collect();
let d1: Vec<f64> = h.windows(2).map(|w| w[1] - w[0]).collect();
let d2: Vec<f64> = 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()));
}
}
}

572
crates/mpa/tests/oracle.rs Normal file
View File

@ -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<PathBuf> {
filmcraft_testkit::oracle::find_tool(name)
}
fn make_encoded(ff: &Path, s: &Encoded) -> Option<PathBuf> {
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<f32> {
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<f32>> = 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<u8> {
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<u32> {
let rep = |v: &[(u32, usize)]| v.iter().flat_map(|&(n, k)| std::iter::repeat_n(n, k)).collect::<Vec<_>>();
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<u8> {
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<u8> = (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<Vec<u8>> = 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));
}
}

View File

@ -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-<dir>` (`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 |

View File

@ -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) in raw, MP4, MOV (#800), Matroska, TS, PS; **not** MP2 mono above 192 kbit/s or stereo below 64 kbit/s (ffmpeg's mono default; symphonia rejects them) | **none** | MP2 sample-exact vs ffmpeg (raw, MP4, MOV, TS, PS) | 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); in raw, MP4, MOV (#800), Matroska, TS, PS | **none** | MP1 / MP2: SNR vs ffmpeg 73–84 dB; MP2 in MP4 / MOV within 1e-4 of ffmpeg, also after a seek (#800) | 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 |

View File

@ -174,13 +174,13 @@ 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. MP2 (MPEG audio layer II) in MP4 and
MOV, sample-exact against ffmpeg (#800).
- **Missing:** MP2 at bit rates ISO/IEC 11172-3 disallows: mono above 192 kbit/s (ffmpeg's default
for mono MP2 is 384 kbit/s) and stereo below 64 kbit/s; the bootstrap decoder (symphonia, also
0.6) rejects every such frame, so the track plays silent: needs our own layer I/II decoder.
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, MP4 and MOV (#800), 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, HEIF / HEIC, Radiance HDR, AI / EPS, and the rarer

View File

@ -40,6 +40,7 @@ const LAYERS: &[(&str, u8)] = &[
("mpegts", 0),
("mpeg2v", 0),
("ac3", 0),
("mpa", 0),
("ogg", 0),
("riff", 0),
("h264", 0),
@ -407,6 +408,7 @@ const FIXTURE_GENERATORS: &[(&str, &str)] = &[
("codecs", "mpeg_oracle"),
("mpeg2v", "oracle"),
("ac3", "oracle"),
("mpa", "oracle"),
("codecs", "ogg_oracle"),
];