craft-audio: native MP3 (Layer III) decoder (feature mp3-decode)

* craft-audio: native MP3 (Layer III) decoder (feature `mp3-decode`)

A clean-room Layer III decoder written from ISO/IEC 11172-3 and 13818-3:
MPEG-1, MPEG-2 and MPEG-2.5 (8-48 kHz), CBR and VBR, mono, stereo, dual
channel and joint stereo (mid/side and intensity), long, short and mixed
blocks, and the bit reservoir. It shares the encoder's Huffman and
scalefactor band tables and the MP1/MP2 synthesis filter.

`decode`, `Decoder` and `TrackDecoder` now read raw .mp3 files and MP3 in
MP4 and Matroska natively instead of through symphonia. A Xing / Info / VBRI
frame is not a packet, and the LAME tag's encoder delay and padding are
applied, so a file decodes to its exact presented length. The seek
pre-roll covers how far back the bit reservoir can reach, so a seek is
bit-exact. symphonia stays the fallback for free-format MP3.

The encoder moves to mp3/encoder.rs so that each half builds without the
other.

* craft-audio: MP3 review and CI fixes

- Huffman: big values stop at the granule's part2_3_length. A damaged
  granule no longer reads the next granule's bits, as ffmpeg does.
- Joint stereo runs on the lines in coded order, before short blocks are
  reordered, as ffmpeg and libmad do. Where the two channels' block layouts
  differ, against the specification, each channel is then reordered by its
  own layout.
- Seek pre-roll: sized from the smallest frame the stream can hold (the
  lowest bit rate for its version, rate, mode and CRC), not the last frame,
  so a seek into a VBR stream's small frames still has the reservoir. The
  decoder learns the stream's format from its first frame.
- Mixed-block oracle test: edit only granules whose scalefactors take no
  bits, so the edited streams stay valid, and not before a pure short
  block. ffmpeg's short-block IMDCT drops overlap that a mixed block's long
  subbands carry, where the specification adds it. Use 16 kHz instead of
  12 kHz.
- Routing oracle: ffmpeg before 9 decodes the 1105 samples of the MP3
  Matroska CodecDelay.

* craft-audio: MPEG audio packets of several frames keep their length

An MP4 or Matroska packet may hold several MP1/MP2/MP3 frames.
TrackDecoder now counts the frames each packet holds from their headers:

- frames_per_packet is None once a packet in the lookahead or a decoded
  packet has held several frames, so an MP4 track's length is no longer
  capped at one frame per sample and the container's timing decides.
- Concealment is the length of the frames a damaged packet holds. For MP3
  a packet lost whole is as long as the last packet, and one frame before
  any is decoded.

Also corrects the Huffman doc comment: a big-value pair that starts
before the granule's end is kept whole, as ffmpeg does.

* craft-audio: MPEG audio concealment after partial packets and before the first

- A packet whose later frame fails decodes only in part: like AC-3, it no
  longer replaces the length of the last whole packet that concealment
  uses.
- MP1/MP2: a packet lost before any has decoded, or right after a seek, is
  one frame (384 or 1152 samples) of silence rather than none, as MP3's
  already is.

---------

Co-authored-by: alehel <git@ornumhelgaker.no>
This commit is contained in:
Aleksander Helgaker authored and GitHub committed 2026-11-12 09:12:40 -05:00
1 parent 77a3ddbf26
commit 493476eb8d
17 files changed
+2370 -563

No files matched your search

