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 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
This commit is contained in:
Aleksander Helgaker
2026-11-11 08:02:37 -05:00
committed by GitHub
co-authored by Claude Opus 5.5 Dylan M. Taylor
parent b1c64766e6
commit ca53e90b18
16 changed files with 1598 additions and 59 deletions
+1 -1
View File
@@ -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::"
+2 -2
View File
@@ -93,8 +93,8 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
applications' shortcut sets. Command catalogs, file readers and the shortcut editor UI stay
in each app.
- [`craft-audio`](crates/audio/README.md): audio file I/O and codecs (WAV, AIFF, FLAC, AAC, Opus,
AC-3/E-AC-3), streaming and seeking, and packet decoding for any demuxer. See its README for
formats and features.
AC-3/E-AC-3, MP1/MP2), streaming and seeking, and packet decoding for any demuxer. See its
README for formats and features.
- [`craft-media`](crates/media/README.md): bit-level I/O (`bitstream`), clean-room MP4/MOV
(`isobmff`) and Matroska/WebM (`matroska`) demuxers and muxers, an Ogg demuxer (`ogg`), from
FilmCraft, and an ISO BMFF box reader. Containers only; codecs are in `craft-audio`, and
+7 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "craft-audio"
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC decoder and encoder, Opus and AC-3/E-AC-3 decoders."
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC decoder and encoder, Opus, AC-3/E-AC-3 and MP1/MP2 decoders."
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -14,12 +14,12 @@ all-features = true
workspace = true
[features]
# Probe and decode files: WAV and AIFF natively; AAC, Opus, AC-3/E-AC-3 and PCM through
# craft-media's demuxers and this crate's decoders; MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM,
# A-law and µ-law through symphonia.
# Probe and decode files: WAV and AIFF natively; AAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM through
# craft-media's demuxers and this crate's decoders; MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law
# and µ-law through symphonia.
decode = ["decode-native", "symphonia"]
# `decode` without symphonia: only the formats with a native decoder (others are Unsupported).
decode-native = ["aac-decode", "opus-decode", "ac3-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
decode-native = ["aac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
# The symphonia fallback for the formats with no native decoder yet.
symphonia = ["decode-native", "dep:symphonia"]
# Every encoder: the lossless file writers and the AAC encoder.
@@ -35,6 +35,7 @@ aac-decode = ["craft-media/bitstream"]
aac-encode = ["craft-media/bitstream"]
opus-decode = []
ac3-decode = []
mpa-decode = ["craft-media/bitstream"]
# Multi-resolution waveform overviews (`peaks`).
peaks = []
# Encode AAC channels in parallel (with `aac-encode`).
@@ -49,7 +50,7 @@ serde = { workspace = true, optional = true }
log = { version = "0.4", optional = true }
rayon = { workspace = true, optional = true }
# symphonia keeps its AAC decoder for AAC in CAF, a container craft-media does not read.
symphonia = { version = "0.6.1", optional = true, default-features = false, features = ["mp1", "mp2", "mp3", "flac", "vorbis", "ogg", "wav", "pcm", "aiff", "isomp4", "aac", "alac", "adpcm", "caf", "mkv"] }
symphonia = { version = "0.6.1", optional = true, default-features = false, features = ["mp3", "flac", "vorbis", "ogg", "wav", "pcm", "aiff", "isomp4", "aac", "alac", "adpcm", "caf", "mkv"] }
[dev-dependencies]
craft-media-testkit = { workspace = true }
+14 -11
View File
@@ -1,7 +1,7 @@
# craft-audio
Audio file I/O and codecs for the Craft apps. Native implementations: WAV and AIFF read/write,
FLAC encoder, AAC decoder/encoder, Opus and AC-3/E-AC-3 decoders.
FLAC encoder, AAC decoder/encoder, Opus, AC-3/E-AC-3 and MP1/MP2 decoders.
Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced
by native decoders.
@@ -24,7 +24,7 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
| Feature | Provides | Pulls in |
|---|---|---|
| `decode` | `decode`, `probe`, `Decoder`, `tracks`, `TrackDecoder`; every format in [Format support](#format-support) | `decode-native`, `symphonia` |
| `decode-native` | the same, native formats only (others are `Unsupported`) | the three decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` |
| `decode-native` | the same, native formats only (others are `Unsupported`) | the four decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` |
| `symphonia` | the fallback for formats with no native decoder; turning it on is the same as `decode` | `decode-native`, symphonia |
| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC packet encoder | `lossless-encode`, `aac-encode` |
| `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` |
@@ -33,11 +33,12 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
| `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder` | craft-media `bitstream` |
| `opus-decode` | `opus::Decoder`/`StreamDecoder` | none |
| `ac3-decode` | `ac3::Decoder` | none |
| `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` |
| `peaks` | `peaks::Peaks` | none |
| `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` |
| `serde` | `Serialize`/`Deserialize` on the shared types | `serde` |
Any one decoder feature (`aac-decode`, `opus-decode`, `ac3-decode`) also gives
Any one decoder feature (`aac-decode`, `opus-decode`, `ac3-decode`, `mpa-decode`) also gives
`TrackDecoder`, for that codec and PCM.
An app that decodes 32-bit FLAC with `decode` needs a profile override in its workspace; see
@@ -179,7 +180,7 @@ for p in &packets {
}
```
It decodes AAC (finding HE-AAC in the lookahead), Opus, AC-3/E-AC-3 and PCM, and returns
It decodes AAC (finding HE-AAC in the lookahead), Opus, AC-3/E-AC-3, MP1/MP2 and PCM, and returns
`AudioBuffer`s in WAV order when `layout()` is ordered. `codec()`, `sample_rate()` and `layout()`
describe the output. Container timing is the caller's.
@@ -192,7 +193,7 @@ target, call `reset()`, decode from there, and drop the output before the target
The codec modules are also usable on their own. `aac::Decoder` and `opus::Decoder` return the
codec's channel order; `aac::wav_order(channel_config)` and `opus::wav_order(channels)` map it to
WAV order. `ac3::Decoder` outputs WAV order.
WAV order. `ac3::Decoder` and `mpa::Decoder` output WAV order.
### Encode a file
@@ -293,7 +294,8 @@ match craft_audio::decode(&bytes, None) {
| FLAC | symphonia | native |
| AAC-LC | native | native |
| HE-AAC v1/v2 | native, core only (see [Limits](#limits)) | — |
| MP1, MP2, MP3 (MPEG audio Layer I/II/III) | symphonia | — |
| MP1, MP2 (MPEG audio Layer I/II) | native | — |
| MP3 (Layer III) | symphonia | — |
| Opus | native | — |
| AC-3, E-AC-3 | native | — |
| ALAC, Vorbis, ADPCM, A-law, µ-law | symphonia | — |
@@ -303,8 +305,8 @@ match craft_audio::decode(&bytes, None) {
| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (ALAC, MP3, FLAC) | edit lists (AAC priming, Opus pre-skip) |
| Matroska, WebM | `craft_media::matroska` for native codecs; symphonia for the others (Vorbis, FLAC, MP3) | `CodecDelay`, `DiscardPadding` |
| Ogg | `craft_media::ogg` (Opus); symphonia (Vorbis, FLAC) | pre-skip, end trimming |
| Raw ADTS AAC, AC-3, E-AC-3 | native frame sync | — |
| Raw MP1/MP2/MP3 | symphonia | — |
| Raw ADTS AAC, AC-3, E-AC-3, MP1/MP2 | native frame sync | — |
| Raw MP3 | symphonia | — |
| FLAC | symphonia | — |
| CAF | symphonia | — |
@@ -387,6 +389,7 @@ Each codec module's rustdoc lists the coding tools it implements.
| AAC (`aac`) | ISO/IEC 14496-3 | AAC-LC decode and encode (CBR, VBR, ADTS), 1–8 channels; HE-AAC: see [Limits](#limits) | ffmpeg: ~135 dB SNR; PNS noise differs per sample at equal band energy |
| Opus (`opus`) | RFC 6716, 8251, 7845 | decoder: SILK, hybrid, CELT, all frame sizes, PLC and FEC, multistream (`Decoder`) and single stream (`StreamDecoder`), 8–48 kHz output | all 36 RFC 8251 conformance cases |
| AC-3/E-AC-3 (`ac3`) | ATSC A/52:2012 | all AC-3 channel modes and frame sizes; E-AC-3 independent substream 0 with spectral extension; output in WAV order, no downmix | ffmpeg: 65–94 dB SNR on tones, 27–48 dB on white noise (see [Limits](#limits)) |
| MP1/MP2 (`mpa`) | ISO/IEC 11172-3, 13818-3 | Layer I and II, MPEG-1/2/2.5, 8–48 kHz; also the Layer II rate/mode pairs the spec disallows (ffmpeg's default mono 384 kbit/s); no free format | ffmpeg-encoded streams, 73–84 dB SNR |
## symphonia
@@ -452,7 +455,7 @@ the full tests with ffmpeg and the vectors required.
| Part | Source |
|---|---|
| WAV, AIFF, `decode`/`probe`, peaks | SoundCraft `soundcraft-audio-io` [7dc56a4](https://github.com/storytold/soundcraft/commit/7dc56a4dbeebf57eacece09536726c57844fc59d) |
| FLAC encoder, AAC, Opus decoder, AC-3, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) |
| FLAC encoder, AAC, Opus decoder, AC-3, MP1/MP2, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) |
Where apps had overlapping implementations, the one that measured better was kept, with the
tests of both:
@@ -461,7 +464,7 @@ tests of both:
|---|---|---|
| FLAC encoding | FilmCraft | 0.1–37% smaller output than SoundCraft's on 12 test signals; STREAMINFO MD5 |
| WAV/AIFF writing | SoundCraft | only one with BWF, dither, arbitrary speaker masks, streaming without known length |
| AAC-LC decoding | FilmCraft (over symphonia) | equal SNR against ffmpeg; also decodes 5.1 AAC, which symphonia rejects |
| AAC-LC, MP1/MP2 decoding | FilmCraft (over symphonia) | equal SNR against ffmpeg; also decodes 5.1 AAC and MP2 bit rates symphonia rejects |
## Migration notes for apps
@@ -499,7 +502,7 @@ The codec modules' public items keep their names, so apps can re-export them
short slices instead of filling them with silence.
- `DecodeOptions` is not `Copy`.
- The FLAC encoder's vendor string is `craft-audio <version>`.
- `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`.
+1 -1
View File
@@ -145,7 +145,7 @@ impl TryFrom<RawLayout> 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)
}
+21 -9
View File
@@ -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<ac3::Error> for AudioError {
}
}
#[cfg(feature = "mpa-decode")]
impl From<mpa::Error> 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]
+432
View File
@@ -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<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 {
/// 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<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.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Frame {
/// The frame's header.
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 {
/// 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<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
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,
}
}
+154
View File
@@ -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<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()));
}
}
}
+6 -6
View File
@@ -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<()> {
+31 -5
View File
@@ -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<Result<Native<'a>>> {
Some(Ok(n))
}
/// A raw MPEG audio layer I or II stream.
fn open_mpa<'a>(src: &Src<'a>, at: u64) -> Option<Result<Native<'a>>> {
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<Result<Native<'a>>> {
let frames = match raw_frames(src, at, |rest| {
+73 -17
View File
@@ -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<opus::Decoder>,
order: Option<&'static [usize]>,
},
#[cfg(feature = "mpa-decode")]
Mpa(Box<mpa::Decoder>),
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<u8>]) -> 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<TrackDecoder> {
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<f32>> = 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<f32>> = 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<usize> {
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<Vec<f32>>, order: Option<&[usize]>) -> Vec<Vec<f32>
/// 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<Vec<f32>>, more: Vec<Vec<f32>>) -> 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.
+1
View File
@@ -10,6 +10,7 @@ mod testkit;
mod aac;
mod ac3_oracle;
mod concealment;
mod mpa_oracle;
mod opus;
mod routing_oracle;
mod track_decoder;
+586
View File
@@ -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<PathBuf> {
crate::testkit::oracle::find_tool(name)
}
fn make_encoded(ff: &Path, s: &Encoded) -> Option<PathBuf> {
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<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 = 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 = 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<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 = 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) = 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));
}
}
+57 -1
View File
@@ -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();
+9
View File
@@ -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<PathBuf> {
crate::common::tool(name)
}
}
pub mod fixtures {
use super::*;