+7 -5
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 and ALAC decoders and encoders, AC-3/E-AC-3 decoder and encoder, MP3 encoder, Opus and MP1/MP2 decoders."
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, AC-3/E-AC-3 decoder and encoder, MP3 decoder and encoder, Opus 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, ALAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM
# through craft-media's demuxers and this crate's decoders; MP3, FLAC, Vorbis, CAF, ADPCM, A-law
# and µ-law through symphonia.
# Probe and decode files: WAV and AIFF natively; AAC, ALAC, Opus, AC-3/E-AC-3, MP1/MP2/MP3 and
# PCM through craft-media's demuxers and this crate's decoders; FLAC, Vorbis, CAF, ADPCM, A-law,
# µ-law and free-format MP3 through symphonia.
decode = ["decode-native", "symphonia"]
# `decode` without symphonia: only the formats with a native decoder (others are Unsupported).
decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "mp3-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, ALAC, AC-3/E-AC-3 and MP3 encoders.
@@ -36,6 +36,8 @@ aac-encode = ["craft-media/bitstream"]
opus-decode = []
ac3-decode = ["craft-media/bitstream"]
mpa-decode = ["craft-media/bitstream"]
# MPEG-1/2 Layer III (MP3) decoder (`mp3::Decoder`).
mp3-decode = ["mpa-decode"]
# ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation.
alac-decode = []
alac-encode = []
+12 -8
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 and ALAC decoders/encoders, AC-3/E-AC-3 decoder/encoder, MP3 encoder, Opus and MP1/MP2 decoders.
FLAC encoder, AAC and ALAC decoders/encoders, AC-3/E-AC-3 decoder/encoder, MP3 decoder/encoder, Opus and MP1/MP2 decoders.
Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced
by native decoders.
@@ -26,7 +26,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`); `wav_pcm`, `aiff_pcm` (a file's integer PCM bit for bit, as `RawPcm`) | the five decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` |
| `decode-native` | the same, native formats only (others are `Unsupported`); `wav_pcm`, `aiff_pcm` (a file's integer PCM bit for bit, as `RawPcm`) | the six 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, ALAC, AC-3/E-AC-3 and MP3 encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode`, `mp3-encode` |
| `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` |
@@ -39,12 +39,14 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
| `ac3-decode` | `ac3::Decoder`, `ac3::ProgrammeDecoder` | craft-media `bitstream` |
| `ac3-encode`, `eac3-encode` | `ac3::encoder::Encoder` | craft-media `bitstream` |
| `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` |
| `mp3-decode` | `mp3::Decoder`, `mp3::InfoTag` | `mpa-decode` |
| `mp3-encode` | `mp3::Encoder` | 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`, `alac-decode`, `opus-decode`, `ac3-decode`, `mpa-decode`)
Any one decoder feature (`aac-decode`, `alac-decode`, `opus-decode`, `ac3-decode`, `mpa-decode`,
`mp3-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
@@ -310,18 +312,18 @@ match craft_audio::decode(&bytes, None) {
| HE-AAC v1/v2 | native, core only (see [Limits](#limits)) | — |
| ALAC | native | native, packets (`alac::Encoder`; no file writer) |
| MP1, MP2 (MPEG audio Layer I/II) | native | — |
| MP3 (Layer III) | symphonia | native, frames (`mp3::Encoder`; the frames in a row are an `.mp3` file) |
| MP3 (Layer III) | native | native, frames (`mp3::Encoder`; the frames in a row are an `.mp3` file) |
| Opus | native | — |
| AC-3, E-AC-3 | native | native packet encoder |
| Vorbis, ADPCM, A-law, µ-law | symphonia | — |
| Container | Demuxer | Timing |
|---|---|---|
| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (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` |
| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (FLAC) | edit lists (AAC priming, Opus pre-skip) |
| Matroska, WebM | `craft_media::matroska` for native codecs; symphonia for the others (Vorbis, FLAC) | `CodecDelay`, `DiscardPadding` |
| Ogg | `craft_media::ogg` (Opus); symphonia (Vorbis, FLAC) | pre-skip, end trimming |
| Raw ADTS AAC, AC-3, E-AC-3, MP1/MP2 | native frame sync | — |
| Raw MP3 | symphonia | — |
| Raw MP3 | native frame sync; symphonia for the free format | Xing / Info / VBRI frame skipped; LAME tag encoder delay and padding |
| FLAC | symphonia | — |
| CAF | symphonia | — |
@@ -369,7 +371,7 @@ match craft_audio::decode(&bytes, None) {
- `ChannelLayout::unordered` is written to FLAC as mask `0x0000`, as RFC 9639 §8.6.2 allows, but
ffmpeg and `flac -d` ignore that mask and use their default layout for the channel count.
- Not decoded at all: Speex.
- `TrackDecoder` returns `Unsupported` for MP3, FLAC and Vorbis; `decode` and `Decoder` read
- `TrackDecoder` returns `Unsupported` for FLAC and Vorbis; `decode` and `Decoder` read
those through symphonia. ALAC in CAF still goes through symphonia (craft-media has no CAF
demuxer).
- ALAC: `EncodeFormat` has no ALAC entry; `alac::Encoder` writes packets and the configuration,
@@ -415,6 +417,7 @@ Each codec module's rustdoc lists the coding tools it implements.
| 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`) | decoder: ATSC A/52:2012; encoder/programme API: ETSI TS 102 366 V1.4.1 | single-syncframe and complete-programme decoding; packet encoding through discrete 7.1 (see below) | decoder comparisons and encoder output through ffmpeg, CRC/channel/timing/bit-budget regressions; synthetic quality limitations below |
| 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 |
| MP3 decoder (`mp3`) | ISO/IEC 11172-3, 13818-3 | Layer III, MPEG-1/2/2.5, 8–48 kHz, every bit rate, CBR and VBR; mono, stereo, dual channel, joint stereo (mid/side and intensity); long, short and mixed blocks; bit reservoir; CRC read, not checked; Xing / Info / VBRI and LAME tags; no free format (read through symphonia) | ffmpeg's float decoder: 127–131 dB SNR on LAME streams of every sampling frequency (CBR, VBR, CRC, no reservoir, transients with short blocks) and on this crate's encoder's; intensity stereo and mixed blocks through LAME frames edited to use them (112–131 dB) |
| MP3 encoder (`mp3`) | ISO/IEC 11172-3, 13818-3 | Layer III, constant bit rate: 32–320 kbit/s at 32/44.1/48 kHz, 8–160 kbit/s at 16/22.05/24 kHz; mono, stereo, joint stereo (mid/side); bit reservoir; long blocks only; no Xing/Info tag, VBR, CRC or free format | ffmpeg decodes every Huffman code as intended; full encodes at every sampling frequency checked with ffprobe and by SNR of ffmpeg's decode |
| ALAC (`alac`) | Apple's reference implementation | decode and encode, 1–8 channels, 16/20/24/32 bits (decode: any depth 1–32), fast mode, magic cookie in MP4/Matroska and CAF form | Apple's reference: same packets from the encoder, same samples from both decoders on 766 configurations; ffmpeg: bit-exact both ways |
@@ -576,6 +579,7 @@ the full tests with ffmpeg and the vectors required.
|---|---|
| WAV, AIFF, `decode`/`probe`, peaks | SoundCraft `soundcraft-audio-io` [7dc56a4](https://github.com/storytold/soundcraft/commit/7dc56a4dbeebf57eacece09536726c57844fc59d) |
| FLAC encoder, AAC, Opus decoder, AC-3, MP1/MP2, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) |
| MP3 decoder | written here, from ISO/IEC 11172-3 and 13818-3; shares the encoder's tables and the MP1/MP2 synthesis filter |
| MP3 encoder | FilmCraft `filmcraft-mp3enc` [4bec22b](https://github.com/storytold/filmcraft/commit/4bec22bd03bd1d995c9cf72e02a284f96d3d72cd) |
| ALAC | Apple's reference implementation [macosforge/alac@c38887c](https://github.com/macosforge/alac/tree/c38887c5c5e64a4b31108733bd79ca9b2496d987/codec), Copyright © 2011 Apple Inc., Apache-2.0; ported to Rust and modified (see the `alac` module docs and [NOTICE](../../NOTICE)) |
+5 -5
View File
@@ -8,9 +8,9 @@
//! `wasm32-unknown-unknown`.
//!
//! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`]
//! (feature `decode`): WAV and AIFF are read natively; AAC, ALAC, 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, Vorbis, CAF, ADPCM, A-law, µ-law) through
//! (feature `decode`): WAV and AIFF are read natively; AAC, ALAC, Opus, AC-3/E-AC-3,
//! MP1/MP2/MP3 and PCM in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's
//! demuxers and [`TrackDecoder`]; everything else (FLAC, 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
@@ -25,7 +25,7 @@
//! - [`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`), [`alac`] (`alac-decode`, `alac-encode`), [`opus`] (`opus-decode`), [`ac3`]
//! (`ac3-decode`), [`mpa`] (`mpa-decode`), [`mp3`] (`mp3-encode`: the Layer III encoder),
//! (`ac3-decode`), [`mpa`] (`mpa-decode`), [`mp3`] (`mp3-decode`, `mp3-encode`),
//! [`flac`] (`flac-encode`: the packet encoder).
//!
//! The shared types below build with no features.
@@ -38,7 +38,7 @@ pub mod ac3;
pub mod alac;
mod buffer;
mod layout;
#[cfg(feature = "mp3-encode")]
#[cfg(any(feature = "mp3-encode", feature = "mp3-decode"))]
pub mod mp3;
#[cfg(feature = "mpa-decode")]
pub mod mpa;
File diff suppressed because it is too large. Load diff
+479
View File
@@ -0,0 +1,479 @@
//! The Layer III encoder; the module docs are in `mp3/mod.rs`.
// Index loops follow the specification's pseudo-code (FilmCraft allows this lint workspace-wide).
#![allow(clippy::needless_range_loop)]
use std::collections::VecDeque;
use craft_media::bitstream::BitWriter;
use super::filterbank::{Analysis, GRANULE};
use super::quantize::{Granule, Model};
use super::tables;
/// Settings an [`Encoder`] refuses.
#[non_exhaustive]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
/// A sample rate outside [`SAMPLE_RATES`].
#[error("MP3: unsupported sample rate {0} Hz (32, 44.1, 48 kHz, or 16, 22.05, 24 kHz)")]
SampleRate(u32),
/// A bit rate that is not one of [`bitrates`] for the sample rate.
#[error("MP3: unsupported bit rate {kbps} kbit/s at {rate} Hz")]
Bitrate {
/// The bit rate asked for, kbit/s.
kbps: u32,
/// The sample rate, Hz.
rate: u32,
},
/// More than two channels, or none.
#[error("MP3: unsupported channel count {0} (1 or 2)")]
Channels(usize),
}
/// Result with this module's [`Error`].
pub type Result<T> = std::result::Result<T, Error>;
/// How two channels are coded.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StereoMode {
/// Left and right coded independently (`mode` 00).
Stereo,
/// Joint stereo (`mode` 01): each frame chooses left / right or mid / side.
#[default]
JointStereo,
}
/// Encoder settings.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Config {
/// One of [`SAMPLE_RATES`].
pub sample_rate: u32,
/// 1 or 2.
pub channels: usize,
/// One of [`bitrates`] for the sample rate, kbit/s.
pub bitrate_kbps: u32,
/// How two channels are coded (ignored for mono).
pub stereo: StereoMode,
}
impl Config {
/// `channels` at `sample_rate`, constant `bitrate_kbps`, joint stereo.
pub fn cbr(sample_rate: u32, channels: usize, bitrate_kbps: u32) -> Config {
Config { sample_rate, channels, bitrate_kbps, stereo: StereoMode::JointStereo }
}
}
/// Whether the sample rate needs MPEG-2's lower sampling frequencies.
fn is_lsf(rate: u32) -> Option<bool> {
if tables::RATES_MPEG1.contains(&rate) {
Some(false)
} else if tables::RATES_LSF.contains(&rate) {
Some(true)
} else {
None
}
}
/// The sample rates the encoder accepts.
pub const SAMPLE_RATES: [u32; 6] = [32_000, 44_100, 48_000, 16_000, 22_050, 24_000];
/// The bit rates (kbit/s) a stream at `sample_rate` can use; empty for an unsupported rate.
pub fn bitrates(sample_rate: u32) -> &'static [u32] {
match is_lsf(sample_rate) {
Some(false) => &tables::BITRATES_MPEG1,
Some(true) => &tables::BITRATES_LSF,
None => &[],
}
}
/// The highest bit rate at `sample_rate` not above `kbps` (the lowest one when `kbps` is below
/// all of them); `None` for an unsupported rate.
pub fn nearest_bitrate(sample_rate: u32, kbps: u32) -> Option<u32> {
let rates = bitrates(sample_rate);
rates.iter().rev().find(|&&r| r <= kbps).or(rates.first()).copied()
}
/// The supported sample rate an encoder should use for audio at `rate`: `rate` itself when
/// supported, else the closest supported one at or above it (48 kHz above 48 kHz).
pub fn nearest_sample_rate(rate: u32) -> u32 {
let mut sorted = SAMPLE_RATES;
sorted.sort_unstable();
sorted.iter().copied().find(|&r| r >= rate).unwrap_or(48_000)
}
/// Energy of the frequency lines of a full-scale sine (amplitude 1) over one granule.
const FULL_SCALE_TONE_ENERGY: f32 = 1.0;
/// Playback level of a full-scale sine assumed by the threshold in quiet, dB SPL.
const FULL_SCALE_SPL: f64 = 96.0;
/// Samples by which a standard decoder's output lags the input: the polyphase analysis and
/// synthesis filters (481) and the MDCT overlap of one granule (576).
pub const DELAY: u32 = 1057;
/// A Layer III encoder. Feed planar `f32` samples (full scale ±1) to [`Encoder::encode`]; it
/// returns whole frames as they are completed. [`Encoder::flush`] pads the end with silence and
/// returns the rest.
///
/// ```
/// use craft_audio::mp3::{Config, DELAY, Encoder};
///
/// let left: Vec<f32> = (0..48_000).map(|i| (i as f32 * 0.05).sin() * 0.5).collect();
/// let right = left.clone();
/// let mut enc = Encoder::new(Config::cbr(48_000, 2, 192))?;
/// let mut mp3 = enc.encode(&[&left, &right]); // whole frames as they complete
/// mp3.extend(enc.flush()); // pads with silence to the last input sample
/// assert_eq!(mp3.len() % 576, 0); // 192 kbit/s at 48 kHz: 576 bytes per frame
/// assert_eq!(DELAY, 1057); // a decoder's output lags the input by this many samples
/// # Ok::<(), craft_audio::mp3::Error>(())
/// ```
pub struct Encoder {
cfg: Config,
lsf: bool,
sr_index: u32,
br_index: u32,
granules: usize,
analysis: Vec<Analysis>,
model: Model,
/// Buffered input per channel, and how much of it is consumed.
input: Vec<Vec<f32>>,
consumed: usize,
/// Frames whose main-data slot is not yet filled: header + side information, slot bytes.
pending: VecDeque<(Vec<u8>, usize)>,
/// Main data from the first pending frame's slot on.
main: Vec<u8>,
/// Slot bytes of the pending frames.
slot_total: usize,
/// Largest `main_data_begin`, bytes.
max_reservoir: usize,
/// Remainder accumulator for the padding slot (44.1 / 22.05 kHz).
pad_acc: u64,
samples_in: u64,
frames: u64,
}
impl Encoder {
/// An encoder for `cfg`, or the setting it cannot carry.
pub fn new(cfg: Config) -> Result<Encoder> {
let lsf = is_lsf(cfg.sample_rate).ok_or(Error::SampleRate(cfg.sample_rate))?;
if !(1..=2).contains(&cfg.channels) {
return Err(Error::Channels(cfg.channels));
}
let rates = if lsf { &tables::RATES_LSF } else { &tables::RATES_MPEG1 };
let sr_index = rates.iter().position(|&r| r == cfg.sample_rate).ok_or(Error::SampleRate(cfg.sample_rate))?;
let br_index = bitrates(cfg.sample_rate)
.iter()
.position(|&b| b == cfg.bitrate_kbps)
.ok_or(Error::Bitrate { kbps: cfg.bitrate_kbps, rate: cfg.sample_rate })?;
let bands = if lsf { tables::SFB_LSF[sr_index] } else { tables::SFB_MPEG1[sr_index] };
let line_hz = cfg.sample_rate as f64 / 2.0 / GRANULE as f64;
let mut ath = [0f32; 22];
for (b, a) in ath.iter_mut().enumerate() {
let (lo, hi) = (usize::from(bands[b]), usize::from(bands[b + 1]));
let db = (lo..hi).map(|i| tables::ath_db((i as f64 + 0.5) * line_hz)).fold(f64::MAX, f64::min);
*a = (f64::from(FULL_SCALE_TONE_ENERGY) * 10f64.powf((db - FULL_SCALE_SPL) / 10.0)) as f32;
}
let cutoff_hz = cutoff_hz(cfg.bitrate_kbps, cfg.channels).min(cfg.sample_rate as f64 * 0.5);
let cutoff = ((cutoff_hz / line_hz).ceil() as usize).min(GRANULE);
let frame_bits = u64::from(cfg.bitrate_kbps) * 1000 * if lsf { 576 } else { 1152 } / u64::from(cfg.sample_rate);
// reservoir and frame must fit the decoder's 7680-bit input buffer
let max_reservoir = (7680u64.saturating_sub(frame_bits) / 8).min(if lsf { 255 } else { 511 }) as usize;
Ok(Encoder {
cfg,
lsf,
sr_index: sr_index as u32,
br_index: br_index as u32 + 1,
granules: if lsf { 1 } else { 2 },
analysis: vec![Analysis::new(); cfg.channels],
model: Model::new(lsf, bands, ath, cutoff),
input: vec![Vec::new(); cfg.channels],
consumed: 0,
pending: VecDeque::new(),
main: Vec::new(),
slot_total: 0,
max_reservoir,
pad_acc: 0,
samples_in: 0,
frames: 0,
})
}
/// The settings.
pub fn config(&self) -> &Config {
&self.cfg
}
/// Samples per channel in a frame: 1152 (MPEG-1) or 576 (MPEG-2 LSF).
pub fn frame_samples(&self) -> usize {
GRANULE * self.granules
}
/// Samples by which a standard decoder's output lags the input.
pub fn delay(&self) -> u32 {
DELAY
}
/// Frames written so far.
pub fn frames(&self) -> u64 {
self.frames
}
/// Encode planar samples (one slice per channel; a missing channel is silence, extra ones
/// are ignored, every channel takes the first channel's length). Returns the completed frames.
pub fn encode(&mut self, planar: &[&[f32]]) -> Vec<u8> {
let n = planar.first().map_or(0, |p| p.len());
for (c, buf) in self.input.iter_mut().enumerate() {
let src = planar.get(c).copied().unwrap_or(&[]);
buf.extend((0..n).map(|i| src.get(i).copied().filter(|v| v.is_finite()).unwrap_or(0.0).clamp(-4.0, 4.0)));
}
self.samples_in = self.samples_in.saturating_add(n as u64);
let mut out = Vec::new();
self.drain(&mut out);
out
}
/// Encode the buffered rest padded with silence, plus enough silence that a decoder outputs
/// every input sample, and return the remaining frames.
pub fn flush(&mut self) -> Vec<u8> {
let fs = self.frame_samples() as u64;
let needed = if self.samples_in == 0 { self.frames } else { self.samples_in.saturating_add(u64::from(DELAY)).div_ceil(fs) };
let have = self.input.first().map_or(0, |b| b.len().saturating_sub(self.consumed));
let want = (needed.saturating_sub(self.frames) as usize).saturating_mul(fs as usize);
if want > have {
for buf in &mut self.input {
buf.resize(self.consumed + want, 0.0);
}
}
let mut out = Vec::new();
self.drain(&mut out);
// fill the last frames' slots and write them out
self.main.resize(self.slot_total.max(self.main.len()), 0);
self.emit(&mut out);
self.input.iter_mut().for_each(Vec::clear);
self.consumed = 0;
out
}
/// Encode every whole frame of buffered input.
fn drain(&mut self, out: &mut Vec<u8>) {
let fs = self.frame_samples();
let mut pcm = vec![[0f32; GRANULE]; self.cfg.channels * self.granules];
while self.input.first().is_some_and(|b| b.len() - self.consumed >= fs) {
for (c, buf) in self.input.iter().enumerate() {
for gr in 0..self.granules {
let start = self.consumed + gr * GRANULE;
if let (Some(dst), Some(src)) = (pcm.get_mut(gr * self.cfg.channels + c), buf.get(start..start + GRANULE)) {
dst.copy_from_slice(src);
}
}
}
self.consumed += fs;
self.encode_frame(&pcm);
self.emit(out);
}
if self.consumed > 0 {
let k = self.consumed;
self.input.iter_mut().for_each(|b| {
b.drain(..k.min(b.len()));
});
self.consumed = 0;
}
}
/// Bytes of the next frame (with its padding slot).
fn next_frame_len(&mut self) -> usize {
let per = if self.lsf { 72_000 } else { 144_000 };
let num = per * u64::from(self.cfg.bitrate_kbps);
let sr = u64::from(self.cfg.sample_rate);
self.pad_acc += num % sr;
let pad = self.pad_acc >= sr;
if pad {
self.pad_acc -= sr;
}
(num / sr) as usize + usize::from(pad)
}
fn side_info_len(&self) -> usize {
match (self.lsf, self.cfg.channels) {
(false, 1) => 17,
(false, _) => 32,
(true, 1) => 9,
(true, _) => 17,
}
}
/// Analyse, quantize and assemble one frame (`pcm[gr * channels + ch]`).
fn encode_frame(&mut self, pcm: &[[f32; GRANULE]]) {
let nch = self.cfg.channels;
let ngr = self.granules;
let mut xr = vec![[0f32; GRANULE]; nch * ngr];
for gr in 0..ngr {
for ch in 0..nch {
let k = gr * nch + ch;
if let (Some(a), Some(p), Some(x)) = (self.analysis.get_mut(ch), pcm.get(k), xr.get_mut(k)) {
a.granule(p, x);
x.iter_mut().skip(self.model.cutoff).for_each(|v| *v = 0.0);
}
}
}
let mut xmin: Vec<[f32; 22]> = xr.iter().map(|x| self.model.thresholds(x)).collect();
let ms = nch == 2 && self.cfg.stereo == StereoMode::JointStereo && use_mid_side(&xr);
if ms {
for gr in 0..ngr {
let (l, r) = xr.split_at_mut(gr * 2 + 1);
if let (Some(l), Some(r)) = (l.last_mut(), r.first_mut()) {
for (a, b) in l.iter_mut().zip(r.iter_mut()) {
let (m, s) = ((*a + *b) * std::f32::consts::FRAC_1_SQRT_2, (*a - *b) * std::f32::consts::FRAC_1_SQRT_2);
*a = m;
*b = s;
}
}
let t: [f32; 22] = std::array::from_fn(|b| xmin[gr * 2][b].min(xmin[gr * 2 + 1][b]));
xmin[gr * 2] = t;
xmin[gr * 2 + 1] = t;
}
}
// the reservoir: free bytes in the pending frames' slots, stuffed down to the maximum
let free = self.slot_total.saturating_sub(self.main.len());
if free > self.max_reservoir {
self.main.resize(self.slot_total - self.max_reservoir, 0);
}
let main_data_begin = free.min(self.max_reservoir);
let frame_len = self.next_frame_len();
let slot = frame_len.saturating_sub(4 + self.side_info_len());
let budget = 8 * (main_data_begin + slot);
let n = nch * ngr;
let mean = 8 * slot / n;
let max_reservoir_bits = 8 * self.max_reservoir;
let mut used = 0usize;
let mut coded: Vec<Granule> = Vec::with_capacity(n);
for (k, (x, t)) in xr.iter().zip(&xmin).enumerate() {
let left = n - k;
let remaining = budget.saturating_sub(used);
let surplus = remaining.saturating_sub(mean * left);
let max_bits = (mean + surplus * 3 / 5 / left).min(remaining).min(4095);
let room = max_reservoir_bits.saturating_sub(surplus);
let min_bits = mean.saturating_sub(room / left);
let g = self.model.encode(x, t, max_bits, min_bits);
used += g.bits();
coded.push(g);
}
let mut md = BitWriter::new();
for g in &coded {
g.write(&mut md, &self.model);
}
let mut main = md.finish();
if main.len() > main_data_begin + slot || coded.iter().any(|g| g.bits() > 4095) {
// never expected: fall back to silence rather than write a broken frame
coded = (0..n).map(|_| self.model.encode(&[0.0; GRANULE], &[f32::MAX; 22], 0, 0)).collect();
main = Vec::new();
}
let head = self.header_and_side_info(frame_len, ms, main_data_begin, &coded);
self.pending.push_back((head, slot));
self.slot_total += slot;
self.main.extend_from_slice(&main);
self.frames += 1;
}
/// Write out the pending frames whose slots are full.
fn emit(&mut self, out: &mut Vec<u8>) {
while let Some((_, slot)) = self.pending.front() {
let slot = *slot;
if self.main.len() < slot {
break;
}
if let Some((head, _)) = self.pending.pop_front() {
out.extend_from_slice(&head);
out.extend(self.main.drain(..slot));
self.slot_total -= slot;
}
}
}
fn header_and_side_info(&self, frame_len: usize, ms: bool, main_data_begin: usize, coded: &[Granule]) -> Vec<u8> {
let nch = self.cfg.channels;
let padding = frame_len > (if self.lsf { 72_000 } else { 144_000 } * self.cfg.bitrate_kbps / self.cfg.sample_rate) as usize;
let mode = match (nch, self.cfg.stereo) {
(1, _) => 3,
(_, StereoMode::Stereo) => 0,
(_, StereoMode::JointStereo) => 1,
};
let mut w = BitWriter::new();
w.write_bits(0x7FF, 11);
w.write_bits(if self.lsf { 2 } else { 3 }, 2); // ID: MPEG-1 = 11, MPEG-2 = 10
w.write_bits(1, 2); // Layer III
w.write_bit(true); // protection_bit: no CRC
w.write_bits(self.br_index, 4);
w.write_bits(self.sr_index, 2);
w.write_bit(padding);
w.write_bit(false); // private_bit
w.write_bits(mode, 2);
w.write_bits(if ms { 2 } else { 0 }, 2); // mode_extension: mid/side, no intensity
w.write_bit(false); // copyright
w.write_bit(true); // original
w.write_bits(0, 2); // emphasis
if self.lsf {
w.write_bits(main_data_begin as u32, 8);
w.write_bits(0, if nch == 1 { 1 } else { 2 });
} else {
w.write_bits(main_data_begin as u32, 9);
w.write_bits(0, if nch == 1 { 5 } else { 3 });
w.write_bits(0, 4 * nch as u32); // scfsi
}
for g in coded {
let c = &g.coding;
w.write_bits(g.bits() as u32, 12);
w.write_bits(c.big_values as u32, 9);
w.write_bits(g.global_gain, 8);
w.write_bits(g.scalefac_compress, if self.lsf { 9 } else { 4 });
w.write_bit(false); // window_switching_flag
for t in c.table_select {
w.write_bits(t as u32, 5);
}
w.write_bits(c.region0_count as u32, 4);
w.write_bits(c.region1_count as u32, 3);
if !self.lsf {
w.write_bit(false); // preflag
}
w.write_bit(g.scalefac_scale);
w.write_bits(c.count1_table as u32, 1);
}
w.finish()
}
}
/// Mid/side for a frame when the side signal is much weaker than the mid signal.
fn use_mid_side(xr: &[[f32; GRANULE]]) -> bool {
let (mut m, mut s) = (0f64, 0f64);
for pair in xr.as_chunks::<2>().0 {
for (l, r) in pair[0].iter().zip(pair[1].iter()) {
let (a, b) = (f64::from(*l), f64::from(*r));
m += (a + b) * (a + b);
s += (a - b) * (a - b);
}
}
s < 0.25 * m
}
/// The low-pass for a bit rate: the band the bits can carry (joint stereo counted as 1.6
/// channels' worth).
fn cutoff_hz(kbps: u32, channels: usize) -> f64 {
let per = f64::from(kbps) / if channels > 1 { 1.6 } else { 1.0 };
const STEPS: [(f64, f64); 11] = [
(140.0, 20_000.0),
(120.0, 19_000.0),
(100.0, 18_000.0),
(80.0, 16_500.0),
(64.0, 15_000.0),
(56.0, 13_500.0),
(48.0, 12_000.0),
(40.0, 10_000.0),
(32.0, 8_000.0),
(24.0, 6_500.0),
(16.0, 5_000.0),
];
STEPS.iter().find(|(k, _)| per >= *k).map_or(3_500.0, |s| s.1)
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
+1
View File
@@ -338,4 +338,5 @@ pub(crate) const LINBITS: [u32; 32] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
pub(crate) const QUAD_A_CODES: [u8; 16] = [1, 5, 4, 5, 6, 5, 4, 4, 7, 3, 6, 0, 7, 2, 3, 1];
pub(crate) const QUAD_A_LENS: [u8; 16] = [1, 4, 4, 5, 4, 6, 5, 6, 4, 5, 5, 6, 5, 6, 6, 6];
/// Count1 table B: four bits, the inverted quadruple.
#[cfg(feature = "mp3-encode")]
pub(crate) const QUAD_B_CODES: [u8; 16] = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
+27 -476
View File
@@ -1,4 +1,19 @@
//! Clean-room MPEG-1 / MPEG-2 Audio Layer III (MP3) encoder, written from ISO/IEC 11172-3:1993
//! MPEG-1 / MPEG-2 Audio Layer III (MP3): a clean-room decoder ([`Decoder`], feature
//! `mp3-decode`) and encoder ([`Encoder`], feature `mp3-encode`), written from ISO/IEC 11172-3
//! and ISO/IEC 13818-3. They share the Huffman code tables and scalefactor bands.
//!
//! # Decoder (feature `mp3-decode`)
//!
//! [`Decoder`] decodes Layer III frames one at a time: MPEG-1, the MPEG-2 lower sampling
//! frequencies and MPEG-2.5 (8-48 kHz), every bit rate (CBR or VBR), mono, stereo, dual channel
//! and joint stereo (mid/side and intensity), long, short and mixed blocks, across the bit
//! reservoir. [`InfoTag`] reads the Xing / Info or VBRI frame at the start of a file and the
//! LAME tag's encoder delay and padding. `craft_audio::decode` (feature `decode-native`) reads
//! `.mp3` files and MP3 in MP4 and Matroska through them, gapless.
//!
//! # Encoder (feature `mp3-encode`)
//!
//! A clean-room encoder, written from ISO/IEC 11172-3:1993
//! (MPEG-1 Audio: §2.4.1 bit stream syntax, §2.4.2 semantics, Annex B tables, Annex C encoding
//! process) and ISO/IEC 13818-3:1998 (the lower sampling frequencies).
//!
@@ -13,490 +28,26 @@
//! - Output: a plain stream of frames, no ID3 or Xing / Info tag, no CRC. The frames written one
//! after another are a playable `.mp3` file.
//!
//! No decoder here: [`mpa`](crate::mpa) decodes Layers I and II, and files with Layer III are read
//! through `symphonia` (feature `decode`).
//!
//! ```
//! use craft_audio::mp3::{Config, DELAY, Encoder};
//!
//! let left: Vec<f32> = (0..48_000).map(|i| (i as f32 * 0.05).sin() * 0.5).collect();
//! let right = left.clone();
//! let mut enc = Encoder::new(Config::cbr(48_000, 2, 192))?;
//! let mut mp3 = enc.encode(&[&left, &right]); // whole frames as they complete
//! mp3.extend(enc.flush()); // pads with silence to the last input sample
//! assert_eq!(mp3.len() % 576, 0); // 192 kbit/s at 48 kHz: 576 bytes per frame
//! assert_eq!(DELAY, 1057); // a decoder's output lags the input by this many samples
//! # Ok::<(), craft_audio::mp3::Error>(())
//! ```
//!
//! From FilmCraft's `filmcraft-mp3enc` (storytold/filmcraft@4bec22b). 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)]
#[cfg(feature = "mp3-decode")]
mod decoder;
#[cfg(feature = "mp3-encode")]
mod encoder;
#[cfg(feature = "mp3-encode")]
mod filterbank;
#[cfg(feature = "mp3-encode")]
mod huffman;
mod huffman_tables;
#[cfg(feature = "mp3-encode")]
mod quantize;
mod tables;
use std::collections::VecDeque;
use craft_media::bitstream::BitWriter;
use filterbank::{Analysis, GRANULE};
use quantize::{Granule, Model};
/// Settings an [`Encoder`] refuses.
#[non_exhaustive]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
/// A sample rate outside [`SAMPLE_RATES`].
#[error("MP3: unsupported sample rate {0} Hz (32, 44.1, 48 kHz, or 16, 22.05, 24 kHz)")]
SampleRate(u32),
/// A bit rate that is not one of [`bitrates`] for the sample rate.
#[error("MP3: unsupported bit rate {kbps} kbit/s at {rate} Hz")]
Bitrate {
/// The bit rate asked for, kbit/s.
kbps: u32,
/// The sample rate, Hz.
rate: u32,
},
/// More than two channels, or none.
#[error("MP3: unsupported channel count {0} (1 or 2)")]
Channels(usize),
}
/// Result with this module's [`Error`].
pub type Result<T> = std::result::Result<T, Error>;
/// How two channels are coded.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StereoMode {
/// Left and right coded independently (`mode` 00).
Stereo,
/// Joint stereo (`mode` 01): each frame chooses left / right or mid / side.
#[default]
JointStereo,
}
/// Encoder settings.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Config {
/// One of [`SAMPLE_RATES`].
pub sample_rate: u32,
/// 1 or 2.
pub channels: usize,
/// One of [`bitrates`] for the sample rate, kbit/s.
pub bitrate_kbps: u32,
/// How two channels are coded (ignored for mono).
pub stereo: StereoMode,
}
impl Config {
/// `channels` at `sample_rate`, constant `bitrate_kbps`, joint stereo.
pub fn cbr(sample_rate: u32, channels: usize, bitrate_kbps: u32) -> Config {
Config { sample_rate, channels, bitrate_kbps, stereo: StereoMode::JointStereo }
}
}
/// Whether the sample rate needs MPEG-2's lower sampling frequencies.
fn is_lsf(rate: u32) -> Option<bool> {
if tables::RATES_MPEG1.contains(&rate) {
Some(false)
} else if tables::RATES_LSF.contains(&rate) {
Some(true)
} else {
None
}
}
/// The sample rates the encoder accepts.
pub const SAMPLE_RATES: [u32; 6] = [32_000, 44_100, 48_000, 16_000, 22_050, 24_000];
/// The bit rates (kbit/s) a stream at `sample_rate` can use; empty for an unsupported rate.
pub fn bitrates(sample_rate: u32) -> &'static [u32] {
match is_lsf(sample_rate) {
Some(false) => &tables::BITRATES_MPEG1,
Some(true) => &tables::BITRATES_LSF,
None => &[],
}
}
/// The highest bit rate at `sample_rate` not above `kbps` (the lowest one when `kbps` is below
/// all of them); `None` for an unsupported rate.
pub fn nearest_bitrate(sample_rate: u32, kbps: u32) -> Option<u32> {
let rates = bitrates(sample_rate);
rates.iter().rev().find(|&&r| r <= kbps).or(rates.first()).copied()
}
/// The supported sample rate an encoder should use for audio at `rate`: `rate` itself when
/// supported, else the closest supported one at or above it (48 kHz above 48 kHz).
pub fn nearest_sample_rate(rate: u32) -> u32 {
let mut sorted = SAMPLE_RATES;
sorted.sort_unstable();
sorted.iter().copied().find(|&r| r >= rate).unwrap_or(48_000)
}
/// Energy of the frequency lines of a full-scale sine (amplitude 1) over one granule.
const FULL_SCALE_TONE_ENERGY: f32 = 1.0;
/// Playback level of a full-scale sine assumed by the threshold in quiet, dB SPL.
const FULL_SCALE_SPL: f64 = 96.0;
/// Samples by which a standard decoder's output lags the input: the polyphase analysis and
/// synthesis filters (481) and the MDCT overlap of one granule (576).
pub const DELAY: u32 = 1057;
/// A Layer III encoder. Feed planar `f32` samples (full scale ±1) to [`Encoder::encode`]; it
/// returns whole frames as they are completed. [`Encoder::flush`] pads the end with silence and
/// returns the rest.
pub struct Encoder {
cfg: Config,
lsf: bool,
sr_index: u32,
br_index: u32,
granules: usize,
analysis: Vec<Analysis>,
model: Model,
/// Buffered input per channel, and how much of it is consumed.
input: Vec<Vec<f32>>,
consumed: usize,
/// Frames whose main-data slot is not yet filled: header + side information, slot bytes.
pending: VecDeque<(Vec<u8>, usize)>,
/// Main data from the first pending frame's slot on.
main: Vec<u8>,
/// Slot bytes of the pending frames.
slot_total: usize,
/// Largest `main_data_begin`, bytes.
max_reservoir: usize,
/// Remainder accumulator for the padding slot (44.1 / 22.05 kHz).
pad_acc: u64,
samples_in: u64,
frames: u64,
}
impl Encoder {
/// An encoder for `cfg`, or the setting it cannot carry.
pub fn new(cfg: Config) -> Result<Encoder> {
let lsf = is_lsf(cfg.sample_rate).ok_or(Error::SampleRate(cfg.sample_rate))?;
if !(1..=2).contains(&cfg.channels) {
return Err(Error::Channels(cfg.channels));
}
let rates = if lsf { &tables::RATES_LSF } else { &tables::RATES_MPEG1 };
let sr_index = rates.iter().position(|&r| r == cfg.sample_rate).ok_or(Error::SampleRate(cfg.sample_rate))?;
let br_index = bitrates(cfg.sample_rate)
.iter()
.position(|&b| b == cfg.bitrate_kbps)
.ok_or(Error::Bitrate { kbps: cfg.bitrate_kbps, rate: cfg.sample_rate })?;
let bands = if lsf { tables::SFB_LSF[sr_index] } else { tables::SFB_MPEG1[sr_index] };
let line_hz = cfg.sample_rate as f64 / 2.0 / GRANULE as f64;
let mut ath = [0f32; 22];
for (b, a) in ath.iter_mut().enumerate() {
let (lo, hi) = (usize::from(bands[b]), usize::from(bands[b + 1]));
let db = (lo..hi).map(|i| tables::ath_db((i as f64 + 0.5) * line_hz)).fold(f64::MAX, f64::min);
*a = (f64::from(FULL_SCALE_TONE_ENERGY) * 10f64.powf((db - FULL_SCALE_SPL) / 10.0)) as f32;
}
let cutoff_hz = cutoff_hz(cfg.bitrate_kbps, cfg.channels).min(cfg.sample_rate as f64 * 0.5);
let cutoff = ((cutoff_hz / line_hz).ceil() as usize).min(GRANULE);
let frame_bits = u64::from(cfg.bitrate_kbps) * 1000 * if lsf { 576 } else { 1152 } / u64::from(cfg.sample_rate);
// reservoir and frame must fit the decoder's 7680-bit input buffer
let max_reservoir = (7680u64.saturating_sub(frame_bits) / 8).min(if lsf { 255 } else { 511 }) as usize;
Ok(Encoder {
cfg,
lsf,
sr_index: sr_index as u32,
br_index: br_index as u32 + 1,
granules: if lsf { 1 } else { 2 },
analysis: vec![Analysis::new(); cfg.channels],
model: Model::new(lsf, bands, ath, cutoff),
input: vec![Vec::new(); cfg.channels],
consumed: 0,
pending: VecDeque::new(),
main: Vec::new(),
slot_total: 0,
max_reservoir,
pad_acc: 0,
samples_in: 0,
frames: 0,
})
}
/// The settings.
pub fn config(&self) -> &Config {
&self.cfg
}
/// Samples per channel in a frame: 1152 (MPEG-1) or 576 (MPEG-2 LSF).
pub fn frame_samples(&self) -> usize {
GRANULE * self.granules
}
/// Samples by which a standard decoder's output lags the input.
pub fn delay(&self) -> u32 {
DELAY
}
/// Frames written so far.
pub fn frames(&self) -> u64 {
self.frames
}
/// Encode planar samples (one slice per channel; a missing channel is silence, extra ones
/// are ignored, every channel takes the first channel's length). Returns the completed frames.
pub fn encode(&mut self, planar: &[&[f32]]) -> Vec<u8> {
let n = planar.first().map_or(0, |p| p.len());
for (c, buf) in self.input.iter_mut().enumerate() {
let src = planar.get(c).copied().unwrap_or(&[]);
buf.extend((0..n).map(|i| src.get(i).copied().filter(|v| v.is_finite()).unwrap_or(0.0).clamp(-4.0, 4.0)));
}
self.samples_in = self.samples_in.saturating_add(n as u64);
let mut out = Vec::new();
self.drain(&mut out);
out
}
/// Encode the buffered rest padded with silence, plus enough silence that a decoder outputs
/// every input sample, and return the remaining frames.
pub fn flush(&mut self) -> Vec<u8> {
let fs = self.frame_samples() as u64;
let needed = if self.samples_in == 0 { self.frames } else { self.samples_in.saturating_add(u64::from(DELAY)).div_ceil(fs) };
let have = self.input.first().map_or(0, |b| b.len().saturating_sub(self.consumed));
let want = (needed.saturating_sub(self.frames) as usize).saturating_mul(fs as usize);
if want > have {
for buf in &mut self.input {
buf.resize(self.consumed + want, 0.0);
}
}
let mut out = Vec::new();
self.drain(&mut out);
// fill the last frames' slots and write them out
self.main.resize(self.slot_total.max(self.main.len()), 0);
self.emit(&mut out);
self.input.iter_mut().for_each(Vec::clear);
self.consumed = 0;
out
}
/// Encode every whole frame of buffered input.
fn drain(&mut self, out: &mut Vec<u8>) {
let fs = self.frame_samples();
let mut pcm = vec![[0f32; GRANULE]; self.cfg.channels * self.granules];
while self.input.first().is_some_and(|b| b.len() - self.consumed >= fs) {
for (c, buf) in self.input.iter().enumerate() {
for gr in 0..self.granules {
let start = self.consumed + gr * GRANULE;
if let (Some(dst), Some(src)) = (pcm.get_mut(gr * self.cfg.channels + c), buf.get(start..start + GRANULE)) {
dst.copy_from_slice(src);
}
}
}
self.consumed += fs;
self.encode_frame(&pcm);
self.emit(out);
}
if self.consumed > 0 {
let k = self.consumed;
self.input.iter_mut().for_each(|b| {
b.drain(..k.min(b.len()));
});
self.consumed = 0;
}
}
/// Bytes of the next frame (with its padding slot).
fn next_frame_len(&mut self) -> usize {
let per = if self.lsf { 72_000 } else { 144_000 };
let num = per * u64::from(self.cfg.bitrate_kbps);
let sr = u64::from(self.cfg.sample_rate);
self.pad_acc += num % sr;
let pad = self.pad_acc >= sr;
if pad {
self.pad_acc -= sr;
}
(num / sr) as usize + usize::from(pad)
}
fn side_info_len(&self) -> usize {
match (self.lsf, self.cfg.channels) {
(false, 1) => 17,
(false, _) => 32,
(true, 1) => 9,
(true, _) => 17,
}
}
/// Analyse, quantize and assemble one frame (`pcm[gr * channels + ch]`).
fn encode_frame(&mut self, pcm: &[[f32; GRANULE]]) {
let nch = self.cfg.channels;
let ngr = self.granules;
let mut xr = vec![[0f32; GRANULE]; nch * ngr];
for gr in 0..ngr {
for ch in 0..nch {
let k = gr * nch + ch;
if let (Some(a), Some(p), Some(x)) = (self.analysis.get_mut(ch), pcm.get(k), xr.get_mut(k)) {
a.granule(p, x);
x.iter_mut().skip(self.model.cutoff).for_each(|v| *v = 0.0);
}
}
}
let mut xmin: Vec<[f32; 22]> = xr.iter().map(|x| self.model.thresholds(x)).collect();
let ms = nch == 2 && self.cfg.stereo == StereoMode::JointStereo && use_mid_side(&xr);
if ms {
for gr in 0..ngr {
let (l, r) = xr.split_at_mut(gr * 2 + 1);
if let (Some(l), Some(r)) = (l.last_mut(), r.first_mut()) {
for (a, b) in l.iter_mut().zip(r.iter_mut()) {
let (m, s) = ((*a + *b) * std::f32::consts::FRAC_1_SQRT_2, (*a - *b) * std::f32::consts::FRAC_1_SQRT_2);
*a = m;
*b = s;
}
}
let t: [f32; 22] = std::array::from_fn(|b| xmin[gr * 2][b].min(xmin[gr * 2 + 1][b]));
xmin[gr * 2] = t;
xmin[gr * 2 + 1] = t;
}
}
// the reservoir: free bytes in the pending frames' slots, stuffed down to the maximum
let free = self.slot_total.saturating_sub(self.main.len());
if free > self.max_reservoir {
self.main.resize(self.slot_total - self.max_reservoir, 0);
}
let main_data_begin = free.min(self.max_reservoir);
let frame_len = self.next_frame_len();
let slot = frame_len.saturating_sub(4 + self.side_info_len());
let budget = 8 * (main_data_begin + slot);
let n = nch * ngr;
let mean = 8 * slot / n;
let max_reservoir_bits = 8 * self.max_reservoir;
let mut used = 0usize;
let mut coded: Vec<Granule> = Vec::with_capacity(n);
for (k, (x, t)) in xr.iter().zip(&xmin).enumerate() {
let left = n - k;
let remaining = budget.saturating_sub(used);
let surplus = remaining.saturating_sub(mean * left);
let max_bits = (mean + surplus * 3 / 5 / left).min(remaining).min(4095);
let room = max_reservoir_bits.saturating_sub(surplus);
let min_bits = mean.saturating_sub(room / left);
let g = self.model.encode(x, t, max_bits, min_bits);
used += g.bits();
coded.push(g);
}
let mut md = BitWriter::new();
for g in &coded {
g.write(&mut md, &self.model);
}
let mut main = md.finish();
if main.len() > main_data_begin + slot || coded.iter().any(|g| g.bits() > 4095) {
// never expected: fall back to silence rather than write a broken frame
coded = (0..n).map(|_| self.model.encode(&[0.0; GRANULE], &[f32::MAX; 22], 0, 0)).collect();
main = Vec::new();
}
let head = self.header_and_side_info(frame_len, ms, main_data_begin, &coded);
self.pending.push_back((head, slot));
self.slot_total += slot;
self.main.extend_from_slice(&main);
self.frames += 1;
}
/// Write out the pending frames whose slots are full.
fn emit(&mut self, out: &mut Vec<u8>) {
while let Some((_, slot)) = self.pending.front() {
let slot = *slot;
if self.main.len() < slot {
break;
}
if let Some((head, _)) = self.pending.pop_front() {
out.extend_from_slice(&head);
out.extend(self.main.drain(..slot));
self.slot_total -= slot;
}
}
}
fn header_and_side_info(&self, frame_len: usize, ms: bool, main_data_begin: usize, coded: &[Granule]) -> Vec<u8> {
let nch = self.cfg.channels;
let padding = frame_len > (if self.lsf { 72_000 } else { 144_000 } * self.cfg.bitrate_kbps / self.cfg.sample_rate) as usize;
let mode = match (nch, self.cfg.stereo) {
(1, _) => 3,
(_, StereoMode::Stereo) => 0,
(_, StereoMode::JointStereo) => 1,
};
let mut w = BitWriter::new();
w.write_bits(0x7FF, 11);
w.write_bits(if self.lsf { 2 } else { 3 }, 2); // ID: MPEG-1 = 11, MPEG-2 = 10
w.write_bits(1, 2); // Layer III
w.write_bit(true); // protection_bit: no CRC
w.write_bits(self.br_index, 4);
w.write_bits(self.sr_index, 2);
w.write_bit(padding);
w.write_bit(false); // private_bit
w.write_bits(mode, 2);
w.write_bits(if ms { 2 } else { 0 }, 2); // mode_extension: mid/side, no intensity
w.write_bit(false); // copyright
w.write_bit(true); // original
w.write_bits(0, 2); // emphasis
if self.lsf {
w.write_bits(main_data_begin as u32, 8);
w.write_bits(0, if nch == 1 { 1 } else { 2 });
} else {
w.write_bits(main_data_begin as u32, 9);
w.write_bits(0, if nch == 1 { 5 } else { 3 });
w.write_bits(0, 4 * nch as u32); // scfsi
}
for g in coded {
let c = &g.coding;
w.write_bits(g.bits() as u32, 12);
w.write_bits(c.big_values as u32, 9);
w.write_bits(g.global_gain, 8);
w.write_bits(g.scalefac_compress, if self.lsf { 9 } else { 4 });
w.write_bit(false); // window_switching_flag
for t in c.table_select {
w.write_bits(t as u32, 5);
}
w.write_bits(c.region0_count as u32, 4);
w.write_bits(c.region1_count as u32, 3);
if !self.lsf {
w.write_bit(false); // preflag
}
w.write_bit(g.scalefac_scale);
w.write_bits(c.count1_table as u32, 1);
}
w.finish()
}
}
/// Mid/side for a frame when the side signal is much weaker than the mid signal.
fn use_mid_side(xr: &[[f32; GRANULE]]) -> bool {
let (mut m, mut s) = (0f64, 0f64);
for pair in xr.as_chunks::<2>().0 {
for (l, r) in pair[0].iter().zip(pair[1].iter()) {
let (a, b) = (f64::from(*l), f64::from(*r));
m += (a + b) * (a + b);
s += (a - b) * (a - b);
}
}
s < 0.25 * m
}
/// The low-pass for a bit rate: the band the bits can carry (joint stereo counted as 1.6
/// channels' worth).
fn cutoff_hz(kbps: u32, channels: usize) -> f64 {
let per = f64::from(kbps) / if channels > 1 { 1.6 } else { 1.0 };
const STEPS: [(f64, f64); 11] = [
(140.0, 20_000.0),
(120.0, 19_000.0),
(100.0, 18_000.0),
(80.0, 16_500.0),
(64.0, 15_000.0),
(56.0, 13_500.0),
(48.0, 12_000.0),
(40.0, 10_000.0),
(32.0, 8_000.0),
(24.0, 6_500.0),
(16.0, 5_000.0),
];
STEPS.iter().find(|(k, _)| per >= *k).map_or(3_500.0, |s| s.1)
}
#[cfg(test)]
mod tests;
#[cfg(feature = "mp3-decode")]
pub use decoder::{Decoder, InfoTag};
#[cfg(feature = "mp3-encode")]
pub use encoder::{Config, DELAY, Encoder, Error, Result, SAMPLE_RATES, StereoMode, bitrates, nearest_bitrate, nearest_sample_rate};
+10
View File
@@ -4,12 +4,16 @@ use std::sync::OnceLock;
/// Layer III bit rates in kbit/s by `bitrate_index` 1-14 (§2.4.2.3); index 0 (free format) and
/// 15 (forbidden) are never written.
#[cfg(feature = "mp3-encode")]
pub(crate) const BITRATES_MPEG1: [u32; 14] = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320];
/// Layer III bit rates of the lower sampling frequencies (ISO/IEC 13818-3 §2.4.2.3).
#[cfg(feature = "mp3-encode")]
pub(crate) const BITRATES_LSF: [u32; 14] = [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160];
/// Sampling frequencies by `sampling_frequency` (0-2) of MPEG-1 and of MPEG-2 LSF.
#[cfg(feature = "mp3-encode")]
pub(crate) const RATES_MPEG1: [u32; 3] = [44_100, 48_000, 32_000];
#[cfg(feature = "mp3-encode")]
pub(crate) const RATES_LSF: [u32; 3] = [22_050, 24_000, 16_000];
/// Long-block scalefactor band boundaries (Table 3-B.8 of 11172-3; Table B.2 of 13818-3), by
@@ -31,11 +35,13 @@ pub(crate) const SLEN: [(u32, u32); 16] =
/// Scalefactor bands per partition for long blocks without intensity stereo when MPEG-2
/// `scalefac_compress` < 400 (13818-3 §2.4.3.2, `nr_of_sfb_block[0][0]`).
#[cfg(feature = "mp3-encode")]
pub(crate) const LSF_PARTITIONS: [usize; 4] = [6, 5, 5, 5];
/// 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 analysis window of Table 3-C.1 is the
/// same prototype divided by 32: `C[i] = D[i] / 32`.
#[cfg(feature = "mp3-encode")]
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, //
@@ -59,6 +65,7 @@ const WINDOW_HALF: [i32; 257] = [
/// 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.
#[cfg(feature = "mp3-encode")]
pub(crate) fn synthesis_window() -> &'static [f32; 512] {
static W: OnceLock<[f32; 512]> = OnceLock::new();
W.get_or_init(|| {
@@ -72,6 +79,7 @@ pub(crate) fn synthesis_window() -> &'static [f32; 512] {
}
/// The analysis window `C[i]` (Table 3-C.1): `D[i] / 32`.
#[cfg(feature = "mp3-encode")]
pub(crate) fn analysis_window() -> &'static [f32; 512] {
static C: OnceLock<[f32; 512]> = OnceLock::new();
C.get_or_init(|| {
@@ -83,6 +91,7 @@ pub(crate) fn analysis_window() -> &'static [f32; 512] {
/// The analysis matrixing coefficients `M[i][k] = cos((2i + 1)(k - 16)π / 64)` (Annex C.1.3),
/// `i` in `0..32`, `k` in `0..64`.
#[cfg(feature = "mp3-encode")]
pub(crate) fn analysis_matrix() -> &'static [[f32; 64]; 32] {
static M: OnceLock<[[f32; 64]; 32]> = OnceLock::new();
M.get_or_init(|| {
@@ -119,6 +128,7 @@ pub(crate) fn pow43() -> &'static [f32] {
/// The absolute threshold of hearing in dB SPL at `f` Hz (Terhardt's approximation, as in the
/// psychoacoustic models of 11172-3 Annex D).
#[cfg(feature = "mp3-encode")]
pub(crate) fn ath_db(f: f64) -> f64 {
let k = (f.max(20.0)) / 1000.0;
3.64 * k.powf(-0.8) - 6.5 * (-0.6 * (k - 3.3) * (k - 3.3)).exp() + 0.001 * k.powi(4)
+3 -2
View File
@@ -1,6 +1,7 @@
use super::filterbank::Analysis;
use super::tables::{alias_coefficients, synthesis_window};
use super::*;
use crate::mp3::filterbank::Analysis;
use crate::mp3::tables::{alias_coefficients, synthesis_window};
use crate::mp3::{huffman, huffman_tables};
/// The decoder side of the hybrid filterbank (§2.4.3.4.10: alias butterflies, IMDCT with the
/// normal window and overlap-add, frequency inversion, polyphase synthesis), for checking the
+2 -2
View File
@@ -19,7 +19,7 @@
// Index loops follow the specification's pseudo-code (FilmCraft allows this lint workspace-wide).
#![allow(clippy::needless_range_loop)]
mod tables;
pub(crate) mod tables;
use craft_media::bitstream::BitReader;
use tables::AllocTable;
@@ -418,7 +418,7 @@ fn layer2(r: &mut BitReader, h: &Header, out: &mut Subbands) -> Result<()> {
/// 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], pos: &mut usize, n: &[[f32; 36]; 32], d: &[f32; 512], s: &[f32; 32], out: &mut [f32; 32]) {
pub(crate) fn synthesize(v: &mut [f32; 1024], pos: &mut usize, n: &[[f32; 36]; 32], d: &[f32; 512], s: &[f32; 32], out: &mut [f32; 32]) {
// shift V by 64: move the ring's start back instead of copying 960 values
*pos = (*pos + 1024 - 64) % 1024;
let p = *pos;
+4 -4
View File
@@ -95,7 +95,7 @@ fn detect_by_magic(b: &[u8], depth: u32) -> Option<Container> {
/// The file extensions of the formats [`decode`] reads, lower case and without the dot: what a
/// file picker or a browser filters by. Each is a hint [`detect_container`] knows; a file's
/// contents still decide what it is. Without the `symphonia` feature the formats only symphonia
/// decodes (FLAC, MP3, CAF) are left out.
/// decodes (FLAC, CAF) are left out.
#[cfg(feature = "symphonia")]
pub const DECODE_EXTENSIONS: &[&str] = &[
"wav", "wave", "bwf", "rf64", "aif", "aiff", "aifc", "flac", "mp3", "mp2", "mp1", "mpa", "ogg", "oga", "opus", "m4a", "m4b", "mp4", "mov", "aac",
@@ -104,11 +104,11 @@ pub const DECODE_EXTENSIONS: &[&str] = &[
/// The file extensions of the formats [`decode`] reads, lower case and without the dot: what a
/// file picker or a browser filters by. Each is a hint [`detect_container`] knows; a file's
/// contents still decide what it is. Without the `symphonia` feature the formats only symphonia
/// decodes (FLAC, MP3, CAF) are left out.
/// decodes (FLAC, CAF) are left out.
#[cfg(not(feature = "symphonia"))]
pub const DECODE_EXTENSIONS: &[&str] = &[
"wav", "wave", "bwf", "rf64", "aif", "aiff", "aifc", "mp2", "mp1", "mpa", "ogg", "oga", "opus", "m4a", "m4b", "mp4", "mov", "aac", "adts", "ac3",
"eac3", "ec3", "mkv", "mka", "webm",
"wav", "wave", "bwf", "rf64", "aif", "aiff", "aifc", "mp3", "mp2", "mp1", "mpa", "ogg", "oga", "opus", "m4a", "m4b", "mp4", "mov", "aac", "adts",
"ac3", "eac3", "ec3", "mkv", "mka", "webm",
];
fn detect_by_ext(ext: &str) -> Option<Container> {
+40 -13
View File
@@ -1,12 +1,13 @@
//! The native decode path: craft-media's demuxers (ISO BMFF / QuickTime, Matroska / WebM), raw
//! 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.
//! [`Native::open`] answers `None` for a stream with no native codec (FLAC, 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
//! `CodecDelay` and per-block `DiscardPadding`, and Ogg Opus pre-skip and end trimming (RFC 7845
//! §4) are applied, so a file decodes to exactly its presentation length.
//! §4) are applied, and so are an MP3's encoder delay and padding from its LAME tag, so a file
//! decodes to exactly its presentation length.
use super::aiff::{self, AiffParse};
use super::id3v2_len;
@@ -716,15 +717,41 @@ 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.
/// The decoder's own delay that a LAME tag's encoder delay leaves out: decoded audio lags the
/// encoder's input by the delay plus these 528 + 1 samples.
#[cfg(feature = "mp3-decode")]
const MP3_DECODER_DELAY: u64 = 529;
/// A raw MPEG audio stream: layer I or II, and layer III with `mp3-decode`. A Xing / Info or
/// VBRI frame at the start carries no audio and is not a packet; its LAME tag's encoder delay
/// and padding trim the decoded audio to what was encoded.
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 layer_ok = |layer: u8| layer == 1 || layer == 2 || (cfg!(feature = "mp3-decode") && layer == 3);
#[cfg_attr(not(feature = "mp3-decode"), allow(unused_mut))]
let mut frames =
match raw_frames(src, at, |rest| mpa::parse_header(rest).ok().filter(|h| layer_ok(h.layer)).map(|h| (h.frame_bytes, h.samples() as u32))) {
Ok(frames) => frames?,
Err(e) => return Some(Err(e)),
};
#[cfg_attr(not(feature = "mp3-decode"), allow(unused_mut))]
let mut gapless: Option<(u32, u32)> = None;
#[cfg(feature = "mp3-decode")]
if let Some(&(p, n, _)) = frames.first() {
let first = match src.read(p, n as usize) {
Ok(first) => first,
Err(e) => return Some(Err(e)),
};
if let Some(tag) = crate::mp3::InfoTag::parse(&first) {
frames.remove(0);
gapless = tag.delay_padding;
}
}
let total: u64 = frames.iter().map(|&(_, _, f)| u64::from(f)).sum();
let (skip, limit) = match gapless {
#[cfg(feature = "mp3-decode")]
Some((delay, padding)) => (u64::from(delay) + MP3_DECODER_DELAY, Some(total.saturating_sub(u64::from(delay) + u64::from(padding)))),
_ => (0, None),
};
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)),
@@ -734,9 +761,9 @@ fn open_mpa<'a>(src: &Src<'a>, at: u64) -> Option<Result<Native<'a>>> {
Packets::Raw { frames, src: src.clone(), next: 0 },
&CodecParams::new(CodecId::Mp2),
&lookahead,
0,
None,
Some(total),
skip,
limit,
Some(limit.unwrap_or(total)),
)
.map(Ok)
}
+211 -31
View File
@@ -8,6 +8,8 @@ use crate::aac;
use crate::ac3;
#[cfg(feature = "alac-decode")]
use crate::alac;
#[cfg(feature = "mp3-decode")]
use crate::mp3;
#[cfg(feature = "mpa-decode")]
use crate::mpa;
#[cfg(feature = "opus-decode")]
@@ -43,8 +45,20 @@ enum Inner {
dec: Box<opus::Decoder>,
order: Option<&'static [usize]>,
},
/// The decoder, and whether a packet has held several frames.
#[cfg(feature = "mpa-decode")]
Mpa(Box<mpa::Decoder>),
Mpa {
dec: Box<mpa::Decoder>,
several: bool,
},
/// The Layer III decoder, the samples per channel of a frame (1152, or 576 at the lower
/// sampling frequencies), and whether a packet has held several frames.
#[cfg(feature = "mp3-decode")]
Mp3 {
dec: Box<mp3::Decoder>,
samples: usize,
several: bool,
},
Pcm {
kind: PcmKind,
channels: usize,
@@ -61,7 +75,7 @@ pub(crate) type PacketFrames = fn(&[u8], u64) -> Option<u64>;
/// 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), ALAC, Opus, AC-3/E-AC-3,
/// MP1/MP2 and PCM. Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's:
/// MP1/MP2, MP3 and PCM. Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's:
/// every decoded sample is returned.
///
/// ```no_run
@@ -116,7 +130,7 @@ impl TrackDecoder {
///
/// ALAC needs its magic cookie in `extradata`.
///
/// Codecs without a native packet decoder yet (MP3, FLAC, Vorbis) are
/// Codecs without a native packet decoder yet (FLAC, Vorbis) are
/// [`AudioError::Unsupported`]; `craft_audio::decode` decodes them through symphonia when the
/// `symphonia` feature is on.
pub fn new(params: &CodecParams, lookahead: &[&[u8]]) -> Result<TrackDecoder> {
@@ -128,6 +142,10 @@ impl TrackDecoder {
// Opus needs none.
#[cfg_attr(not(any(feature = "ac3-decode", feature = "mpa-decode")), allow(unused_variables))]
let first = lookahead.first().copied().unwrap_or_default();
// MP4 and Matroska packets may hold several MPEG audio frames: then they are not all
// one frame long.
#[cfg(feature = "mpa-decode")]
let several = lookahead.iter().any(|p| mpa_packet_frames(p).0 > 1);
let (inner, codec, sample_rate, layout, sample_format) = match params.codec {
#[cfg(feature = "aac-decode")]
CodecId::Aac | CodecId::HeAac => {
@@ -220,15 +238,32 @@ impl TrackDecoder {
}
#[cfg(feature = "mpa-decode")]
CodecId::Mp1 | CodecId::Mp2 | CodecId::Mp3 => match mpa::parse_header(first) {
#[cfg(feature = "mp3-decode")]
Ok(h) if h.layer == 3 => {
let mut dec = Box::<mp3::Decoder>::default();
dec.prime(h);
(Inner::Mp3 { dec, samples: h.samples(), several }, CodecId::Mp3, h.sample_rate, mpa_layout(&h), SampleFormat::Compressed)
}
#[cfg(not(feature = "mp3-decode"))]
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)
(Inner::Mpa { dec: Box::default(), several }, codec, h.sample_rate, mpa_layout(&h), SampleFormat::Compressed)
}
// Without a first frame, a container that names layer III is left to symphonia.
// Without a first frame, the container's rate and channels stand in until the
// first frame is decoded.
#[cfg(feature = "mp3-decode")]
Err(_) if params.codec == CodecId::Mp3 => (
Inner::Mp3 { dec: Box::default(), samples: if rate >= 32_000 { 1152 } else { 576 }, several },
CodecId::Mp3,
rate,
ChannelLayout::default_for(crate::layout::count(channels)),
SampleFormat::Compressed,
),
#[cfg(not(feature = "mp3-decode"))]
Err(_) if params.codec == CodecId::Mp3 => return Err(unsupported(CodecId::Mp3)),
Err(_) => (
Inner::Mpa(Box::default()),
Inner::Mpa { dec: Box::default(), several },
params.codec,
rate,
ChannelLayout::default_for(crate::layout::count(channels)),
@@ -281,7 +316,7 @@ impl TrackDecoder {
/// its timing.
pub fn decode(&mut self, packet: &[u8]) -> Result<AudioBuffer> {
// A packet decoded only in part says nothing of how long packets are.
#[cfg_attr(not(feature = "ac3-decode"), allow(unused_mut))]
#[cfg_attr(not(any(feature = "ac3-decode", feature = "mpa-decode")), allow(unused_mut))]
let mut partial = false;
let channels = match &mut self.inner {
#[cfg(feature = "aac-decode")]
@@ -316,21 +351,27 @@ impl TrackDecoder {
reorder(floats, *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);
Inner::Mpa { dec, several } => {
let (out, first, count, cut) = mpa_frames(packet, |f| dec.decode(f))?;
*several |= count > 1;
partial = cut;
if let Some(h) = first.filter(|_| self.last_frames == 0) {
self.sample_rate = h.sample_rate;
self.layout = mpa_layout(&h);
}
out
}
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { dec, samples, several } => {
let (out, first, count, cut) = mpa_frames(packet, |f| dec.decode(f))?;
*several |= count > 1;
partial = cut;
if let Some(h) = first {
*samples = h.samples();
if self.last_frames == 0 {
self.sample_rate = f.header.sample_rate;
self.layout = mpa_layout(&f.header);
self.sample_rate = h.sample_rate;
self.layout = mpa_layout(&h);
}
append(&mut out, f.channels)?;
}
out
}
@@ -418,7 +459,19 @@ 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()),
// a packet lost before any has decoded is a frame
Inner::Mpa { .. } => Some(match mpa_packet_frames(data) {
(0, _) if self.last_frames == 0 && self.codec == CodecId::Mp1 => 384,
(0, _) if self.last_frames == 0 => 1152,
(_, n) => n,
}),
// a packet lost whole is as long as the last one, a frame at the start
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { samples, .. } => Some(match mpa_packet_frames(data) {
(0, _) if self.last_frames > 0 => self.last_frames,
(0, _) => *samples,
(_, n) => n,
}),
Inner::Pcm { kind, channels } => Some(data.len() / (kind.bytes() * *channels).max(1)),
};
let frames = frames.filter(|&n| n > 0).unwrap_or(self.last_frames);
@@ -428,9 +481,10 @@ 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) and MP1/MP2 (one frame per packet); `None` for Opus, PCM and ALAC
/// (whose last packet is shorter; the container's timing says), and for AC-3 / E-AC-3 whose
/// packets read so far differ in length.
/// syncframes, else 1536) and MP1/MP2/MP3 (one frame per packet; 576 for MP3 at the lower
/// sampling frequencies); `None` for Opus, PCM and ALAC (whose last packet is shorter; the
/// container's timing says), for AC-3 / E-AC-3 whose packets read so far differ in length,
/// and for MPEG audio once a packet (in the lookahead or decoded) has held several frames.
pub fn frames_per_packet(&self) -> Option<usize> {
match &self.inner {
#[cfg(feature = "aac-decode")]
@@ -447,7 +501,13 @@ 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::Mpa { several: true, .. } => None,
#[cfg(feature = "mpa-decode")]
Inner::Mpa { .. } => Some(if self.codec == CodecId::Mp1 { 384 } else { 1152 }),
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { several: true, .. } => None,
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { samples, .. } => Some(*samples),
Inner::Pcm { .. } => None,
}
}
@@ -476,8 +536,9 @@ impl TrackDecoder {
}
/// 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 or MPEG audio frame
/// (their filter banks overlap one frame), none for ALAC (its packets decode independently)
/// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 or MP1/MP2 frame
/// (their filter banks overlap one frame), for MP3 the frames its bit reservoir can reach back
/// over and two more, none for ALAC (its packets decode independently)
/// and 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 {
@@ -493,7 +554,10 @@ impl TrackDecoder {
#[cfg(feature = "opus-decode")]
Inner::Opus { .. } => 3840,
#[cfg(feature = "mpa-decode")]
Inner::Mpa(_) => self.last_frames.max(1152),
Inner::Mpa { .. } => self.last_frames.max(1152),
// the bit reservoir reaches up to 511 bytes back: as many frames as that takes
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { dec, samples, .. } => dec.preroll().unwrap_or(MP3_PREROLL_FRAMES * *samples),
Inner::Pcm { .. } => 0,
}
}
@@ -519,7 +583,9 @@ impl TrackDecoder {
#[cfg(feature = "opus-decode")]
Inner::Opus { dec, .. } => dec.reset(),
#[cfg(feature = "mpa-decode")]
Inner::Mpa(d) => d.reset(),
Inner::Mpa { dec, .. } => dec.reset(),
#[cfg(feature = "mp3-decode")]
Inner::Mp3 { dec, .. } => dec.reset(),
Inner::Pcm { .. } => {}
}
}
@@ -532,12 +598,13 @@ fn unsupported(codec: CodecId) -> AudioError {
CodecId::Ac3 | CodecId::Eac3 => Some("ac3-decode"),
CodecId::Opus => Some("opus-decode"),
CodecId::Mp1 | CodecId::Mp2 => Some("mpa-decode"),
CodecId::Mp3 => Some("mp3-decode"),
_ => None,
};
match feature {
// A codec this crate decodes, in a build without its feature.
Some(f) => AudioError::Unsupported(format!("decoding {} needs the `{f}` feature", codec.name())),
None if cfg!(feature = "symphonia") && matches!(codec, CodecId::Mp3 | CodecId::Flac | CodecId::Vorbis) => {
None if cfg!(feature = "symphonia") && matches!(codec, CodecId::Flac | CodecId::Vorbis) => {
AudioError::Unsupported(format!("no native {} packet decoder yet (craft_audio::decode reads it through symphonia)", codec.name()))
}
None => AudioError::Unsupported(format!("no native {} packet decoder yet", codec.name())),
@@ -554,6 +621,54 @@ fn reorder(mut channels: Vec<Vec<f32>>, order: Option<&[usize]>) -> Vec<Vec<f32>
}
}
/// Frames of an MP3 stream decoded before a seek target when neither a lookahead nor a decoded
/// frame has said what the stream is: the most any stream needs. The smallest frames (8 kbit/s at
/// 24 kHz, stereo, with CRC) hold 1 byte of main data, so 255 bytes of reservoir span 255 of them,
/// and two more settle the filter banks.
#[cfg(feature = "mp3-decode")]
const MP3_PREROLL_FRAMES: usize = 257;
/// The MPEG audio frames of a packet (an MP4 or Matroska packet may hold several), decoded by
/// `decode`, the first frame's header, how many frames decoded, and whether a frame after the
/// first failed, which ends the packet (then it decoded only in part).
#[cfg(feature = "mpa-decode")]
#[allow(clippy::type_complexity)]
fn mpa_frames(packet: &[u8], mut decode: impl FnMut(&[u8]) -> mpa::Result<mpa::Frame>) -> Result<(Vec<Vec<f32>>, Option<mpa::Header>, usize, bool)> {
let mut out: Vec<Vec<f32>> = Vec::new();
let mut first = None;
let mut count = 0;
let mut p = 0;
while let Some(rest) = packet.get(p..).filter(|r| !r.is_empty()) {
let f = match decode(rest) {
Ok(f) => f,
Err(e) if out.is_empty() => return Err(e.into()),
Err(_) => return Ok((out, first, count, true)),
};
p += f.header.frame_bytes.max(1);
first.get_or_insert(f.header);
count += 1;
append(&mut out, f.channels)?;
}
Ok((out, first, count, false))
}
/// The MPEG audio frames a packet holds by their headers, and the samples per channel they
/// decode to: frames are counted while their headers parse, the last one even when the packet
/// ends inside it.
#[cfg(feature = "mpa-decode")]
fn mpa_packet_frames(packet: &[u8]) -> (usize, usize) {
let (mut frames, mut samples, mut p) = (0, 0, 0);
while let Some(h) = packet.get(p..).and_then(|rest| mpa::parse_header(rest).ok()) {
frames += 1;
samples += h.samples();
if h.frame_bytes == 0 {
break;
}
p += h.frame_bytes;
}
(frames, samples)
}
/// 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", feature = "mpa-decode"))]
@@ -781,6 +896,69 @@ mod tests {
use super::*;
use crate::PcmFormat;
/// An MP3 frame of silence: MPEG-1 Layer III, 128 kbit/s, 44.1 kHz, stereo, 417 bytes.
#[cfg(feature = "mp3-decode")]
fn mp3_frame() -> Vec<u8> {
let mut f = vec![0u8; 417];
f[..4].copy_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
f
}
/// An MP2 frame of silence: MPEG-1 Layer II, 192 kbit/s, 48 kHz, stereo, 576 bytes.
#[cfg(feature = "mpa-decode")]
fn mp2_frame() -> Vec<u8> {
let mut f = vec![0u8; 576];
f[..4].copy_from_slice(&[0xFF, 0xFD, 0xA4, 0x00]);
f
}
/// An MP4 or Matroska packet may hold several MPEG audio frames: then packets are not one
/// frame long (an MP4 track's length is not capped at a frame per packet), and a lost packet
/// is as long as the frames it held.
#[cfg(feature = "mp3-decode")]
#[test]
fn mp3_packets_of_several_frames_have_their_length() {
let one = mp3_frame();
let two = [one.clone(), one.clone()].concat();
let params = CodecParams::new(CodecId::Mp3);
let mut dec = TrackDecoder::new(&params, &[&one]).unwrap();
assert_eq!(dec.frames_per_packet(), Some(1152));
assert_eq!(dec.conceal(None).unwrap().frames(), 1152, "a packet lost before any is a frame");
assert_eq!(dec.decode(&two).unwrap().frames(), 2304);
assert_eq!(dec.frames_per_packet(), None);
assert_eq!(dec.conceal(None).unwrap().frames(), 2304, "a packet lost whole is as long as the last");
assert_eq!(dec.conceal(Some(&[two.clone(), one.clone()].concat())).unwrap().frames(), 3456);
// several frames in the lookahead say so before anything is decoded
assert_eq!(TrackDecoder::new(&params, &[&one, &two]).unwrap().frames_per_packet(), None);
// a packet decoded only in part (its second frame is cut short) keeps the length of the
// last whole packet for the next lost one
let cut = [one.clone(), one[..100].to_vec()].concat();
assert_eq!(dec.decode(&cut).unwrap().frames(), 1152);
assert_eq!(dec.conceal(None).unwrap().frames(), 2304);
}
#[cfg(feature = "mpa-decode")]
#[test]
fn mp2_packets_of_several_frames_have_their_length() {
let one = mp2_frame();
let two = [one.clone(), one.clone()].concat();
let params = CodecParams::new(CodecId::Mp2);
let mut dec = TrackDecoder::new(&params, &[&one]).unwrap();
assert_eq!(dec.frames_per_packet(), Some(1152));
assert_eq!(dec.decode(&two).unwrap().frames(), 2304);
assert_eq!(dec.frames_per_packet(), None);
assert_eq!(dec.conceal(Some(&two)).unwrap().frames(), 2304);
assert_eq!(TrackDecoder::new(&params, &[&two]).unwrap().frames_per_packet(), None);
// a packet lost before any has decoded is a frame: 1152 samples, 384 for Layer I
let stereo = |codec| CodecParams::new(codec).with_sample_rate(48_000).with_channels(2);
assert_eq!(TrackDecoder::new(&stereo(CodecId::Mp2), &[]).unwrap().conceal(None).unwrap().frames(), 1152);
assert_eq!(TrackDecoder::new(&stereo(CodecId::Mp1), &[]).unwrap().conceal(None).unwrap().frames(), 384);
let mut dec = TrackDecoder::new(&params, &[&one]).unwrap();
dec.decode(&one).unwrap();
dec.reset();
assert_eq!(dec.conceal(None).unwrap().frames(), 1152, "after a seek");
}
#[test]
fn pcm_tracks_decode_in_wav_order() {
let params = CodecParams::new(CodecId::Pcm).with_sample_rate(48_000).with_channels(2).with_pcm(PcmFormat::int(16, true, false));
@@ -1145,7 +1323,7 @@ mod tests {
#[test]
fn codecs_without_a_packet_decoder_are_unsupported() {
for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::Mp3, CodecId::H264] {
for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::H264] {
assert!(matches!(TrackDecoder::new(&CodecParams::new(codec), &[]), Err(AudioError::Unsupported(_))), "{codec:?}");
}
// A codec whose feature is off names the feature.
@@ -1155,6 +1333,8 @@ mod tests {
assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Alac), &[]), Err(AudioError::Unsupported(m)) if m.contains("alac-decode")));
#[cfg(not(feature = "opus-decode"))]
assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Opus), &[]), Err(AudioError::Unsupported(m)) if m.contains("opus-decode")));
#[cfg(not(feature = "mp3-decode"))]
assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Mp3), &[]), Err(AudioError::Unsupported(m)) if m.contains("mp3-decode")));
// PCM needs its sample format.
assert!(TrackDecoder::new(&CodecParams::new(CodecId::Pcm).with_channels(2).with_sample_rate(48_000), &[]).is_err());
}
+19 -16
View File
@@ -424,23 +424,26 @@ fn a_valid_mp3_with_incidental_container_magic_still_probes() {
assert_eq!((info.codec, info.sample_rate, info.channels), (CodecId::Mp3, 44_100, 2));
}
/// Symphonia 0.6's CAF/FLAC/MP4 `Scoreable` implementations are unconditional
/// (`Supported(255)`), so a container magic before the first audio frame *is* the reader the probe
/// selects; there is no API to ask the probe to skip it. The preflight must therefore keep
/// refusing an oversized CAF cookie instead of ignoring the marker and playing the MPEG frames
/// behind it (which would also let a real malformed container bypass the size guard).
/// MP3 frames are read natively (as MP1 and MP2 are), so a container marker in junk before the
/// first frame does not hide the frames: the stream plays, and the bytes before it never reach
/// symphonia, whose CAF, FLAC and MP4 probes would claim the stream at the marker. A file that
/// starts with the container is read as that container whatever the hint, so its size checks
/// still refuse an oversized CAF cookie.
#[test]
fn a_container_marker_before_the_first_audio_frame_is_the_probed_candidate() {
fn a_container_marker_before_the_first_audio_frame_does_not_hide_the_frames() {
let mut frame = vec![0u8; 417];
frame[..4].copy_from_slice(&[0xFF, 0xFB, 0x90, 0x00]); // MPEG-1 Layer III, 128 kbps, 44.1 kHz, stereo
let mp3: Vec<u8> = (0..8).flat_map(|_| frame.clone()).collect();
let malformed = caf(&[caf_desc(b"aac ", 0, 1024, 2, 0), (b"kuki", 1i64 << 50, vec![0; 8])]);
for f in [[vec![0u8], malformed.clone(), mp3.clone()].concat(), [vec![0u8; 32], malformed.clone(), mp3.clone()].concat()] {
// The hint still names the raw stream, but the probe sees the CAF marker first.
assert_eq!(detect_container(&f, Some("mp3")), Some(Container::MpegAudio));
assert!(matches!(probe(&f, Some("mp3")), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
assert!(matches!(decode(&f, Some("mp3")), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
let info = probe(&f, Some("mp3")).expect("MP3 frames behind a container marker");
assert_eq!((info.codec, info.sample_rate, info.channels, info.frames), (CodecId::Mp3, 44_100, 2, 8 * 1152));
assert_eq!(decode(&f, Some("mp3")).expect("decode them").1.frames(), 8 * 1152);
}
let f = [malformed, mp3.clone()].concat();
assert!(matches!(probe(&f, Some("mp3")), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
assert!(matches!(decode(&f, Some("mp3")), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
// Plain leading junk without format magic is tolerated, as before.
let f = [vec![0u8; 32], mp3].concat();
assert_eq!(detect_container(&f, Some("mp3")), Some(Container::MpegAudio));
@@ -874,10 +877,10 @@ fn damaged_mp4_and_matroska_are_malformed() {
}
}
/// MP3 in MP4 is decoded by symphonia (PCM, AAC, ALAC, Opus and AC-3 are native), so it reaches
/// the checks in `read/mp4.rs`.
fn mp3_mp4(frames: u32, swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8> {
mp4(frames, &sample_entry(b".mp3", 0, &[], &[]), swap)
/// FLAC in MP4 is decoded by symphonia (PCM, AAC, ALAC, Opus, AC-3, MPEG audio are native), so
/// it reaches the checks in `read/mp4.rs`.
fn symphonia_mp4(frames: u32, swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8> {
mp4(frames, &sample_entry(b"fLaC", 0, &[], &[]), swap)
}
/// An `stts` atom of `n` one-sample runs.
@@ -890,11 +893,11 @@ fn one_sample_runs(n: u32) -> Vec<u8> {
#[test]
fn an_audio_timing_table_with_a_run_per_sample_is_bounded() {
let frames = 200_000;
let hostile = mp3_mp4(frames, |k, a| if k == b"stts" { one_sample_runs(frames) } else { a });
let hostile = symphonia_mp4(frames, |k, a| if k == b"stts" { one_sample_runs(frames) } else { a });
let r = decode(&hostile, None);
assert!(matches!(&r, Err(AudioError::Malformed(m)) if m.contains("timing table")), "{:?}", r.map(|x| x.0));
// a table of many runs over few packets is cheap to scan, so it is not refused for its size
let few = mp3_mp4(100, |k, a| if k == b"stts" { one_sample_runs(100_000) } else { a });
let few = symphonia_mp4(100, |k, a| if k == b"stts" { one_sample_runs(100_000) } else { a });
assert!(!matches!(decode(&few, None), Err(AudioError::Malformed(m)) if m.contains("timing table")));
}
@@ -903,7 +906,7 @@ fn an_audio_timing_table_with_a_run_per_sample_is_bounded() {
fn the_audio_timing_bound_follows_the_sample_entry_not_the_handler() {
let video_handler = atom(b"hdlr", &[0u32, 0, u32::from_be_bytes(*b"vide"), 0, 0, 0, 0].map(u32::to_be_bytes).concat());
let frames = 200_000;
let f = mp3_mp4(frames, |k, a| match k {
let f = symphonia_mp4(frames, |k, a| match k {
b"stts" => one_sample_runs(frames),
b"hdlr" => video_handler.clone(),
_ => a,
+1
View File
@@ -12,6 +12,7 @@ mod ac3_encoder_oracle;
mod ac3_oracle;
mod alac;
mod concealment;
mod mp3_decoder_oracle;
mod mp3_encoder_oracle;
mod mpa_oracle;
mod opus;
@@ -0,0 +1,397 @@
//! The Layer III decoder against ffmpeg's float MP3 decoder as the reference, sample for sample.
//!
//! - LAME through ffmpeg (`libmp3lame`): MPEG-1, MPEG-2 and MPEG-2.5 sampling frequencies, mono,
//! stereo and joint stereo, CBR from 8 to 320 kbit/s and VBR, transients (block switching).
//! - The `lame` command, when installed: CRC-protected frames, forced mid / side and no bit
//! reservoir.
//! - This crate's own encoder.
//! - What no encoder at hand writes, made by editing LAME's frames so that they stay valid:
//! intensity stereo (the `mode_extension` bit turned on, which MPEG-1 scalefactors do not
//! depend on) and mixed blocks (`mixed_block_flag` set on short blocks whose scalefactors take
//! no bits, so the granule's `part2_3_length` still holds).
//!
//! Each stream's side information is read here as well, to prove what it covers (short blocks,
//! mid / side, CRC).
//!
//! Streams are written without a Xing / Info frame, so ffmpeg decodes every frame and trims
//! nothing: both decoders' outputs line up from the first sample.
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use craft_audio::mp3::{Config, Decoder, Encoder, InfoTag, StereoMode};
use craft_audio::mpa::{Mode, parse_header};
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)";
/// Clicks over a quiet tone: an encoder switches to short blocks around each click.
const CLICKS: &str = "aevalsrc=exprs=if(lt(mod(t\\,0.2)\\,0.004)\\,0.9*sin(2*PI*2500*t)\\,0.05*sin(2*PI*330*t))|if(lt(mod(t+0.1\\,0.2)\\,0.004)\\,0.8*sin(2*PI*1800*t)\\,0.04*sin(2*PI*220*t))";
const NOISE: &str = "anoisesrc=color=pink:amplitude=0.4:seed=7";
/// Two nearly equal channels: an encoder codes them as mid / side.
const CORRELATED: &str = "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.05*sin(2*PI*5000*t)|0.28*sin(2*PI*441*t+0.1)+0.05*sin(2*PI*5000*t)";
/// A stream from ffmpeg's LAME encoder.
struct Lame {
name: &'static str,
input: &'static str,
rate: u32,
channels: usize,
/// Encoder arguments after `-c:a libmp3lame`.
enc: &'static [&'static str],
}
const LAME: &[Lame] = &[
Lame { name: "joint_44k_128k", input: TONES, rate: 44_100, channels: 2, enc: &["-b:a", "128k"] },
Lame { name: "joint_44k_correlated", input: CORRELATED, rate: 44_100, channels: 2, enc: &["-b:a", "96k"] },
Lame { name: "joint_44k_clicks", input: CLICKS, rate: 44_100, channels: 2, enc: &["-b:a", "160k"] },
Lame { name: "stereo_48k_320k", input: NOISE, rate: 48_000, channels: 2, enc: &["-b:a", "320k", "-joint_stereo", "0"] },
Lame { name: "mono_32k_48k", input: CLICKS, rate: 32_000, channels: 1, enc: &["-b:a", "48k"] },
Lame { name: "vbr_44k_noise", input: NOISE, rate: 44_100, channels: 2, enc: &["-q:a", "2"] },
Lame { name: "lsf_22k_64k", input: CLICKS, rate: 22_050, channels: 2, enc: &["-b:a", "64k"] },
Lame { name: "lsf_24k_mono_32k", input: TONES, rate: 24_000, channels: 1, enc: &["-b:a", "32k"] },
Lame { name: "lsf_16k_vbr", input: NOISE, rate: 16_000, channels: 2, enc: &["-q:a", "4"] },
Lame { name: "mpeg25_12k_24k", input: CLICKS, rate: 12_000, channels: 2, enc: &["-b:a", "24k"] },
Lame { name: "mpeg25_11k_mono", input: TONES, rate: 11_025, channels: 1, enc: &["-b:a", "16k"] },
Lame { name: "mpeg25_8k_8k", input: CLICKS, rate: 8_000, channels: 1, enc: &["-b:a", "8k"] },
// Low bit rates: many short blocks with scalefac_compress 0, for the mixed block edits.
Lame { name: "mixed_44k_32k", input: CLICKS, rate: 44_100, channels: 2, enc: &["-b:a", "32k"] },
Lame { name: "mixed_32k_mono_32k", input: CLICKS, rate: 32_000, channels: 1, enc: &["-b:a", "32k"] },
Lame { name: "mixed_lsf_22k_32k", input: CLICKS, rate: 22_050, channels: 2, enc: &["-b:a", "32k"] },
// 16 kHz has the band tables of MPEG-2.5's 11.025 and 12 kHz, whose streams give LAME few
// short blocks to edit.
Lame { name: "mixed_lsf_16k_32k", input: CLICKS, rate: 16_000, channels: 2, enc: &["-b:a", "32k"] },
];
/// The minimum SNR against ffmpeg. Both decoders compute in f32 with different IMDCT and
/// synthesis factorizations: they agree to 125-132 dB on encoded and edited streams. A wrong
/// table entry, band boundary or stereo factor costs far more: the 8 kHz short-block region
/// boundary at 36 lines instead of 72 measured -5 dB.
const MIN_SNR: f64 = 100.0;
fn generate(out: &Path, cmd: impl FnOnce(&Path) -> Command) -> Option<PathBuf> {
crate::testkit::fixtures::generate(out, |tmp| cmd(tmp).stdin(Stdio::null()).status().is_ok_and(|s| s.success()))
}
/// The LAME stream for `s`, or `None` (after saying so) when this ffmpeg has no `libmp3lame`; a
/// failure with `CRAFT_MEDIA_REQUIRE_ORACLES=1`.
fn lame_stream(ff: &Path, s: &Lame) -> Option<PathBuf> {
let out = crate::testkit::fixtures_dir("mp3dec").join(format!("{}.mp3", s.name));
generate(&out, |tmp| {
let mut c = Command::new(ff);
c.args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i"])
.arg(format!("{}:sample_rate={}", s.input, s.rate))
.args(["-t", "2", "-ac", &s.channels.to_string(), "-c:a", "libmp3lame"])
.args(s.enc)
.args(["-write_xing", "0", "-id3v2_version", "0", "-f", "mp3"])
.arg(tmp);
c
})
.or_else(|| {
assert!(!craft_media_testkit::oracles_required(), "could not generate {} (ffmpeg without libmp3lame?)", s.name);
eprintln!("SKIPPED {}: ffmpeg could not encode it (no libmp3lame?)", s.name);
None
})
}
/// The frames of a raw stream.
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 float decode of `file`, interleaved.
fn ffmpeg_decode(ff: &Path, file: &Path) -> Vec<f32> {
let o = Command::new(ff).args(["-v", "error", "-c:a", "mp3float", "-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()
}
/// Our decode of every frame of `data`, planar.
fn decode_all(data: &[u8], rate: u32, channels: usize) -> Vec<Vec<f32>> {
let mut dec = Decoder::new();
let mut out: Vec<Vec<f32>> = vec![Vec::new(); channels];
for f in frames(data) {
let d = dec.decode(f).unwrap_or_else(|e| panic!("frame {}: {e}", out[0].len()));
assert_eq!((d.header.sample_rate, d.channels.len()), (rate, channels));
for (o, c) in out.iter_mut().zip(d.channels) {
o.extend(c);
}
}
out
}
/// The worst channel's SNR of `ours` against ffmpeg's decode, which must be as long.
fn snr(ours: &[Vec<f32>], want: &[f32], what: &str) -> f64 {
let channels = ours.len();
assert_eq!(want.len() / channels, ours[0].len(), "{what}: sample count");
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 = f64::from(want[i * channels + c]);
sig += b * b;
err += (f64::from(a) - b).powi(2);
}
worst = worst.min(10.0 * (sig / err.max(1e-30)).log10());
}
worst
}
fn check(ff: &Path, file: &Path, rate: u32, channels: usize, what: &str) {
let data = std::fs::read(file).unwrap();
assert!(frames(&data).len() > 10, "{what}: too few frames");
let ours = decode_all(&data, rate, channels);
let snr = snr(&ours, &ffmpeg_decode(ff, file), what);
println!("{what}: SNR vs ffmpeg {snr:.1} dB");
assert!(snr >= MIN_SNR, "{what}: SNR {snr:.1} dB < {MIN_SNR}");
}
#[test]
fn lame_streams_match_ffmpeg() {
let ff = require_ffmpeg!();
for s in LAME {
let Some(f) = lame_stream(&ff, s) else { continue };
check(&ff, &f, s.rate, s.channels, s.name);
}
}
/// The `lame` command's options ffmpeg does not reach: CRC, forced mid / side, no reservoir.
#[test]
fn lame_cli_streams_match_ffmpeg() {
let ff = require_ffmpeg!();
let Some(lame) = crate::testkit::oracle::find_tool("lame") else {
eprintln!("lame not installed, skipped");
return;
};
let cases: &[(&str, &[&str], u32, usize)] = &[
("crc_48k_mono", &["-p", "-b", "96", "-m", "m"], 48_000, 1),
("nores_32k", &["--nores", "-b", "80"], 32_000, 2),
("forced_ms_44k", &["-m", "f", "-b", "96"], 44_100, 2),
];
for &(name, args, rate, channels) in cases {
let dir = crate::testkit::fixtures_dir("mp3dec");
let wav = dir.join(format!("{name}.wav"));
let input = if channels == 1 { "aevalsrc=exprs=if(lt(mod(t\\,0.2)\\,0.004)\\,0.9\\,0.05)*sin(2*PI*700*t)" } else { CLICKS };
generate(&wav, |tmp| {
let mut c = Command::new(&ff);
c.args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i"])
.arg(format!("{input}:sample_rate={rate}"))
.args(["-t", "2", "-c:a", "pcm_s16le", "-f", "wav"])
.arg(tmp);
c
})
.unwrap();
let out = dir.join(format!("{name}.mp3"));
let f = generate(&out, |tmp| {
let mut c = Command::new(&lame);
c.args(["--quiet", "-t", "--resample"]).arg(format!("{}", f64::from(rate) / 1000.0)).args(args).arg(&wav).arg(tmp);
c
})
.unwrap_or_else(|| panic!("lame could not encode {name}"));
if name.starts_with("crc") {
assert!(parse_header(&std::fs::read(&f).unwrap()).unwrap().crc, "{name}: no CRC");
}
check(&ff, &f, rate, channels, name);
}
}
#[test]
fn our_encoder_streams_match_ffmpeg() {
let ff = require_ffmpeg!();
let dir = crate::testkit::fixtures_dir("mp3dec");
for (rate, channels, kbps, stereo) in [
(44_100, 2, 192, StereoMode::JointStereo),
(48_000, 2, 128, StereoMode::Stereo),
(32_000, 1, 64, StereoMode::Stereo),
(24_000, 2, 64, StereoMode::JointStereo),
] {
let input: Vec<Vec<f32>> = (0..channels)
.map(|c| {
(0..rate as usize).map(|i| 0.4 * (i as f32 * (0.03 + 0.02 * c as f32)).sin() * (1.0 + (i as f32 * 0.0007).sin()) / 2.0).collect()
})
.collect();
let refs: Vec<&[f32]> = input.iter().map(Vec::as_slice).collect();
let mut enc = Encoder::new(Config { stereo, ..Config::cbr(rate, channels, kbps) }).unwrap();
let mut data = enc.encode(&refs);
data.extend(enc.flush());
let path = dir.join(format!("ours_{rate}_{channels}ch_{kbps}k.mp3"));
std::fs::write(&path, &data).unwrap();
check(&ff, &path, rate, channels, &format!("our encoder {rate} Hz {channels} ch {kbps} kbit/s"));
}
}
/// The streams cover what they are meant to: short blocks (LAME switches on the clicks, at
/// every sampling frequency family), mid / side and CRC.
#[test]
fn streams_cover_short_blocks_and_mid_side() {
let ff = require_ffmpeg!();
for s in LAME.iter().filter(|s| s.input == CLICKS) {
let Some(f) = lame_stream(&ff, s) else { continue };
let data = std::fs::read(f).unwrap();
let short = frames(&data).iter().flat_map(|f| granules(f)).filter(|g| g.block_type == 2).count();
assert!(short > 5, "{}: {short} short-block granules", s.name);
}
let s = LAME.iter().find(|s| s.name == "joint_44k_correlated").unwrap();
let Some(f) = lame_stream(&ff, s) else { return };
let data = std::fs::read(f).unwrap();
let ms = frames(&data).iter().filter(|f| parse_header(f).is_ok_and(|h| h.mode == Mode::JointStereo && h.mode_extension & 2 != 0)).count();
assert!(ms > 10, "{ms} mid/side frames");
}
/// What the side information says of one granule of one channel, and where its
/// `mixed_block_flag` is (a bit offset in the frame) when it switches windows.
struct GranuleSide {
block_type: u8,
scalefac_compress: u32,
mixed_bit: Option<usize>,
}
/// Read the side information of a Layer III frame (ISO/IEC 11172-3 §2.4.1.7): each granule and
/// channel is 59 bits in MPEG-1, 63 in MPEG-2.
fn granules(frame: &[u8]) -> Vec<GranuleSide> {
let h = parse_header(frame).unwrap();
let lsf = h.sample_rate < 32_000;
let nch = h.channels();
let bit = |p: usize| (frame[p / 8] >> (7 - p % 8)) & 1;
let field = |p: usize, n: usize| (p..p + n).fold(0u32, |v, q| v << 1 | u32::from(bit(q)));
let mut p = 32 + if h.crc { 16 } else { 0 };
p += if lsf { 8 + if nch == 1 { 1 } else { 2 } } else { 9 + if nch == 1 { 5 } else { 3 } + 4 * nch };
let mut out = Vec::new();
for _ in 0..if lsf { 1 } else { 2 } {
for _ in 0..nch {
let scalefac_compress = field(p + 29, if lsf { 9 } else { 4 });
let ws = p + if lsf { 38 } else { 33 };
let (block_type, mixed_bit) = if bit(ws) == 1 { (field(ws + 1, 2) as u8, Some(ws + 3)) } else { (0, None) };
out.push(GranuleSide { block_type, scalefac_compress, mixed_bit });
p += if lsf { 63 } else { 59 };
}
}
out
}
/// A granule the mixed block test may edit: its frame, where its `mixed_block_flag` is, whether
/// it is a short block, and whether its scalefactors take no bits.
#[derive(Clone, Copy)]
struct Candidate {
frame: usize,
bit: Option<usize>,
short: bool,
editable: bool,
}
/// Write `frames` (edited copies) as a stream and compare it with ffmpeg.
fn check_edited(ff: &Path, frames: Vec<Vec<u8>>, rate: u32, channels: usize, name: &str) {
let path = crate::testkit::fixtures_dir("mp3dec").join(format!("{name}.mp3"));
std::fs::write(&path, frames.concat()).unwrap();
check(ff, &path, rate, channels, name);
}
/// Intensity stereo: LAME's MPEG-1 joint stereo frames with the intensity bit of
/// `mode_extension` set. The right channel's zero bands above its last non-zero line become
/// intensity coded with the positions its scalefactors give, in long and short blocks.
#[test]
fn intensity_stereo_matches_ffmpeg() {
let ff = require_ffmpeg!();
for name in ["joint_44k_clicks", "joint_44k_correlated", "joint_44k_128k"] {
let s = LAME.iter().find(|s| s.name == name).unwrap();
let Some(f) = lame_stream(&ff, s) else { continue };
let data = std::fs::read(f).unwrap();
let mut edited = 0;
let fr: Vec<Vec<u8>> = frames(&data)
.into_iter()
.map(|f| {
let mut f = f.to_vec();
if parse_header(&f).unwrap().mode == Mode::JointStereo {
f[3] |= 0x10;
edited += 1;
}
f
})
.collect();
assert!(edited > 10, "{name}: {edited} joint stereo frames");
check_edited(&ff, fr, s.rate, s.channels, &format!("{name}_intensity"));
}
}
/// Mixed blocks: LAME's short-block granules with `mixed_block_flag` set, at MPEG-1 and MPEG-2
/// sampling frequencies (16 kHz shares its band tables with MPEG-2.5's 11.025 and 12 kHz).
///
/// - Only granules whose scalefactors take no bits (`scalefac_compress` 0) are edited: the mixed
/// layout reads the same zero bits, so the stream stays valid. Editing others would shift every
/// later field of the granule past its `part2_3_length`, and decoders differ on such damage.
/// - Not a granule whose channel's next granule is a short block that stays unmixed. ffmpeg's
/// short-block IMDCT drops the last 6 overlap values of each subband, which is exact after a
/// start or short window (whose tails are zero) but not after a mixed block's long subbands
/// (the normal window's tail is not); ISO/IEC 11172-3 adds the whole overlap, as this decoder
/// does.
/// - Not at 8 kHz, where ffmpeg does not implement mixed blocks.
#[test]
fn mixed_blocks_match_ffmpeg() {
let ff = require_ffmpeg!();
for name in ["mixed_44k_32k", "mixed_32k_mono_32k", "mixed_lsf_22k_32k", "mixed_lsf_16k_32k"] {
let s = LAME.iter().find(|s| s.name == name).unwrap();
let Some(f) = lame_stream(&ff, s) else { continue };
let data = std::fs::read(f).unwrap();
let mut fr: Vec<Vec<u8>> = frames(&data).into_iter().map(<[u8]>::to_vec).collect();
// each channel's granules in order
let mut channels: Vec<Vec<Candidate>> = vec![Vec::new(); s.channels];
for (frame, f) in fr.iter().enumerate() {
for (i, g) in granules(f).into_iter().enumerate() {
let short = g.block_type == 2;
let editable = short && g.scalefac_compress == 0;
channels[i % s.channels].push(Candidate { frame, bit: g.mixed_bit, short, editable });
}
}
let mut mixed = 0;
for ch in &channels {
// from the last granule back, so whether the next one is mixed is already decided
let mut next_pure_short = false;
for c in ch.iter().rev() {
let edit = c.editable && !next_pure_short;
if let (true, Some(p)) = (edit, c.bit) {
fr[c.frame][p / 8] |= 0x80 >> (p % 8);
mixed += 1;
}
next_pure_short = c.short && !edit;
}
}
println!("{name}: {mixed} granules made mixed");
assert!(mixed > 5, "{name}: {mixed} mixed granules");
check_edited(&ff, fr, s.rate, s.channels, &format!("{name}_mixed"));
}
}
/// A Xing / Info frame with a LAME tag gives the frame count, encoder delay and padding.
#[test]
fn info_tag_of_a_lame_stream() {
let ff = require_ffmpeg!();
let out = crate::testkit::fixtures_dir("mp3dec").join("tagged_44k.mp3");
let f = generate(&out, |tmp| {
let mut c = Command::new(&ff);
c.args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i", "sine=frequency=500:sample_rate=44100", "-t", "1"])
.args(["-c:a", "libmp3lame", "-b:a", "128k", "-id3v2_version", "0", "-f", "mp3"])
.arg(tmp);
c
})
.or_else(|| {
assert!(!craft_media_testkit::oracles_required(), "could not write a LAME-tagged stream (ffmpeg without libmp3lame?)");
None
});
let Some(f) = f else {
eprintln!("SKIPPED: ffmpeg could not encode a LAME-tagged stream (no libmp3lame?)");
return;
};
let data = std::fs::read(f).unwrap();
let fr = frames(&data);
let tag = InfoTag::parse(fr[0]).expect("Info frame");
assert_eq!(tag.frames, Some(fr.len() as u32 - 1));
let (delay, padding) = tag.delay_padding.expect("LAME tag");
// 1 s at 44.1 kHz: the frames hold the delay, the audio and the padding
assert_eq!(u64::from(delay) + 44_100 + u64::from(padding), (fr.len() as u64 - 1) * 1152);
assert!(InfoTag::parse(fr[1]).is_none());
}
+55 -1
View File
@@ -267,6 +267,58 @@ const CASES: &[Case] = &[
frames: Some(240_000),
min_snr: 80.0,
},
Case {
name: "mp3.mp3",
surround: false,
mono: false,
rate: 48_000,
args: &["-c:a", "libmp3lame", "-b:a", "192k"],
container: Container::MpegAudio,
codec: CodecId::Mp3,
layout: ChannelLayout::STEREO,
channels: 2,
frames: Some(240_000),
min_snr: 100.0,
},
Case {
name: "mp3-lsf-mono.mp3",
surround: false,
mono: true,
rate: 24_000,
args: &["-c:a", "libmp3lame", "-b:a", "32k"],
container: Container::MpegAudio,
codec: CodecId::Mp3,
layout: ChannelLayout::MONO,
channels: 1,
frames: Some(120_000),
min_snr: 100.0,
},
Case {
name: "mp3.mkv",
surround: false,
mono: false,
rate: 48_000,
args: &["-c:a", "libmp3lame", "-b:a", "128k"],
container: Container::Matroska,
codec: CodecId::Mp3,
layout: ChannelLayout::STEREO,
channels: 2,
frames: Some(240_000),
min_snr: 100.0,
},
Case {
name: "mp3.mp4",
surround: false,
mono: false,
rate: 48_000,
args: &["-c:a", "libmp3lame", "-b:a", "128k"],
container: Container::Mp4,
codec: CodecId::Mp3,
layout: ChannelLayout::STEREO,
channels: 2,
frames: Some(240_000),
min_snr: 100.0,
},
Case {
name: "pcm-s24be.mov",
surround: false,
@@ -342,6 +394,8 @@ fn reference_lead(name: &str) -> usize {
"aac.mkv" => 1024,
"eac3-5.1.mkv" => 256,
"mp2.mkv" => 481,
// LAME's encoder delay and the decoder's, the Matroska CodecDelay
"mp3.mkv" => 1105,
_ => 0,
}
}
@@ -493,7 +547,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", "mp2.mp4", "pcm-s24be.mov"] {
for name in ["aac.m4a", "aac-5.1.m4a", "opus.mp4", "mp2.mp4", "mp3.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();