craft-audio: MP3 (Layer III) encoder from FilmCraft (feature mp3-encode) (#869)

Moves filmcraft-mp3enc (storytold/filmcraft@4bec22b) into craft_audio::mp3,
on craft-media's BitWriter. `encode` turns the feature on, mp3::Error converts
into AudioError::Encode, and a test covers every sample rate and bit rate with
extreme signals.
This commit is contained in:
Dylan M. Taylor
2026-11-11 17:26:39 -05:00
committed by GitHub
parent 3f2897d2a6
commit 1ebc34b0cf
13 changed files with 2380 additions and 9 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
applications' shortcut sets. Command catalogs, file readers and the shortcut editor UI stay
in each app.
- [`craft-audio`](crates/audio/README.md): audio file I/O and codecs (WAV, AIFF, FLAC, AAC, Opus,
AC-3/E-AC-3, MP1/MP2), streaming and seeking, and packet decoding for any demuxer. See its
AC-3/E-AC-3, MP1/MP2, MP3 encoding), streaming and seeking, and packet decoding for any demuxer. See its
README for formats and features.
- [`craft-media`](crates/media/README.md): bit-level I/O (`bitstream`), clean-room MP4/MOV
(`isobmff`), Matroska/WebM (`matroska`) and MXF (`mxf`) demuxers and muxers, Ogg (`ogg`) and AVI (`avi`) demuxers, from
+5 -3
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, 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 encoder, Opus and MP1/MP2 decoders."
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -22,8 +22,8 @@ decode = ["decode-native", "symphonia"]
decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
# The symphonia fallback for the formats with no native decoder yet.
symphonia = ["decode-native", "dep:symphonia"]
# Every encoder: the lossless file writers and the AAC, ALAC and AC-3/E-AC-3 packet encoders.
encode = ["lossless-encode", "aac-encode", "alac-encode", "ac3-encode", "eac3-encode"]
# Every encoder: the lossless file writers and the AAC, ALAC, AC-3/E-AC-3 and MP3 encoders.
encode = ["lossless-encode", "aac-encode", "alac-encode", "ac3-encode", "eac3-encode", "mp3-encode"]
# Encode WAV/BWF/RF64, AIFF/AIFF-C and FLAC files.
lossless-encode = ["pcm-encode", "flac-encode"]
# Encode WAV/BWF/RF64 and AIFF/AIFF-C files.
@@ -41,6 +41,8 @@ alac-decode = []
alac-encode = []
ac3-encode = ["craft-media/bitstream"]
eac3-encode = ["craft-media/bitstream"]
# MPEG-1/2 Layer III (MP3) encoder (`mp3`).
mp3-encode = ["craft-media/bitstream"]
# Multi-resolution waveform overviews (`peaks`).
peaks = []
# Encode AAC channels in parallel (with `aac-encode`).
+32 -3
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, Opus and MP1/MP2 decoders.
FLAC encoder, AAC and ALAC decoders/encoders, AC-3/E-AC-3 decoder/encoder, MP3 encoder, Opus and MP1/MP2 decoders.
Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced
by native decoders.
@@ -28,7 +28,7 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
| `decode` | `decode`, `probe`, `Decoder`, `tracks`, `TrackDecoder`; every format in [Format support](#format-support) | `decode-native`, `symphonia` |
| `decode-native` | the same, native formats only (others are `Unsupported`) | the five 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 and AC-3/E-AC-3 packet encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode` |
| `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` |
| `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header` | none |
| `flac-encode` | `encode`, `FileEncoder` for FLAC; `flac::Encoder`, the packet encoder for callers that mux FLAC themselves | craft-media `bitstream` |
@@ -39,6 +39,7 @@ 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-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` |
@@ -309,7 +310,7 @@ 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 | — |
| MP3 (Layer III) | symphonia | 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 | — |
@@ -414,8 +415,35 @@ 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 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 |
### MP3 encoding
```rust
use craft_audio::mp3::{Config, DELAY, Encoder};
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
// a decoder's output lags the input by DELAY (1057) samples
```
`mp3::bitrates(rate)`, `nearest_bitrate(rate, kbps)` and `nearest_sample_rate(rate)` map arbitrary
settings onto what a stream can carry; `Encoder::new` refuses anything else. Samples that are not
finite count as silence and the rest are clamped to ±4, so no input makes an invalid frame.
- Hybrid filterbank: the Annex C polyphase analysis filter, the normal-window MDCT and alias
reduction; reconstruction is checked against a reference of the normative decoder filterbank.
- Psychoacoustic model: band energies spread across scalefactor bands, floored by the threshold in
quiet (full scale = 96 dB SPL); a low-pass by bit rate.
- Quantization: the inner and outer loops with scalefactors, an exhaustive choice of the region
split and the Huffman table per region, and the bit reservoir (`main_data_begin`) bounded by the
decoder's 7680-bit input buffer.
- Not yet: block switching (short blocks; transients pre-echo), `scfsi` and `preflag`, intensity
stereo, a tonality-aware model, VBR/ABR, the Xing/Info and LAME gapless tags (the delay is not
signalled), CRC, free format, and an `EncodeFormat` entry for `encode`/`FileEncoder`.
### AC-3 and E-AC-3 encoding
`ac3::encoder` is implemented from **ETSI TS 102 366 V1.4.1 (September 2017)**,
@@ -548,6 +576,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 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)) |
Where apps had overlapping implementations, the one that measured better was kept, with the
+12 -2
View File
@@ -18,11 +18,12 @@
//! - [`encode`], [`FileEncoder`]: WAV/BWF/RF64 and AIFF/AIFF-C (feature `pcm-encode`, which also
//! gives [`wav_float_header`]) and FLAC (feature `flac-encode`), whole-buffer or incremental into
//! any `Write + Seek`. Feature `lossless-encode` turns on both; feature `encode` turns on
//! `lossless-encode` and `aac-encode`.
//! `lossless-encode` and every codec's encoder.
//! - [`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`), [`flac`] (`flac-encode`: the packet encoder).
//! (`ac3-decode`), [`mpa`] (`mpa-decode`), [`mp3`] (`mp3-encode`: the Layer III encoder),
//! [`flac`] (`flac-encode`: the packet encoder).
//!
//! The shared types below build with no features.
@@ -34,6 +35,8 @@ pub mod ac3;
pub mod alac;
mod buffer;
mod layout;
#[cfg(feature = "mp3-encode")]
pub mod mp3;
#[cfg(feature = "mpa-decode")]
pub mod mpa;
#[cfg(feature = "opus-decode")]
@@ -162,6 +165,13 @@ impl From<ac3::encoder::Error> for AudioError {
}
}
#[cfg(feature = "mp3-encode")]
impl From<mp3::Error> for AudioError {
fn from(e: mp3::Error) -> Self {
AudioError::Encode(e.to_string())
}
}
#[cfg(feature = "flac-encode")]
impl From<flac::Error> for AudioError {
fn from(e: flac::Error) -> Self {
+111
View File
@@ -0,0 +1,111 @@
//! The Layer III hybrid analysis filterbank: the polyphase analysis subband filter of
//! ISO/IEC 11172-3 Annex C.1.3 (32 subbands), then per subband an 18-line MDCT with the normal
//! (block type 0) window over two granules, and the encoder side of the alias-reduction
//! butterflies (the inverse of §2.4.3.4.10.4's decoder butterflies).
use std::sync::OnceLock;
use super::tables::{alias_coefficients, analysis_matrix, analysis_window};
/// Frequency lines per granule.
pub(crate) const GRANULE: usize = 576;
/// The polyphase analysis filter of one channel: a 512-sample FIFO, newest sample first.
#[derive(Clone)]
pub(crate) struct Polyphase {
x: [f32; 512],
}
impl Polyphase {
pub fn new() -> Self {
Polyphase { x: [0.0; 512] }
}
/// Shift in 32 samples (oldest first) and compute the 32 subband samples (Annex C.1.3).
pub fn filter(&mut self, input: &[f32; 32], out: &mut [f32; 32]) {
self.x.copy_within(0..480, 32);
for (i, s) in input.iter().enumerate() {
// X[31] receives the first of the new samples, X[0] the last
self.x[31 - i] = *s;
}
let c = analysis_window();
let mut y = [0f32; 64];
for (i, yi) in y.iter_mut().enumerate() {
let mut acc = 0f32;
for j in 0..8 {
acc += c[i + 64 * j] * self.x[i + 64 * j];
}
*yi = acc;
}
for (o, row) in out.iter_mut().zip(analysis_matrix()) {
*o = row.iter().zip(&y).map(|(m, y)| m * y).sum();
}
}
}
/// The MDCT kernel for normal blocks: `w[i] cos(π/72 (2i + 1 + 18)(2k + 1)) / 9`, `k` in `0..18`,
/// `i` in `0..36`, with the sine window `w[i] = sin(π/36 (i + 1/2))`. The 1/9 (= 2/18) makes it the
/// exact inverse of the decoder's unscaled IMDCT with windowed overlap-add.
fn mdct_kernel() -> &'static [[f32; 36]; 18] {
static K: OnceLock<[[f32; 36]; 18]> = OnceLock::new();
K.get_or_init(|| {
let mut k = [[0f32; 36]; 18];
for (kk, row) in k.iter_mut().enumerate() {
for (i, c) in row.iter_mut().enumerate() {
let w = (std::f64::consts::PI / 36.0 * (i as f64 + 0.5)).sin();
let a = std::f64::consts::PI / 72.0 * (2 * i + 1 + 18) as f64 * (2 * kk + 1) as f64;
*c = (w * a.cos() / 9.0) as f32;
}
}
k
})
}
/// The hybrid filterbank of one channel: the polyphase filter and the previous granule's subband
/// samples for the MDCT overlap.
#[derive(Clone)]
pub(crate) struct Analysis {
poly: Polyphase,
prev: [[f32; 18]; 32],
}
impl Analysis {
pub fn new() -> Self {
Analysis { poly: Polyphase::new(), prev: [[0.0; 18]; 32] }
}
/// Transform one granule of 576 samples into 576 frequency lines (`xr[18 * sb + k]`).
pub fn granule(&mut self, pcm: &[f32; GRANULE], xr: &mut [f32; GRANULE]) {
let mut cur = [[0f32; 18]; 32];
let mut slot = [0f32; 32];
let mut out = [0f32; 32];
for (t, chunk) in pcm.as_chunks::<32>().0.iter().enumerate().take(18) {
slot.copy_from_slice(chunk);
self.poly.filter(&slot, &mut out);
for (sb, v) in out.iter().enumerate() {
// frequency inversion: odd subbands, odd time slots (§2.4.3.4.10.5 mirrored)
cur[sb][t] = if sb % 2 == 1 && t % 2 == 1 { -v } else { *v };
}
}
let kernel = mdct_kernel();
for sb in 0..32 {
let mut z = [0f32; 36];
z[..18].copy_from_slice(&self.prev[sb]);
z[18..].copy_from_slice(&cur[sb]);
for (k, row) in kernel.iter().enumerate() {
xr[18 * sb + k] = row.iter().zip(&z).map(|(a, b)| a * b).sum();
}
}
self.prev = cur;
// alias reduction: the inverse of the decoder's butterflies between adjacent subbands
for sb in 1..32 {
for (i, (cs, ca)) in alias_coefficients().iter().enumerate() {
let up = 18 * sb - 1 - i;
let dn = 18 * sb + i;
let (a, b) = (xr[up], xr[dn]);
xr[up] = a * cs + b * ca;
xr[dn] = b * cs - a * ca;
}
}
}
}
+228
View File
@@ -0,0 +1,228 @@
//! Huffman coding of a granule's quantized lines (ISO/IEC 11172-3 §2.4.2.7 `huffmancodebits`,
//! §2.4.3.4.6): the big-values region in pairs, split into three regions at scalefactor band
//! boundaries with a table each, then the count1 region in quadruples of magnitude ≤ 1, then the
//! zeros. The encoder picks the region split and tables that need the fewest bits.
use craft_media::bitstream::BitWriter;
use super::filterbank::GRANULE;
use super::huffman_tables::{LINBITS, QUAD_A_CODES, QUAD_A_LENS, QUAD_B_CODES, pair_table};
/// The tables without `linbits`, smallest first.
const SMALL: [usize; 13] = [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15];
/// Escape tables: 16-23 share table 16's codes, 24-31 table 24's.
const ESC16: usize = 16;
const ESC24: usize = 24;
/// How a granule's lines are coded, as signalled in the side information.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Coding {
/// Pairs in the big-values region.
pub big_values: usize,
/// Quadruples in the count1 region.
pub count1: usize,
pub table_select: [usize; 3],
pub region0_count: usize,
pub region1_count: usize,
/// 0: table A, 1: table B.
pub count1_table: usize,
/// Huffman-coded bits (part 3 of `part2_3_length`).
pub bits: usize,
}
/// Cumulative bits per table over the scalefactor bands of the big-values region: entry `b` sums
/// bands `0..b`.
struct Prefix {
small: [[u32; 23]; SMALL.len()],
esc16: [u32; 23],
esc24: [u32; 23],
escapes: [u32; 23],
max: [u16; 22],
}
/// Bits needed for `v` (0 for 0).
fn bit_width(v: u32) -> u32 {
32 - v.leading_zeros()
}
impl Prefix {
fn new(ix: &[u16], bands: &[u16; 23], end: usize) -> (Prefix, usize) {
let mut p = Prefix { small: [[0; 23]; SMALL.len()], esc16: [0; 23], esc24: [0; 23], escapes: [0; 23], max: [0; 22] };
let tables: Vec<_> = SMALL.iter().map(|&t| pair_table(t)).collect();
let (t16, t24) = (pair_table(ESC16), pair_table(ESC24));
let mut nb = 0;
for (b, w) in bands.windows(2).enumerate() {
let (lo, hi) = (usize::from(w[0]), usize::from(w[1]).min(end));
if lo >= end || b >= 22 {
break;
}
let lines = ix.get(lo..hi).unwrap_or(&[]);
let max = lines.iter().copied().max().unwrap_or(0);
p.max[b] = max;
for (n, t) in tables.iter().enumerate() {
let bits = match t.as_ref().filter(|t| usize::from(max) < t.size) {
Some(t) => lines
.as_chunks::<2>()
.0
.iter()
.map(|q| {
let (x, y) = (usize::from(q[0]), usize::from(q[1]));
u32::from(t.lens.get(x * t.size + y).copied().unwrap_or(32)) + u32::from(x != 0) + u32::from(y != 0)
})
.sum(),
None => u32::MAX / 32,
};
p.small[n][b + 1] = p.small[n][b].saturating_add(bits);
}
let (mut e16, mut e24, mut esc) = (0u32, 0u32, 0u32);
for q in lines.as_chunks::<2>().0 {
let (x, y) = (usize::from(q[0]), usize::from(q[1]));
let k = x.min(15) * 16 + y.min(15);
let signs = u32::from(x != 0) + u32::from(y != 0);
e16 += u32::from(t16.as_ref().and_then(|t| t.lens.get(k)).copied().unwrap_or(32)) + signs;
e24 += u32::from(t24.as_ref().and_then(|t| t.lens.get(k)).copied().unwrap_or(32)) + signs;
esc += u32::from(x >= 15) + u32::from(y >= 15);
}
p.esc16[b + 1] = p.esc16[b] + e16;
p.esc24[b + 1] = p.esc24[b] + e24;
p.escapes[b + 1] = p.escapes[b] + esc;
nb = b + 1;
}
(p, nb)
}
/// The cheapest table for bands `a..b`: (table_select, bits).
fn best(&self, a: usize, b: usize) -> (usize, u32) {
let max = self.max.get(a..b).and_then(|m| m.iter().copied().max()).unwrap_or(0);
if max == 0 {
return (0, 0);
}
let mut best = (0usize, u32::MAX);
for (n, &t) in SMALL.iter().enumerate() {
let sum = self.small[n][b].saturating_sub(self.small[n][a]);
if usize::from(max) < pair_table(t).map_or(0, |p| p.size) && sum < best.1 {
best = (t, sum);
}
}
// the escape tables: the smallest linbits that hold the largest value
let need = if max >= 15 { bit_width(u32::from(max) - 15) } else { 0 };
let escapes = self.escapes[b] - self.escapes[a];
for (family, base) in [(ESC16, self.esc16[b] - self.esc16[a]), (ESC24, self.esc24[b] - self.esc24[a])] {
if let Some(t) = (family..family + 8).find(|&t| LINBITS.get(t).is_some_and(|&l| l >= need)) {
let bits = base.saturating_add(escapes.saturating_mul(LINBITS.get(t).copied().unwrap_or(0)));
if bits < best.1 {
best = (t, bits);
}
}
}
best
}
}
/// Choose the regions and tables for `ix` (magnitudes ≤ [`super::tables::MAX_QUANT`]) with
/// scalefactor band boundaries `bands`.
pub(crate) fn choose(ix: &[u16; GRANULE], bands: &[u16; 23]) -> Coding {
let mut coding = Coding::default();
let Some(last) = ix.iter().rposition(|&v| v != 0) else {
return coding;
};
let n = (last + 2) & !1; // round up to a whole pair
let mut i = n;
while i >= 4 && ix[i - 4..i].iter().all(|&v| v <= 1) {
i -= 4;
}
// count1 region [i, n)
let (mut bits_a, mut bits_b, mut signs) = (0u32, 0u32, 0u32);
for q in ix[i..n].as_chunks::<4>().0 {
let idx = usize::from(q[0] * 8 + q[1] * 4 + q[2] * 2 + q[3]);
bits_a += u32::from(QUAD_A_LENS.get(idx).copied().unwrap_or(6));
bits_b += 4;
signs += q.iter().map(|&v| u32::from(v)).sum::<u32>();
}
coding.count1 = (n - i) / 4;
coding.count1_table = usize::from(bits_b < bits_a);
let count1_bits = bits_a.min(bits_b) + signs;
coding.big_values = i / 2;
// big-values region [0, i): costs per scalefactor band, clipped at i
let (prefix, nb) = Prefix::new(ix, bands, i);
let mut best: Option<(u32, usize, usize, [usize; 3])> = None;
if nb > 0 {
for r0 in 0..16usize {
for r1 in 0..8usize {
if r0 + r1 + 2 > 22 {
continue;
}
let s1 = (r0 + 1).min(nb);
let s2 = (r0 + r1 + 2).min(nb);
let (t0, b0) = prefix.best(0, s1);
let (t1, b1) = prefix.best(s1, s2);
let (t2, b2) = prefix.best(s2, nb);
let total = b0.saturating_add(b1).saturating_add(b2);
if best.is_none_or(|b| total < b.0) {
best = Some((total, r0, r1, [t0, t1, t2]));
}
if s2 >= nb && r1 > 0 {
break; // longer region 1 changes nothing
}
}
if r0 + 1 >= nb {
break;
}
}
}
let (big_bits, r0, r1, tables) = best.unwrap_or((0, 0, 0, [0; 3]));
coding.region0_count = r0;
coding.region1_count = r1;
coding.table_select = tables;
coding.bits = big_bits as usize + count1_bits as usize;
coding
}
/// Write the Huffman-coded lines; `negative[i]` is the sign of line `i`.
pub(crate) fn write(w: &mut BitWriter, ix: &[u16; GRANULE], negative: &[bool; GRANULE], c: &Coding, bands: &[u16; 23]) {
let r1 = bands.get(c.region0_count + 1).map_or(GRANULE, |&b| usize::from(b));
let r2 = bands.get(c.region0_count + c.region1_count + 2).map_or(GRANULE, |&b| usize::from(b));
let end = (2 * c.big_values).min(GRANULE);
for i in (0..end).step_by(2) {
let select = c.table_select[if i < r1 {
0
} else if i < r2 {
1
} else {
2
}];
let Some(t) = pair_table(select) else { continue };
let linbits = LINBITS.get(select).copied().unwrap_or(0);
let (x, y) = (usize::from(ix[i]), usize::from(ix[i + 1]));
let (xe, ye) = if linbits > 0 { (x.min(15), y.min(15)) } else { (x, y) };
let k = xe * t.size + ye;
w.write_bits(u32::from(t.codes.get(k).copied().unwrap_or(0)), u32::from(t.lens.get(k).copied().unwrap_or(0)));
if linbits > 0 && xe == 15 {
w.write_bits((x - 15) as u32, linbits);
}
if x != 0 {
w.write_bit(negative[i]);
}
if linbits > 0 && ye == 15 {
w.write_bits((y - 15) as u32, linbits);
}
if y != 0 {
w.write_bit(negative[i + 1]);
}
}
let c1_end = (end + 4 * c.count1).min(GRANULE);
for i in (end..c1_end).step_by(4) {
let Some(q) = ix.get(i..i + 4) else { break };
let idx = usize::from(q[0] * 8 + q[1] * 4 + q[2] * 2 + q[3]) & 15;
if c.count1_table == 0 {
w.write_bits(u32::from(QUAD_A_CODES[idx]), u32::from(QUAD_A_LENS[idx]));
} else {
w.write_bits(u32::from(QUAD_B_CODES[idx]), 4);
}
for (k, &v) in q.iter().enumerate() {
if v != 0 {
w.write_bit(negative[i + k]);
}
}
}
}
+341
View File
@@ -0,0 +1,341 @@
//! The Huffman code tables of Layer III (ISO/IEC 11172-3 Annex B, Table 3-B.7): `hcod` and
//! `hlen` per value pair `(x, y)` at index `x * size + y`, and the two count1 quadruple tables.
//! Tables 4 and 14 are not used by the standard; tables 16-23 share the codes of table 16 and
//! 24-31 those of table 24, each with its own number of `linbits`.
/// One pair table: `size × size` codes for values `0..size` (the escape value 15 of tables 16 and
/// 24 is followed by `linbits` bits of `value - 15`).
pub(crate) struct PairTable {
pub size: usize,
pub codes: &'static [u16],
pub lens: &'static [u8],
}
const T1C: [u16; 4] = [1, 1, 1, 0];
const T1L: [u8; 4] = [1, 3, 2, 3];
const T2C: [u16; 9] = [1, 2, 1, 3, 1, 1, 3, 2, 0];
const T2L: [u8; 9] = [1, 3, 6, 3, 3, 5, 5, 5, 6];
const T3C: [u16; 9] = [3, 2, 1, 1, 1, 1, 3, 2, 0];
const T3L: [u8; 9] = [2, 2, 6, 3, 2, 5, 5, 5, 6];
const T5C: [u16; 16] = [1, 2, 6, 5, 3, 1, 4, 4, 7, 5, 7, 1, 6, 1, 1, 0];
const T5L: [u8; 16] = [1, 3, 6, 7, 3, 3, 6, 7, 6, 6, 7, 8, 7, 6, 7, 8];
const T6C: [u16; 16] = [7, 3, 5, 1, 6, 2, 3, 2, 5, 4, 4, 1, 3, 3, 2, 0];
const T6L: [u8; 16] = [3, 3, 5, 7, 3, 2, 4, 5, 4, 4, 5, 6, 6, 5, 6, 7];
#[rustfmt::skip]
const T7C: [u16; 36] = [
1, 2, 10, 19, 16, 10,
3, 3, 7, 10, 5, 3,
11, 4, 13, 17, 8, 4,
12, 11, 18, 15, 11, 2,
7, 6, 9, 14, 3, 1,
6, 4, 5, 3, 2, 0,
];
#[rustfmt::skip]
const T7L: [u8; 36] = [
1, 3, 6, 8, 8, 9,
3, 4, 6, 7, 7, 8,
6, 5, 7, 8, 8, 9,
7, 7, 8, 9, 9, 9,
7, 7, 8, 9, 9, 10,
8, 8, 9, 10, 10, 10,
];
#[rustfmt::skip]
const T8C: [u16; 36] = [
3, 4, 6, 18, 12, 5,
5, 1, 2, 16, 9, 3,
7, 3, 5, 14, 7, 3,
19, 17, 15, 13, 10, 4,
13, 5, 8, 11, 5, 1,
12, 4, 4, 1, 1, 0,
];
#[rustfmt::skip]
const T8L: [u8; 36] = [
2, 3, 6, 8, 8, 9,
3, 2, 4, 8, 8, 8,
6, 4, 6, 8, 8, 9,
8, 8, 8, 9, 9, 10,
8, 7, 8, 9, 10, 10,
9, 8, 9, 9, 11, 11,
];
#[rustfmt::skip]
const T9C: [u16; 36] = [
7, 5, 9, 14, 15, 7,
6, 4, 5, 5, 6, 7,
7, 6, 8, 8, 8, 5,
15, 6, 9, 10, 5, 1,
11, 7, 9, 6, 4, 1,
14, 4, 6, 2, 6, 0,
];
#[rustfmt::skip]
const T9L: [u8; 36] = [
3, 3, 5, 6, 8, 9,
3, 3, 4, 5, 6, 8,
4, 4, 5, 6, 7, 8,
6, 5, 6, 7, 7, 8,
7, 6, 7, 7, 8, 9,
8, 7, 8, 8, 9, 9,
];
#[rustfmt::skip]
const T10C: [u16; 64] = [
1, 2, 10, 23, 35, 30, 12, 17,
3, 3, 8, 12, 18, 21, 12, 7,
11, 9, 15, 21, 32, 40, 19, 6,
14, 13, 22, 34, 46, 23, 18, 7,
20, 19, 33, 47, 27, 22, 9, 3,
31, 22, 41, 26, 21, 20, 5, 3,
14, 13, 10, 11, 16, 6, 5, 1,
9, 8, 7, 8, 4, 4, 2, 0,
];
#[rustfmt::skip]
const T10L: [u8; 64] = [
1, 3, 6, 8, 9, 9, 9, 10,
3, 4, 6, 7, 8, 9, 8, 8,
6, 6, 7, 8, 9, 10, 9, 9,
7, 7, 8, 9, 10, 10, 9, 10,
8, 8, 9, 10, 10, 10, 10, 10,
9, 9, 10, 10, 11, 11, 10, 11,
8, 8, 9, 10, 10, 10, 11, 11,
9, 8, 9, 10, 10, 11, 11, 11,
];
#[rustfmt::skip]
const T11C: [u16; 64] = [
3, 4, 10, 24, 34, 33, 21, 15,
5, 3, 4, 10, 32, 17, 11, 10,
11, 7, 13, 18, 30, 31, 20, 5,
25, 11, 19, 59, 27, 18, 12, 5,
35, 33, 31, 58, 30, 16, 7, 5,
28, 26, 32, 19, 17, 15, 8, 14,
14, 12, 9, 13, 14, 9, 4, 1,
11, 4, 6, 6, 6, 3, 2, 0,
];
#[rustfmt::skip]
const T11L: [u8; 64] = [
2, 3, 5, 7, 8, 9, 8, 9,
3, 3, 4, 6, 8, 8, 7, 8,
5, 5, 6, 7, 8, 9, 8, 8,
7, 6, 7, 9, 8, 10, 8, 9,
8, 8, 8, 9, 9, 10, 9, 10,
8, 8, 9, 10, 10, 11, 10, 11,
8, 7, 7, 8, 9, 10, 10, 10,
8, 7, 8, 9, 10, 10, 10, 10,
];
#[rustfmt::skip]
const T12C: [u16; 64] = [
9, 6, 16, 33, 41, 39, 38, 26,
7, 5, 6, 9, 23, 16, 26, 11,
17, 7, 11, 14, 21, 30, 10, 7,
17, 10, 15, 12, 18, 28, 14, 5,
32, 13, 22, 19, 18, 16, 9, 5,
40, 17, 31, 29, 17, 13, 4, 2,
27, 12, 11, 15, 10, 7, 4, 1,
27, 12, 8, 12, 6, 3, 1, 0,
];
#[rustfmt::skip]
const T12L: [u8; 64] = [
4, 3, 5, 7, 8, 9, 9, 9,
3, 3, 4, 5, 7, 7, 8, 8,
5, 4, 5, 6, 7, 8, 7, 8,
6, 5, 6, 6, 7, 8, 8, 8,
7, 6, 7, 7, 8, 8, 8, 9,
8, 7, 8, 8, 8, 9, 8, 9,
8, 7, 7, 8, 8, 9, 9, 10,
9, 8, 8, 9, 9, 9, 9, 10,
];
#[rustfmt::skip]
const T13C: [u16; 256] = [
1, 5, 14, 21, 34, 51, 46, 71, 42, 52, 68, 52, 67, 44, 43, 19,
3, 4, 12, 19, 31, 26, 44, 33, 31, 24, 32, 24, 31, 35, 22, 14,
15, 13, 23, 36, 59, 49, 77, 65, 29, 40, 30, 40, 27, 33, 42, 16,
22, 20, 37, 61, 56, 79, 73, 64, 43, 76, 56, 37, 26, 31, 25, 14,
35, 16, 60, 57, 97, 75, 114, 91, 54, 73, 55, 41, 48, 53, 23, 24,
58, 27, 50, 96, 76, 70, 93, 84, 77, 58, 79, 29, 74, 49, 41, 17,
47, 45, 78, 74, 115, 94, 90, 79, 69, 83, 71, 50, 59, 38, 36, 15,
72, 34, 56, 95, 92, 85, 91, 90, 86, 73, 77, 65, 51, 44, 43, 42,
43, 20, 30, 44, 55, 78, 72, 87, 78, 61, 46, 54, 37, 30, 20, 16,
53, 25, 41, 37, 44, 59, 54, 81, 66, 76, 57, 54, 37, 18, 39, 11,
35, 33, 31, 57, 42, 82, 72, 80, 47, 58, 55, 21, 22, 26, 38, 22,
53, 25, 23, 38, 70, 60, 51, 36, 55, 26, 34, 23, 27, 14, 9, 7,
34, 32, 28, 39, 49, 75, 30, 52, 48, 40, 52, 28, 18, 17, 9, 5,
45, 21, 34, 64, 56, 50, 49, 45, 31, 19, 12, 15, 10, 7, 6, 3,
48, 23, 20, 39, 36, 35, 53, 21, 16, 23, 13, 10, 6, 1, 4, 2,
16, 15, 17, 27, 25, 20, 29, 11, 17, 12, 16, 8, 1, 1, 0, 1,
];
#[rustfmt::skip]
const T13L: [u8; 256] = [
1, 4, 6, 7, 8, 9, 9, 10, 9, 10, 11, 11, 12, 12, 13, 13,
3, 4, 6, 7, 8, 8, 9, 9, 9, 9, 10, 10, 11, 12, 12, 12,
6, 6, 7, 8, 9, 9, 10, 10, 9, 10, 10, 11, 11, 12, 13, 13,
7, 7, 8, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 13, 13,
8, 7, 9, 9, 10, 10, 11, 11, 10, 11, 11, 12, 12, 13, 13, 14,
9, 8, 9, 10, 10, 10, 11, 11, 11, 11, 12, 11, 13, 13, 14, 14,
9, 9, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 13, 13, 14, 14,
10, 9, 10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 16, 16,
9, 8, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13, 13, 14, 15, 15,
10, 9, 10, 10, 11, 11, 11, 13, 12, 13, 13, 14, 14, 14, 16, 15,
10, 10, 10, 11, 11, 12, 12, 13, 12, 13, 14, 13, 14, 15, 16, 17,
11, 10, 10, 11, 12, 12, 12, 12, 13, 13, 13, 14, 15, 15, 15, 16,
11, 11, 11, 12, 12, 13, 12, 13, 14, 14, 15, 15, 15, 16, 16, 16,
12, 11, 12, 13, 13, 13, 14, 14, 14, 14, 14, 15, 16, 15, 16, 16,
13, 12, 12, 13, 13, 13, 15, 14, 14, 17, 15, 15, 15, 17, 16, 16,
12, 12, 13, 14, 14, 14, 15, 14, 15, 15, 16, 16, 19, 18, 19, 16,
];
#[rustfmt::skip]
const T15C: [u16; 256] = [
7, 12, 18, 53, 47, 76, 124, 108, 89, 123, 108, 119, 107, 81, 122, 63,
13, 5, 16, 27, 46, 36, 61, 51, 42, 70, 52, 83, 65, 41, 59, 36,
19, 17, 15, 24, 41, 34, 59, 48, 40, 64, 50, 78, 62, 80, 56, 33,
29, 28, 25, 43, 39, 63, 55, 93, 76, 59, 93, 72, 54, 75, 50, 29,
52, 22, 42, 40, 67, 57, 95, 79, 72, 57, 89, 69, 49, 66, 46, 27,
77, 37, 35, 66, 58, 52, 91, 74, 62, 48, 79, 63, 90, 62, 40, 38,
125, 32, 60, 56, 50, 92, 78, 65, 55, 87, 71, 51, 73, 51, 70, 30,
109, 53, 49, 94, 88, 75, 66, 122, 91, 73, 56, 42, 64, 44, 21, 25,
90, 43, 41, 77, 73, 63, 56, 92, 77, 66, 47, 67, 48, 53, 36, 20,
71, 34, 67, 60, 58, 49, 88, 76, 67, 106, 71, 54, 38, 39, 23, 15,
109, 53, 51, 47, 90, 82, 58, 57, 48, 72, 57, 41, 23, 27, 62, 9,
86, 42, 40, 37, 70, 64, 52, 43, 70, 55, 42, 25, 29, 18, 11, 11,
118, 68, 30, 55, 50, 46, 74, 65, 49, 39, 24, 16, 22, 13, 14, 7,
91, 44, 39, 38, 34, 63, 52, 45, 31, 52, 28, 19, 14, 8, 9, 3,
123, 60, 58, 53, 47, 43, 32, 22, 37, 24, 17, 12, 15, 10, 2, 1,
71, 37, 34, 30, 28, 20, 17, 26, 21, 16, 10, 6, 8, 6, 2, 0,
];
#[rustfmt::skip]
const T15L: [u8; 256] = [
3, 4, 5, 7, 7, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12, 13,
4, 3, 5, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11,
5, 5, 5, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 11, 11, 11,
6, 6, 6, 7, 7, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11,
7, 6, 7, 7, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11,
8, 7, 7, 8, 8, 8, 9, 9, 9, 9, 10, 10, 11, 11, 11, 12,
9, 7, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12,
9, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 12,
9, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 12, 12, 12,
9, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12,
10, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, 12, 13, 12,
10, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 13,
11, 10, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 13, 13,
11, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13,
12, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 12, 13,
12, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13,
];
#[rustfmt::skip]
const T16C: [u16; 256] = [
1, 5, 14, 44, 74, 63, 110, 93, 172, 149, 138, 242, 225, 195, 376, 17,
3, 4, 12, 20, 35, 62, 53, 47, 83, 75, 68, 119, 201, 107, 207, 9,
15, 13, 23, 38, 67, 58, 103, 90, 161, 72, 127, 117, 110, 209, 206, 16,
45, 21, 39, 69, 64, 114, 99, 87, 158, 140, 252, 212, 199, 387, 365, 26,
75, 36, 68, 65, 115, 101, 179, 164, 155, 264, 246, 226, 395, 382, 362, 9,
66, 30, 59, 56, 102, 185, 173, 265, 142, 253, 232, 400, 388, 378, 445, 16,
111, 54, 52, 100, 184, 178, 160, 133, 257, 244, 228, 217, 385, 366, 715, 10,
98, 48, 91, 88, 165, 157, 148, 261, 248, 407, 397, 372, 380, 889, 884, 8,
85, 84, 81, 159, 156, 143, 260, 249, 427, 401, 392, 383, 727, 713, 708, 7,
154, 76, 73, 141, 131, 256, 245, 426, 406, 394, 384, 735, 359, 710, 352, 11,
139, 129, 67, 125, 247, 233, 229, 219, 393, 743, 737, 720, 885, 882, 439, 4,
243, 120, 118, 115, 227, 223, 396, 746, 742, 736, 721, 712, 706, 223, 436, 6,
202, 224, 222, 218, 216, 389, 386, 381, 364, 888, 443, 707, 440, 437, 1728, 4,
747, 211, 210, 208, 370, 379, 734, 723, 714, 1735, 883, 877, 876, 3459, 865, 2,
377, 369, 102, 187, 726, 722, 358, 711, 709, 866, 1734, 871, 3458, 870, 434, 0,
12, 10, 7, 11, 10, 17, 11, 9, 13, 12, 10, 7, 5, 3, 1, 3,
];
#[rustfmt::skip]
const T16L: [u8; 256] = [
1, 4, 6, 8, 9, 9, 10, 10, 11, 11, 11, 12, 12, 12, 13, 9,
3, 4, 6, 7, 8, 9, 9, 9, 10, 10, 10, 11, 12, 11, 12, 8,
6, 6, 7, 8, 9, 9, 10, 10, 11, 10, 11, 11, 11, 12, 12, 9,
8, 7, 8, 9, 9, 10, 10, 10, 11, 11, 12, 12, 12, 13, 13, 10,
9, 8, 9, 9, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 9,
9, 8, 9, 9, 10, 11, 11, 12, 11, 12, 12, 13, 13, 13, 14, 10,
10, 9, 9, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 14, 10,
10, 9, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 13, 15, 15, 10,
10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 13, 14, 14, 14, 10,
11, 10, 10, 11, 11, 12, 12, 13, 13, 13, 13, 14, 13, 14, 13, 11,
11, 11, 10, 11, 12, 12, 12, 12, 13, 14, 14, 14, 15, 15, 14, 10,
12, 11, 11, 11, 12, 12, 13, 14, 14, 14, 14, 14, 14, 13, 14, 11,
12, 12, 12, 12, 12, 13, 13, 13, 13, 15, 14, 14, 14, 14, 16, 11,
14, 12, 12, 12, 13, 13, 14, 14, 14, 16, 15, 15, 15, 17, 15, 11,
13, 13, 11, 12, 14, 14, 13, 14, 14, 15, 16, 15, 17, 15, 14, 11,
9, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 8,
];
#[rustfmt::skip]
const T24C: [u16; 256] = [
15, 13, 46, 80, 146, 262, 248, 434, 426, 669, 653, 649, 621, 517, 1032, 88,
14, 12, 21, 38, 71, 130, 122, 216, 209, 198, 327, 345, 319, 297, 279, 42,
47, 22, 41, 74, 68, 128, 120, 221, 207, 194, 182, 340, 315, 295, 541, 18,
81, 39, 75, 70, 134, 125, 116, 220, 204, 190, 178, 325, 311, 293, 271, 16,
147, 72, 69, 135, 127, 118, 112, 210, 200, 188, 352, 323, 306, 285, 540, 14,
263, 66, 129, 126, 119, 114, 214, 202, 192, 180, 341, 317, 301, 281, 262, 12,
249, 123, 121, 117, 113, 215, 206, 195, 185, 347, 330, 308, 291, 272, 520, 10,
435, 115, 111, 109, 211, 203, 196, 187, 353, 332, 313, 298, 283, 531, 381, 17,
427, 212, 208, 205, 201, 193, 186, 177, 169, 320, 303, 286, 268, 514, 377, 16,
335, 199, 197, 191, 189, 181, 174, 333, 321, 305, 289, 275, 521, 379, 371, 11,
668, 184, 183, 179, 175, 344, 331, 314, 304, 290, 277, 530, 383, 373, 366, 10,
652, 346, 171, 168, 164, 318, 309, 299, 287, 276, 263, 513, 375, 368, 362, 6,
648, 322, 316, 312, 307, 302, 292, 284, 269, 261, 512, 376, 370, 364, 359, 4,
620, 300, 296, 294, 288, 282, 273, 266, 515, 380, 374, 369, 365, 361, 357, 2,
1033, 280, 278, 274, 267, 264, 259, 382, 378, 372, 367, 363, 360, 358, 356, 0,
43, 20, 19, 17, 15, 13, 11, 9, 7, 6, 4, 7, 5, 3, 1, 3,
];
#[rustfmt::skip]
const T24L: [u8; 256] = [
4, 4, 6, 7, 8, 9, 9, 10, 10, 11, 11, 11, 11, 11, 12, 9,
4, 4, 5, 6, 7, 8, 8, 9, 9, 9, 10, 10, 10, 10, 10, 8,
6, 5, 6, 7, 7, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 7,
7, 6, 7, 7, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 7,
8, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11, 7,
9, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 7,
9, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 7,
10, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 8,
10, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 8,
10, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 8,
11, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 8,
11, 10, 9, 9, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 8,
11, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 8,
11, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 8,
12, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 8,
8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 4,
];
/// The pair tables by `table_select` (0-31); `None` for table 0 (all zeros) and the unused 4 and 14.
pub(crate) fn pair_table(select: usize) -> Option<PairTable> {
let (size, codes, lens): (usize, &'static [u16], &'static [u8]) = match select {
1 => (2, &T1C, &T1L),
2 => (3, &T2C, &T2L),
3 => (3, &T3C, &T3L),
5 => (4, &T5C, &T5L),
6 => (4, &T6C, &T6L),
7 => (6, &T7C, &T7L),
8 => (6, &T8C, &T8L),
9 => (6, &T9C, &T9L),
10 => (8, &T10C, &T10L),
11 => (8, &T11C, &T11L),
12 => (8, &T12C, &T12L),
13 => (16, &T13C, &T13L),
15 => (16, &T15C, &T15L),
16..=23 => (16, &T16C, &T16L),
24..=31 => (16, &T24C, &T24L),
_ => return None,
};
Some(PairTable { size, codes, lens })
}
/// `linbits` of each `table_select`.
pub(crate) const LINBITS: [u32; 32] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 6, 8, 10, 13, 4, 5, 6, 7, 8, 9, 11, 13];
/// Count1 table A (`count1table_select` 0): codes and lengths of the quadruple `8v + 4w + 2x + y`.
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.
pub(crate) const QUAD_B_CODES: [u8; 16] = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
+502
View File
@@ -0,0 +1,502 @@
//! Clean-room MPEG-1 / MPEG-2 Audio Layer III (MP3) 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).
//!
//! - Constant bit rate: 32-320 kbit/s at 32 / 44.1 / 48 kHz (MPEG-1, two granules per frame) and
//! 8-160 kbit/s at 16 / 22.05 / 24 kHz (MPEG-2 LSF, one granule per frame).
//! - Mono, stereo, or joint stereo with frame-by-frame mid/side coding.
//! - The polyphase analysis filterbank of Annex C.1.3, the normal-block MDCT and alias reduction;
//! long blocks only (no block switching yet).
//! - A simple psychoacoustic model (spread band energies against the threshold in quiet), the
//! inner / outer quantization loops with scalefactors, Huffman table and region selection, and
//! the bit reservoir (`main_data_begin`).
//! - 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)]
mod filterbank;
mod huffman;
mod huffman_tables;
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;
+370
View File
@@ -0,0 +1,370 @@
//! A simple psychoacoustic model and the quantization loops (after ISO/IEC 11172-3 Annex C.1.5.4).
//!
//! The model gives every scalefactor band an allowed noise energy: the band energies spread to
//! their neighbours (10 dB per band upwards, 25 dB per band downwards) lowered by a fixed
//! signal-to-mask offset, but never below the absolute threshold of hearing. The inner loop picks
//! the finest global gain whose Huffman code fits the granule's bit budget; the outer loop
//! amplifies (raises the scalefactors of) the bands whose quantization noise exceeds the allowed
//! noise and keeps the result with the lowest worst noise-to-mask ratio. When every band is under
//! its threshold and the bit reservoir can take the savings, the global gain is coarsened again
//! as far as the thresholds allow.
use craft_media::bitstream::BitWriter;
use super::filterbank::GRANULE;
use super::huffman::{self, Coding};
use super::tables::{LSF_PARTITIONS, MAX_QUANT, SLEN, pow43};
/// Outer-loop iterations per granule (each raises at least one scalefactor).
const MAX_OUTER: usize = 24;
/// Signal-to-mask offset of the spread band energies, dB.
const MASK_OFFSET_DB: f32 = 12.0;
/// The fixed per-stream parameters of the model and the loops.
pub(crate) struct Model {
pub lsf: bool,
pub bands: [u16; 23],
/// Allowed noise energy from the threshold in quiet, per band.
pub ath: [f32; 22],
/// Lines at and above this are not coded (the low-pass).
pub cutoff: usize,
/// `spread[b][j]`: how much of band `j`'s energy masks band `b`.
spread: [[f32; 22]; 22],
}
/// One coded granule of one channel.
#[derive(Clone)]
pub(crate) struct Granule {
pub ix: [u16; GRANULE],
pub negative: [bool; GRANULE],
pub global_gain: u32,
pub scalefac: [u8; 22],
pub scalefac_scale: bool,
pub scalefac_compress: u32,
/// Bits per scalefactor of each band group (MPEG-1: bands 0-10, 11-20; MPEG-2: four partitions).
pub slen: [u32; 4],
pub part2_bits: usize,
pub coding: Coding,
}
impl Granule {
fn silent() -> Granule {
Granule {
ix: [0; GRANULE],
negative: [false; GRANULE],
global_gain: 210,
scalefac: [0; 22],
scalefac_scale: false,
scalefac_compress: 0,
slen: [0; 4],
part2_bits: 0,
coding: Coding::default(),
}
}
/// `part2_3_length`: scalefactor and Huffman bits.
pub fn bits(&self) -> usize {
self.part2_bits + self.coding.bits
}
/// Write the scalefactors (no scfsi reuse) and the Huffman code.
pub fn write(&self, w: &mut BitWriter, model: &Model) {
let groups: [(usize, usize, u32); 4] = if model.lsf {
let mut start = 0;
let mut g = [(0, 0, 0); 4];
for (p, (&n, gi)) in LSF_PARTITIONS.iter().zip(g.iter_mut()).enumerate() {
*gi = (start, start + n, self.slen[p]);
start += n;
}
g
} else {
[(0, 11, self.slen[0]), (11, 21, self.slen[1]), (21, 21, 0), (21, 21, 0)]
};
for (a, b, bits) in groups {
for &sf in self.scalefac.get(a..b).unwrap_or(&[]) {
w.write_bits(u32::from(sf), bits);
}
}
huffman::write(w, &self.ix, &self.negative, &self.coding, &model.bands);
}
}
/// Bits needed for `v` (0 for 0).
fn bit_width(v: u32) -> u32 {
32 - v.leading_zeros()
}
impl Model {
pub fn new(lsf: bool, bands: [u16; 23], ath: [f32; 22], cutoff: usize) -> Model {
let mut spread = [[0f32; 22]; 22];
for (b, row) in spread.iter_mut().enumerate() {
for (j, s) in row.iter_mut().enumerate() {
let db = if b >= j { (b - j) as f32 * 10.0 } else { (j - b) as f32 * 25.0 };
*s = 10f32.powf(-(db + MASK_OFFSET_DB) / 10.0);
}
}
Model { lsf, bands, ath, cutoff: cutoff.min(GRANULE), spread }
}
fn band(&self, b: usize) -> std::ops::Range<usize> {
let lo = self.bands.get(b).map_or(GRANULE, |&v| usize::from(v));
let hi = self.bands.get(b + 1).map_or(GRANULE, |&v| usize::from(v));
lo..hi.max(lo)
}
/// The largest scalefactor of band `b` the side information can carry.
fn sf_limit(&self, b: usize) -> u8 {
// MPEG-1 slen1 / MPEG-2 slen1-2 reach 4 bits, slen2 / slen3-4 3 bits; band 21 has none
match b {
0..=10 => 15,
11..=20 => 7,
_ => 0,
}
}
/// Allowed noise energy per band for the lines `xr` (zero above the cutoff).
pub fn thresholds(&self, xr: &[f32; GRANULE]) -> [f32; 22] {
let mut en = [0f32; 22];
for (b, e) in en.iter_mut().enumerate() {
*e = xr.get(self.band(b)).unwrap_or(&[]).iter().map(|v| v * v).sum();
}
let mut xmin = [0f32; 22];
for (b, x) in xmin.iter_mut().enumerate() {
let r = self.band(b);
if r.start >= self.cutoff {
*x = f32::MAX;
continue;
}
let mask: f32 = self.spread[b].iter().zip(&en).map(|(s, e)| s * e).sum();
*x = mask.max(self.ath[b]);
}
xmin
}
/// Scalefactor bits and `scalefac_compress` (and the slen per group) for `sf`.
fn part2(&self, sf: &[u8; 22]) -> (usize, u32, [u32; 4]) {
let max_in = |a: usize, b: usize| bit_width(u32::from(sf.get(a..b).and_then(|s| s.iter().copied().max()).unwrap_or(0)));
if self.lsf {
let mut slen = [0u32; 4];
let mut start = 0;
let mut bits = 0usize;
for (p, &n) in LSF_PARTITIONS.iter().enumerate() {
slen[p] = max_in(start, start + n);
bits += n * slen[p] as usize;
start += n;
}
// scalefac_compress < 400: slen1, slen2 ≤ 4; slen3, slen4 ≤ 3 (13818-3 §2.4.3.2)
let sfc = ((slen[0] * 5 + slen[1]) << 4) + (slen[2] << 2) + slen[3];
(bits, sfc, slen)
} else {
let (n1, n2) = (max_in(0, 11), max_in(11, 21));
let mut best = (usize::MAX, 15u32, [4, 3, 0, 0]);
for (i, &(s1, s2)) in SLEN.iter().enumerate() {
let bits = 11 * s1 as usize + 10 * s2 as usize;
if s1 >= n1 && s2 >= n2 && bits < best.0 {
best = (bits, i as u32, [s1, s2, 0, 0]);
}
}
best
}
}
/// Code one granule's lines `xr` with at most `max_bits` and, when the thresholds `xmin` are
/// met, preferably no fewer than `min_bits`.
pub fn encode(&self, xr: &[f32; GRANULE], xmin: &[f32; 22], max_bits: usize, min_bits: usize) -> Granule {
let mut q = Quantizer { model: self, xr, xr34: [0.0; GRANULE] };
let mut peak = 0f32;
for (o, v) in q.xr34.iter_mut().zip(xr.iter()).take(self.cutoff) {
let a = v.abs();
*o = a.sqrt() * a.sqrt().sqrt();
peak = peak.max(a);
}
let mut silent = Granule::silent();
if peak <= 1e-9 || max_bits == 0 {
return silent;
}
silent.negative.iter_mut().zip(xr.iter()).for_each(|(n, v)| *n = *v < 0.0);
let mut sf = [0u8; 22];
let mut scale = false;
let mut hint: Option<u32> = None;
let mut best: Option<(f32, Granule)> = None;
for _ in 0..MAX_OUTER {
let (p2, sfc, slen) = self.part2(&sf);
if p2 >= max_bits {
break;
}
let Some((gg, ix, coding)) = q.inner(&sf, scale, max_bits - p2, hint) else { break };
hint = Some(gg);
let nmr = q.nmr(&ix, gg, &sf, scale, xmin);
let worst = nmr.iter().copied().fold(0f32, f32::max);
if best.as_ref().is_none_or(|b| worst < b.0) {
let mut g = silent.clone();
g.ix = ix;
g.global_gain = gg;
g.scalefac = sf;
g.scalefac_scale = scale;
g.scalefac_compress = sfc;
g.slen = slen;
g.part2_bits = p2;
g.coding = coding;
best = Some((worst, g));
}
// amplify the bands over their threshold
let over: Vec<usize> = (0..21).filter(|&b| nmr[b] > 1.0).collect();
if over.is_empty() || over.len() == 21 {
break;
}
for &b in &over {
sf[b] = sf[b].saturating_add(1);
}
if (0..21).any(|b| sf[b] > self.sf_limit(b)) {
if scale {
break;
}
scale = true;
sf.iter_mut().for_each(|s| *s = s.div_ceil(2));
}
}
let Some((worst, mut g)) = best else { return silent };
if worst <= 1.0 && g.bits() > min_bits {
q.coarsen(&mut g, xmin, min_bits);
}
g
}
}
struct Quantizer<'a> {
model: &'a Model,
xr: &'a [f32; GRANULE],
/// `|xr|^(3/4)`.
xr34: [f32; GRANULE],
}
impl Quantizer<'_> {
/// Quantize with global gain `gg` and the scalefactors; `None` when a line exceeds the
/// largest escape value.
fn quantize(&self, gg: u32, sf: &[u8; 22], scale: bool) -> Option<[u16; GRANULE]> {
let mut ix = [0u16; GRANULE];
let sfm = if scale { 1.0 } else { 0.5 };
for b in 0..22 {
let r = self.model.band(b);
let e = -0.1875 * (gg as f32 - 210.0) + 0.75 * sfm * f32::from(sf[b]);
let f = 2f32.powf(e);
for i in r.start..r.end.min(self.model.cutoff) {
let v = self.xr34[i] * f + 0.4054;
if v.is_nan() || v >= (MAX_QUANT + 1) as f32 {
return None;
}
ix[i] = v as u16;
}
}
Some(ix)
}
/// Noise-to-mask ratio per band.
fn nmr(&self, ix: &[u16; GRANULE], gg: u32, sf: &[u8; 22], scale: bool, xmin: &[f32; 22]) -> [f32; 22] {
let p43 = pow43();
let sfm = if scale { 1.0 } else { 0.5 };
let mut out = [0f32; 22];
for (b, o) in out.iter_mut().enumerate() {
let r = self.model.band(b);
let step = 2f32.powf(0.25 * (gg as f32 - 210.0) - sfm * f32::from(sf[b]));
let mut noise = 0f32;
for i in r.start..r.end.min(self.model.cutoff) {
let rec = p43.get(usize::from(ix[i])).copied().unwrap_or(0.0) * step;
let d = self.xr[i].abs() - rec;
noise += d * d;
}
*o = if xmin[b] > 0.0 {
noise / xmin[b]
} else if noise > 0.0 {
f32::MAX
} else {
0.0
};
}
out
}
/// The code of `gg` if it fits `budget` bits.
fn fits(&self, gg: u32, sf: &[u8; 22], scale: bool, budget: usize) -> Option<([u16; GRANULE], Coding)> {
let ix = self.quantize(gg, sf, scale)?;
let c = huffman::choose(&ix, &self.model.bands);
(c.bits <= budget).then_some((ix, c))
}
/// The inner loop: the smallest global gain whose code fits `budget` bits.
fn inner(&self, sf: &[u8; 22], scale: bool, budget: usize, hint: Option<u32>) -> Option<(u32, [u16; GRANULE], Coding)> {
match hint {
None => {
let mut hi = 255u32;
let mut found = self.fits(hi, sf, scale, budget)?;
let mut lo = 0u32;
while lo < hi {
let mid = (lo + hi) / 2;
match self.fits(mid, sf, scale, budget) {
Some(r) => {
hi = mid;
found = r;
}
None => lo = mid + 1,
}
}
Some((hi, found.0, found.1))
}
Some(start) => {
let mut g = start.min(255);
if let Some(mut found) = self.fits(g, sf, scale, budget) {
while g > 0 {
match self.fits(g - 1, sf, scale, budget) {
Some(r) => {
g -= 1;
found = r;
}
None => break,
}
}
Some((g, found.0, found.1))
} else {
while g < 255 {
g += 1;
if let Some(r) = self.fits(g, sf, scale, budget) {
return Some((g, r.0, r.1));
}
}
None
}
}
}
}
/// Raise the global gain of `g` as far as every band stays under its threshold and the code
/// keeps at least `min_bits`.
fn coarsen(&self, g: &mut Granule, xmin: &[f32; 22], min_bits: usize) {
let ok = |gg: u32| -> Option<([u16; GRANULE], Coding)> {
let ix = self.quantize(gg, &g.scalefac, g.scalefac_scale)?;
let c = huffman::choose(&ix, &self.model.bands);
let nmr = self.nmr(&ix, gg, &g.scalefac, g.scalefac_scale, xmin);
(nmr.iter().all(|&v| v <= 1.0) && g.part2_bits + c.bits >= min_bits).then_some((ix, c))
};
let (mut lo, mut hi) = (g.global_gain, 255u32);
let mut found = None;
while lo < hi {
let mid = (lo + hi).div_ceil(2);
match ok(mid) {
Some(r) => {
lo = mid;
found = Some(r);
}
None => hi = mid - 1,
}
}
if let Some((ix, c)) = found
&& lo > g.global_gain
{
g.global_gain = lo;
g.ix = ix;
g.coding = c;
}
}
}
+125
View File
@@ -0,0 +1,125 @@
//! Tables from ISO/IEC 11172-3 (MPEG-1) and ISO/IEC 13818-3 (MPEG-2 lower sampling frequencies).
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.
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).
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.
pub(crate) const RATES_MPEG1: [u32; 3] = [44_100, 48_000, 32_000];
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
/// `sampling_frequency`: 22 bands over the 576 frequency lines of a granule.
pub(crate) const SFB_MPEG1: [[u16; 23]; 3] = [
[0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 52, 62, 74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576],
[0, 4, 8, 12, 16, 20, 24, 30, 36, 42, 50, 60, 72, 88, 106, 128, 156, 190, 230, 276, 330, 384, 576],
[0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 54, 66, 82, 102, 126, 156, 194, 240, 296, 364, 448, 550, 576],
];
pub(crate) const SFB_LSF: [[u16; 23]; 3] = [
[0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576],
[0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 114, 136, 162, 194, 232, 278, 332, 394, 464, 540, 576],
[0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576],
];
/// `slen1` (bands 0-10) and `slen2` (bands 11-20) by MPEG-1 `scalefac_compress` (§2.4.2.7).
pub(crate) const SLEN: [(u32, u32); 16] =
[(0, 0), (0, 1), (0, 2), (0, 3), (3, 0), (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3), (4, 2), (4, 3)];
/// 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]`).
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`.
const WINDOW_HALF: [i32; 257] = [
0, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -3, -3, -4, -4, -5, //
-5, -6, -7, -7, -8, -9, -10, -11, -13, -14, -16, -17, -19, -21, -24, -26, //
-29, -31, -35, -38, -41, -45, -49, -53, -58, -63, -68, -73, -79, -85, -91, -97, //
-104, -111, -117, -125, -132, -139, -147, -154, -161, -169, -176, -183, -190, -196, -202, -208, //
213, 218, 222, 225, 227, 228, 228, 227, 224, 221, 215, 208, 200, 189, 177, 163, //
146, 127, 106, 83, 57, 29, -2, -36, -72, -111, -153, -197, -244, -294, -347, -401, //
-459, -519, -581, -645, -711, -779, -848, -919, -991, -1064, -1137, -1210, -1283, -1356, -1428, -1498, //
-1567, -1634, -1698, -1759, -1817, -1870, -1919, -1962, -2001, -2032, -2057, -2075, -2085, -2087, -2080, -2063, //
2037, 2000, 1952, 1893, 1822, 1739, 1644, 1535, 1414, 1280, 1131, 970, 794, 605, 402, 185, //
-45, -288, -545, -814, -1095, -1388, -1692, -2006, -2330, -2663, -3004, -3351, -3705, -4063, -4425, -4788, //
-5153, -5517, -5879, -6237, -6589, -6935, -7271, -7597, -7910, -8209, -8491, -8755, -8998, -9219, -9416, -9585, //
-9727, -9838, -9916, -9959, -9966, -9935, -9863, -9750, -9592, -9389, -9139, -8840, -8492, -8092, -7640, -7134, //
6574, 5959, 5288, 4561, 3776, 2935, 2037, 1082, 70, -998, -2122, -3300, -4533, -5818, -7154, -8540, //
-9975, -11455, -12980, -14548, -16155, -17799, -19478, -21189, -22929, -24694, -26482, -28289, -30112, -31947, -33791, -35640, //
-37489, -39336, -41176, -43006, -44821, -46617, -48390, -50137, -51853, -53534, -55178, -56778, -58333, -59838, -61289, -62684, //
-64019, -65290, -66494, -67629, -68692, -69679, -70590, -71420, -72169, -72835, -73415, -73908, -74313, -74630, -74856, -74992, //
75038,
];
/// The 512 synthesis window coefficients `D[i]` (Table 3-B.3). The table stores the symmetric
/// prototype `h[i] = h[512 - i]` with every other block of 64 negated, so for `i > 256`
/// `D[i] = -D[512 - i]`, except at multiples of 64 where both halves share the block parity.
pub(crate) fn synthesis_window() -> &'static [f32; 512] {
static W: OnceLock<[f32; 512]> = OnceLock::new();
W.get_or_init(|| {
let mut w = [0f32; 512];
for (i, d) in w.iter_mut().enumerate() {
let (j, sign) = if i <= 256 { (i, 1.0) } else { (512 - i, if i % 64 == 0 { 1.0 } else { -1.0 }) };
*d = sign * WINDOW_HALF.get(j).copied().unwrap_or(0) as f32 / 65_536.0;
}
w
})
}
/// The analysis window `C[i]` (Table 3-C.1): `D[i] / 32`.
pub(crate) fn analysis_window() -> &'static [f32; 512] {
static C: OnceLock<[f32; 512]> = OnceLock::new();
C.get_or_init(|| {
let mut c = [0f32; 512];
c.iter_mut().zip(synthesis_window()).for_each(|(c, d)| *c = d / 32.0);
c
})
}
/// 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`.
pub(crate) fn analysis_matrix() -> &'static [[f32; 64]; 32] {
static M: OnceLock<[[f32; 64]; 32]> = OnceLock::new();
M.get_or_init(|| {
let mut m = [[0f32; 64]; 32];
for (i, row) in m.iter_mut().enumerate() {
for (k, c) in row.iter_mut().enumerate() {
*c = ((2 * i + 1) as f64 * (k as f64 - 16.0) * std::f64::consts::PI / 64.0).cos() as f32;
}
}
m
})
}
/// Alias-reduction butterfly coefficients `(cs[i], ca[i])` from `c[i]` (Table 3-B.9).
pub(crate) fn alias_coefficients() -> &'static [(f32, f32); 8] {
static A: OnceLock<[(f32, f32); 8]> = OnceLock::new();
A.get_or_init(|| {
const C: [f64; 8] = [-0.6, -0.535, -0.33, -0.185, -0.095, -0.041, -0.0142, -0.0037];
let mut a = [(0f32, 0f32); 8];
for (o, c) in a.iter_mut().zip(C) {
let n = (1.0 + c * c).sqrt();
*o = ((1.0 / n) as f32, (c / n) as f32);
}
a
})
}
/// `|i|^(4/3)` for every quantized magnitude a big-values escape can carry (`15 + 2^13 - 1`).
pub(crate) const MAX_QUANT: usize = 8206;
pub(crate) fn pow43() -> &'static [f32] {
static P: OnceLock<Vec<f32>> = OnceLock::new();
P.get_or_init(|| (0..=MAX_QUANT).map(|i| (i as f64).powf(4.0 / 3.0) as f32).collect())
}
/// The absolute threshold of hearing in dB SPL at `f` Hz (Terhardt's approximation, as in the
/// psychoacoustic models of 11172-3 Annex D).
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)
}
+490
View File
@@ -0,0 +1,490 @@
use super::filterbank::Analysis;
use super::tables::{alias_coefficients, synthesis_window};
use super::*;
/// 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
/// analysis side.
struct Synth {
overlap: [[f32; 18]; 32],
v: [f32; 1024],
}
impl Synth {
fn new() -> Synth {
Synth { overlap: [[0.0; 18]; 32], v: [0.0; 1024] }
}
fn granule(&mut self, xr: &[f32; GRANULE]) -> Vec<f32> {
let mut x = *xr;
for sb in 1..32 {
for (i, (cs, ca)) in alias_coefficients().iter().enumerate() {
let (up, dn) = (18 * sb - 1 - i, 18 * sb + i);
let (bu, bd) = (x[up], x[dn]);
x[up] = bu * cs - bd * ca;
x[dn] = bd * cs + bu * ca;
}
}
let mut sub = [[0f32; 18]; 32];
for sb in 0..32 {
let mut y = [0f32; 36];
for (i, yi) in y.iter_mut().enumerate() {
let w = (std::f64::consts::PI / 36.0 * (i as f64 + 0.5)).sin();
let mut acc = 0f64;
for k in 0..18 {
acc += f64::from(x[18 * sb + k]) * (std::f64::consts::PI / 72.0 * (2 * i + 1 + 18) as f64 * (2 * k + 1) as f64).cos();
}
*yi = (acc * w) as f32;
}
for t in 0..18 {
let mut v = y[t] + self.overlap[sb][t];
if sb % 2 == 1 && t % 2 == 1 {
v = -v;
}
sub[sb][t] = v;
self.overlap[sb][t] = y[t + 18];
}
}
let d = synthesis_window();
let mut out = Vec::with_capacity(GRANULE);
for t in 0..18 {
self.v.copy_within(0..960, 64);
for i in 0..64 {
self.v[i] = (0..32).map(|k| (((16 + i) * (2 * k + 1)) as f64 * std::f64::consts::PI / 64.0).cos() as f32 * sub[k][t]).sum();
}
for j in 0..32 {
let mut acc = 0f32;
for i in 0..8 {
acc += self.v[i * 128 + j] * d[i * 64 + j] + self.v[i * 128 + 96 + j] * d[i * 64 + 32 + j];
}
out.push(acc);
}
}
out
}
}
fn noise(n: usize, amp: f32, seed: u64) -> Vec<f32> {
let mut s = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(0..n)
.map(|_| {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((s >> 40) as f32 / (1u64 << 24) as f32 * 2.0 - 1.0) * amp
})
.collect()
}
fn sine(n: usize, rate: f32, hz: f32, amp: f32) -> Vec<f32> {
(0..n).map(|i| amp * (2.0 * std::f32::consts::PI * hz * i as f32 / rate).sin()).collect()
}
fn analyse(pcm: &[f32]) -> Vec<[f32; GRANULE]> {
let mut a = Analysis::new();
pcm.as_chunks::<GRANULE>()
.0
.iter()
.map(|c| {
let mut x = [0f32; GRANULE];
a.granule(c, &mut x);
x
})
.collect()
}
#[test]
fn hybrid_filterbank_reconstructs_with_the_documented_delay() {
let input = noise(GRANULE * 12, 0.5, 3);
let mut s = Synth::new();
let out: Vec<f32> = analyse(&input).iter().flat_map(|x| s.granule(x)).collect();
let d = DELAY as usize;
let err = (0..input.len() - d).map(|i| (out[i + d] - input[i]).abs()).fold(0f32, f32::max);
assert!(err < 2e-4, "max reconstruction error {err} at delay {d}");
}
#[test]
fn full_scale_tone_energy_matches_the_threshold_scale() {
// the MDCT is not shift invariant: average a few granules
let x = analyse(&sine(GRANULE * 10, 44_100.0, 1000.0, 1.0));
let e = x[2..].iter().flatten().map(|v| v * v).sum::<f32>() / 8.0;
assert!((e / FULL_SCALE_TONE_ENERGY - 1.0).abs() < 0.1, "tone energy {e}");
}
/// Frames (MPEG-1, 44.1 kHz mono, 320 kbit/s, no reservoir) carrying the given granules.
fn frames_of(granules: &[Granule]) -> Vec<u8> {
let mut enc = Encoder::new(Config::cbr(44_100, 1, 320)).unwrap();
let mut out = Vec::new();
for pair in granules.chunks(2) {
let mut coded = pair.to_vec();
if coded.len() == 1 {
coded.push(enc.model.encode(&[0.0; GRANULE], &[f32::MAX; 22], 0, 0));
}
let len = enc.next_frame_len();
let slot = len - 4 - enc.side_info_len();
let mut md = BitWriter::new();
coded.iter().for_each(|g| g.write(&mut md, &enc.model));
let mut main = md.finish();
assert!(main.len() <= slot, "{} main-data bytes in a {slot}-byte slot", main.len());
main.resize(slot, 0);
out.extend(enc.header_and_side_info(len, false, 0, &coded));
out.extend(main);
}
out
}
/// A granule coding `pairs` with table `table` in all three regions, then `quads` (magnitudes
/// ≤ 1) with count1 table `c1`; every third nonzero line negative.
fn forced_granule(model: &Model, pairs: &[(u16, u16)], quads: &[[u16; 4]], table: usize, c1: usize, gain: u32) -> Granule {
let mut g = model.encode(&[0.0; GRANULE], &[f32::MAX; 22], 0, 0);
for (k, (x, y)) in pairs.iter().enumerate() {
g.ix[2 * k] = *x;
g.ix[2 * k + 1] = *y;
}
for (k, q) in quads.iter().enumerate() {
g.ix[2 * pairs.len() + 4 * k..2 * pairs.len() + 4 * k + 4].copy_from_slice(q);
}
for (i, n) in g.negative.iter_mut().enumerate() {
*n = i % 3 == 1;
}
g.global_gain = gain;
g.coding = huffman::Coding {
big_values: pairs.len(),
count1: quads.len(),
table_select: [table; 3],
region0_count: 7,
region1_count: 7,
count1_table: c1,
bits: 0,
};
let mut w = BitWriter::new();
huffman::write(&mut w, &g.ix, &g.negative, &g.coding, &model.bands);
g.coding.bits = w.bit_len();
assert!(g.bits() <= 4095, "table {table}: {} bits", g.bits());
g
}
/// What a standard decoder outputs for the granules (mono, no scalefactors).
fn reference_decode(granules: &[Granule]) -> Vec<f32> {
let mut s = Synth::new();
let p43 = super::tables::pow43();
let mut out = Vec::new();
for g in granules {
let step = 2f32.powf(0.25 * (g.global_gain as f32 - 210.0));
let mut xr = [0f32; GRANULE];
for (i, x) in xr.iter_mut().enumerate() {
let v = p43[usize::from(g.ix[i])] * step;
*x = if g.negative[i] { -v } else { v };
}
out.extend(s.granule(&xr));
}
out
}
fn ffmpeg_decode_mono(bytes: &[u8], name: &str) -> Option<(Vec<f32>, String)> {
let ff = craft_media_testkit::ffmpeg_or_skip("mp3 Huffman tables")?;
let path = craft_media_testkit::fixtures_dir("mp3enc").join(name);
std::fs::write(&path, bytes).unwrap();
let out =
std::process::Command::new(ff).args(["-v", "error", "-i"]).arg(&path).args(["-f", "f32le", "-acodec", "pcm_f32le", "-"]).output().unwrap();
let pcm = out.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
Some((pcm, String::from_utf8_lossy(&out.stderr).into_owned()))
}
/// Every code of every pair table (escapes with several linbits values) and of both count1
/// tables, as ffmpeg decodes them, against the reference decode of the intended lines.
#[test]
fn every_huffman_code_decodes_as_intended_in_ffmpeg() {
let enc = Encoder::new(Config::cbr(44_100, 1, 320)).unwrap();
let mut granules = Vec::new();
let mut owners = Vec::new();
for table in (1..32).filter(|t| *t != 4 && *t != 14) {
let size = huffman_tables::pair_table(table).unwrap().size as u16;
let linbits = huffman_tables::LINBITS[table];
// every code, with small escape values so that any decoding error stands out ...
let esc = |v: u16, k: u16, big: bool| match (linbits > 0 && v == 15, big) {
(false, _) => v,
(true, false) => 15 + k % 8 % (1 << linbits),
(true, true) => 15 + ((1u32 << linbits) - 1).saturating_sub(u32::from(k % 3)) as u16,
};
let mut pairs = Vec::new();
for x in 0..size {
for y in 0..size {
pairs.push((esc(x, x + 3 * y, false), esc(y, 7 * x + y, false)));
}
}
for chunk in pairs.chunks(48) {
granules.push(forced_granule(&enc.model, chunk, &[], table, 0, 190));
owners.push(format!("table {table}"));
}
// ... and the largest escapes
if linbits > 0 {
let big: Vec<(u16, u16)> = (0..16).map(|k| (esc(15, k, true), esc(if k % 2 == 0 { 15 } else { k }, k + 1, true))).collect();
granules.push(forced_granule(&enc.model, &big, &[], table, 0, 120));
owners.push(format!("table {table}, large escapes"));
}
}
let quads: Vec<[u16; 4]> = (0..16u16).map(|q| [q >> 3, (q >> 2) & 1, (q >> 1) & 1, q & 1]).collect();
for c1 in 0..2 {
granules.push(forced_granule(&enc.model, &[(1, 2), (0, 1)], &quads, 2, c1, 190));
owners.push(format!("count1 table {}", if c1 == 0 { "A" } else { "B" }));
}
let bytes = frames_of(&granules);
let Some((ff, err)) = ffmpeg_decode_mono(&bytes, "huffman_tables.mp3") else { return };
assert!(err.trim().is_empty(), "ffmpeg: {err}");
let ours = reference_decode(&granules);
assert!(ff.len() >= ours.len(), "{} vs {}", ff.len(), ours.len());
// a wrong code desynchronises the rest of its granule: report the first granule that differs
for (k, owner) in owners.iter().enumerate() {
let r = k * GRANULE..(k + 1) * GRANULE;
let peak = ours[r.clone()].iter().fold(1e-3f32, |m, v| m.max(v.abs()));
let diff = ours[r.clone()].iter().zip(&ff[r]).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
assert!(diff < 1e-5 * peak, "{owner} (granule {k}): differs by {diff} (peak {peak})");
}
}
/// A frame's header and side-information fields we check.
#[derive(Debug)]
struct Parsed {
lsf: bool,
bitrate_index: u32,
rate_index: u32,
padding: bool,
mode: u32,
mode_extension: u32,
len: usize,
main_data_begin: usize,
part2_3: Vec<usize>,
global_gain: Vec<u32>,
}
/// Walk the frames of a stream, checking sync and lengths.
fn parse(bytes: &[u8]) -> Vec<Parsed> {
let mut out = Vec::new();
let mut pos = 0;
while pos < bytes.len() {
let h = u32::from_be_bytes(bytes[pos..pos + 4].try_into().unwrap());
assert_eq!(h >> 21, 0x7FF, "sync at {pos}");
let lsf = (h >> 19) & 3 == 2;
assert!(lsf || (h >> 19) & 3 == 3, "MPEG-1 or MPEG-2");
assert_eq!((h >> 17) & 3, 1, "Layer III");
assert_eq!((h >> 16) & 1, 1, "no CRC");
let bitrate_index = (h >> 12) & 15;
let rate_index = (h >> 10) & 3;
let padding = (h >> 9) & 1 == 1;
let mode = (h >> 6) & 3;
let kbps = if lsf { tables::BITRATES_LSF } else { tables::BITRATES_MPEG1 }[bitrate_index as usize - 1];
let rate = if lsf { tables::RATES_LSF } else { tables::RATES_MPEG1 }[rate_index as usize];
let len = (if lsf { 72_000 } else { 144_000 } * kbps / rate) as usize + usize::from(padding);
let nch = if mode == 3 { 1 } else { 2 };
let mut r = craft_media::bitstream::BitReader::new(&bytes[pos + 4..pos + len]);
let main_data_begin = r.read_bits(if lsf { 8 } else { 9 }).unwrap() as usize;
r.skip(if lsf { nch } else { [5, 3][nch - 1] + 4 * nch }).unwrap();
let (mut part2_3, mut global_gain) = (Vec::new(), Vec::new());
for _ in 0..(if lsf { 1 } else { 2 }) * nch {
part2_3.push(r.read_bits(12).unwrap() as usize);
let big_values = r.read_bits(9).unwrap();
assert!(big_values <= 288);
global_gain.push(r.read_bits(8).unwrap());
r.skip(if lsf { 9 } else { 4 }).unwrap();
assert!(!r.read_flag().unwrap(), "long blocks only");
r.skip(15 + 7 + if lsf { 2 } else { 3 }).unwrap();
}
out.push(Parsed { lsf, bitrate_index, rate_index, padding, mode, mode_extension: (h >> 4) & 3, len, main_data_begin, part2_3, global_gain });
pos += len;
}
assert_eq!(pos, bytes.len(), "whole frames");
out
}
fn encode_all(cfg: Config, input: &[Vec<f32>]) -> Vec<u8> {
let mut enc = Encoder::new(cfg).unwrap();
let planes: Vec<&[f32]> = input.iter().map(Vec::as_slice).collect();
let mut out = enc.encode(&planes);
out.extend(enc.flush());
out
}
#[test]
fn headers_carry_the_configuration_and_frames_have_their_nominal_sizes() {
let n = 44_100;
let input = vec![sine(n, 44_100.0, 440.0, 0.5), sine(n, 44_100.0, 660.0, 0.4)];
let frames = parse(&encode_all(Config::cbr(44_100, 2, 128), &input));
// the decoder needs (input + delay) samples: whole frames of 1152
assert_eq!(frames.len(), (n + DELAY as usize).div_ceil(1152));
let f = &frames[0];
assert_eq!((f.lsf, f.bitrate_index, f.rate_index, f.mode), (false, 9, 0, 1));
// 44.1 kHz: 417 or 418 bytes, padded so that the mean is exactly 128 kbit/s
let total: usize = frames.iter().map(|f| f.len).sum();
let exact = 144_000.0 * 128.0 / 44_100.0 * frames.len() as f64;
assert!((total as f64 - exact).abs() < 1.0, "{total} vs {exact}");
assert!(frames.iter().any(|f| f.padding) && frames.iter().any(|f| !f.padding));
// the same tone in both channels at nearly the same level: mid/side
let ms = parse(&encode_all(Config::cbr(44_100, 2, 128), &[input[0].clone(), input[0].iter().map(|v| v * 0.9).collect()]));
assert!(ms.iter().skip(1).all(|f| f.mode_extension == 2), "mid/side expected");
// simple stereo never uses mid/side
let st = parse(&encode_all(Config { stereo: StereoMode::Stereo, ..Config::cbr(48_000, 2, 192) }, &input));
assert!(st.iter().all(|f| f.mode == 0 && f.mode_extension == 0));
// MPEG-2 LSF mono
let lsf = parse(&encode_all(Config::cbr(22_050, 1, 32), &input[..1]));
assert_eq!((lsf[0].lsf, lsf[0].rate_index, lsf[0].mode, lsf[0].part2_3.len()), (true, 0, 3, 1));
assert_eq!(lsf.len(), (n + DELAY as usize).div_ceil(576));
}
#[test]
fn the_reservoir_lends_bits_within_the_decoder_buffer() {
// quiet passages save bits that a loud burst of noise then borrows
let mut x = vec![0f32; 44_100];
let burst = noise(4410, 0.7, 9);
x[22_050..26_460].copy_from_slice(&burst);
x.iter_mut().take(22_050).enumerate().for_each(|(i, v)| *v = 0.01 * (i as f32 * 0.05).sin());
for (kbps, max_begin) in [(64u32, 511usize), (128, 511), (256, 125), (320, 0)] {
let frames = parse(&encode_all(Config::cbr(44_100, 1, kbps), &[x.clone()]));
let mut slots = 0usize;
let mut lent = 0;
for f in &frames {
let slot = f.len - 4 - 17;
assert!(f.main_data_begin <= max_begin, "{kbps} kbps: main_data_begin {}", f.main_data_begin);
assert!(f.main_data_begin <= slots, "{kbps} kbps: points before the stream");
let bits: usize = f.part2_3.iter().sum();
assert!(bits <= 8 * (f.main_data_begin + slot), "{kbps} kbps: {bits} bits of main data");
assert!(f.part2_3.iter().all(|&b| b <= 4095));
slots += slot;
lent = lent.max(f.main_data_begin);
}
if max_begin > 0 {
assert!(lent > 0, "{kbps} kbps: the reservoir was never used");
}
}
}
#[test]
fn hostile_settings_are_refused() {
for rate in [0, 8_000, 11_025, 44_101, 96_000, u32::MAX] {
assert_eq!(Encoder::new(Config::cbr(rate, 2, 128)).err(), Some(Error::SampleRate(rate)));
}
for kbps in [0, 8, 33, 144, 384, u32::MAX] {
assert!(matches!(Encoder::new(Config::cbr(44_100, 2, kbps)), Err(Error::Bitrate { .. })), "{kbps}");
}
assert!(Encoder::new(Config::cbr(24_000, 1, 8)).is_ok());
assert!(matches!(Encoder::new(Config::cbr(24_000, 1, 192)), Err(Error::Bitrate { .. })));
for ch in [0, 3, 6, usize::MAX] {
assert_eq!(Encoder::new(Config::cbr(48_000, ch, 128)).err(), Some(Error::Channels(ch)));
}
assert_eq!(nearest_bitrate(44_100, 1000), Some(320));
assert_eq!(nearest_bitrate(44_100, 130), Some(128));
assert_eq!(nearest_bitrate(44_100, 0), Some(32));
assert_eq!(nearest_bitrate(22_050, 320), Some(160));
assert_eq!(nearest_bitrate(96_000, 128), None);
assert_eq!(
(nearest_sample_rate(44_100), nearest_sample_rate(96_000), nearest_sample_rate(8_000), nearest_sample_rate(0)),
(44_100, 48_000, 16_000, 16_000)
);
assert_eq!(nearest_sample_rate(u32::MAX), 48_000);
assert!(bitrates(0).is_empty());
}
#[test]
fn hostile_samples_still_make_valid_frames() {
let mut enc = Encoder::new(Config::cbr(48_000, 2, 96)).unwrap();
let mut out = Vec::new();
let wild: Vec<f32> = (0..5000)
.map(|i| match i % 5 {
0 => f32::NAN,
1 => f32::INFINITY,
2 => -1e30,
3 => 1e-40,
_ => 3.0,
})
.collect();
out.extend(enc.encode(&[&wild, &wild[..10]])); // ragged: the second channel is padded with silence
out.extend(enc.encode(&[&wild[..7]])); // a missing channel is silence
out.extend(enc.encode(&[]));
out.extend(enc.encode(&[&[], &[], &[]]));
out.extend(enc.flush());
out.extend(enc.flush()); // a second flush adds nothing
let frames = parse(&out);
assert_eq!(frames.len(), (5007 + DELAY as usize).div_ceil(1152));
assert!(frames.iter().all(|f| f.global_gain.iter().all(|&g| g <= 255)));
// no input, no frames
let mut empty = Encoder::new(Config::cbr(44_100, 1, 64)).unwrap();
assert!(empty.flush().is_empty());
}
#[test]
fn chunking_does_not_change_the_stream() {
let input = vec![sine(20_000, 32_000.0, 300.0, 0.3), noise(20_000, 0.1, 5)];
let whole = encode_all(Config::cbr(32_000, 2, 96), &input);
let mut enc = Encoder::new(Config::cbr(32_000, 2, 96)).unwrap();
let mut parts = Vec::new();
for k in (0..20_000).step_by(777) {
let e = (k + 777).min(20_000);
parts.extend(enc.encode(&[&input[0][k..e], &input[1][k..e]]));
}
parts.extend(enc.flush());
assert!(whole == parts, "chunked encode differs");
}
/// Every sample rate and bit rate in mono, and the lowest and highest bit rate of each sample
/// rate in both stereo modes, fed the signals that stress the
/// quantizer (full-scale and clipped noise, an impulse train, DC, the Nyquist tone, denormals and
/// silence): whole frames of the nominal size, every granule within its 12-bit length and the
/// reservoir within the frames before it.
#[test]
fn every_setting_makes_valid_frames_from_extreme_signals() {
let signals: [fn(usize, u64) -> Vec<f32>; 7] = [
|n, s| noise(n, 1.0, s),
|n, s| noise(n, 40.0, s), // clamped to ±4 by the encoder
|n, _| (0..n).map(|i| if i % 97 == 0 { 1.0 } else { 0.0 }).collect(),
|n, _| vec![1.0; n],
|n, _| (0..n).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect(),
|n, _| vec![1e-42; n],
|n, _| vec![0.0; n],
];
let mut k = 0usize;
for rate in SAMPLE_RATES {
let rates = bitrates(rate);
for (i, &kbps) in rates.iter().enumerate() {
let edge = i == 0 || i + 1 == rates.len();
for (channels, stereo) in [(1, StereoMode::Stereo), (2, StereoMode::Stereo), (2, StereoMode::JointStereo)] {
if channels == 2 && !edge {
continue;
}
k += 1;
// two or three frames per setting keep a debug build quick
let n = 600;
let input: Vec<Vec<f32>> = (0..channels).map(|c| signals[(k + 3 * c) % signals.len()](n, (k + c) as u64)).collect();
let cfg = Config { sample_rate: rate, channels, bitrate_kbps: kbps, stereo };
let what = format!("{rate} Hz {kbps} kbit/s {channels} ch {stereo:?}");
let frames = parse(&encode_all(cfg, &input));
let fs = if rate >= 32_000 { 1152 } else { 576 };
assert_eq!(frames.len(), (n + DELAY as usize).div_ceil(fs), "{what}");
let side = match (rate >= 32_000, channels) {
(true, 1) => 17,
(true, _) => 32,
(false, 1) => 9,
(false, _) => 17,
};
let mut before = 0usize;
for f in &frames {
assert_eq!(f.lsf, rate < 32_000, "{what}");
assert_eq!(
f.mode,
if channels == 1 {
3
} else if stereo == StereoMode::Stereo {
0
} else {
1
},
"{what}"
);
let slot = f.len - 4 - side;
assert!(f.main_data_begin <= before, "{what}: main_data_begin {} points before the stream", f.main_data_begin);
assert!(f.part2_3.iter().all(|&b| b <= 4095), "{what}: {:?}", f.part2_3);
assert!(f.part2_3.iter().sum::<usize>() <= 8 * (f.main_data_begin + slot), "{what}: main data overruns its frame");
before += slot;
}
}
}
}
assert_eq!(k, 6 * 14 + 6 * 2 * 2);
}
+1
View File
@@ -12,6 +12,7 @@ mod ac3_encoder_oracle;
mod ac3_oracle;
mod alac;
mod concealment;
mod mp3_encoder_oracle;
mod mpa_oracle;
mod opus;
mod routing_oracle;
@@ -0,0 +1,162 @@
//! The encoder against ffmpeg as an external decoder (skipped without ffmpeg): ffprobe must see
//! an MP3 stream with our rate, channels and duration, ffmpeg must decode every frame without a
//! complaint, and its decode, aligned by [`DELAY`], must match the input within a signal-to-noise
//! ratio per bit rate. White noise loses what lies above the bit rate's low-pass, so its bound is
//! low; the bounds sit about 6 dB under what the encoder reaches today.
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use craft_audio::mp3::{Config, DELAY, Encoder, StereoMode};
fn fixtures() -> PathBuf {
crate::testkit::fixtures_dir("mp3enc")
}
fn chords(rate: u32, n: usize, detune: f32) -> Vec<f32> {
let f = [220.0, 277.18, 329.63, 440.0, 1318.5, 3520.0];
let a = [0.2, 0.15, 0.12, 0.1, 0.05, 0.02];
(0..n)
.map(|i| {
let t = i as f32 / rate as f32;
f.iter().zip(a).map(|(f, a)| a * (2.0 * std::f32::consts::PI * f * (1.0 + detune) * t).sin()).sum::<f32>()
})
.collect()
}
fn noise(n: usize, amp: f32, seed: u64) -> Vec<f32> {
let mut s = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
((s >> 40) as f32 / (1u64 << 24) as f32 * 2.0 - 1.0) * amp
})
.collect()
}
fn sweep(rate: u32, n: usize) -> Vec<f32> {
let secs = n as f32 / rate as f32;
let (f0, f1) = (40.0f32, rate as f32 * 0.4);
let k = (f1 / f0).ln() / secs;
(0..n).map(|i| 0.4 * (2.0 * std::f32::consts::PI * f0 * (((i as f32 / rate as f32) * k).exp() - 1.0) / k).sin()).collect()
}
fn encode(cfg: Config, input: &[Vec<f32>]) -> Vec<u8> {
let mut enc = Encoder::new(cfg).unwrap();
let mut out = Vec::new();
let n = input[0].len();
let mut pos = 0;
while pos < n {
let end = (pos + 1000).min(n);
let planes: Vec<&[f32]> = input.iter().map(|c| &c[pos..end]).collect();
out.extend(enc.encode(&planes));
pos = end;
}
out.extend(enc.flush());
out
}
fn ffmpeg_decode(ff: &Path, path: &Path, channels: usize) -> (Vec<Vec<f32>>, String) {
let out = Command::new(ff)
.args(["-v", "error", "-i"])
.arg(path)
.args(["-f", "f32le", "-acodec", "pcm_f32le", "-"])
.stdin(Stdio::null())
.output()
.unwrap();
let samples: Vec<f32> = out.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
let planar = (0..channels).map(|c| samples.iter().skip(c).step_by(channels).copied().collect()).collect();
(planar, String::from_utf8_lossy(&out.stderr).into_owned())
}
/// SNR in dB of `dec` (aligned) against `input`.
fn snr(input: &[f32], dec: &[f32]) -> f64 {
let (mut s, mut n) = (0f64, 0f64);
for (a, b) in input.iter().zip(dec) {
s += f64::from(*a) * f64::from(*a);
n += f64::from(a - b) * f64::from(a - b);
}
10.0 * (s / n.max(1e-30)).log10()
}
struct Case {
name: &'static str,
rate: u32,
channels: usize,
kbps: u32,
stereo: StereoMode,
signal: fn(u32, usize, usize) -> Vec<f32>,
min_snr: f64,
}
fn sig_chords(rate: u32, n: usize, c: usize) -> Vec<f32> {
chords(rate, n, 0.003 * c as f32)
}
fn sig_noise(_rate: u32, n: usize, c: usize) -> Vec<f32> {
noise(n, 0.25, 11 + c as u64)
}
fn sig_sweep(rate: u32, n: usize, c: usize) -> Vec<f32> {
sweep(rate, n).iter().map(|v| v * (1.0 - 0.2 * c as f32)).collect()
}
fn run(ff: &Path, c: &Case) -> f64 {
let n = c.rate as usize * 2 + 777;
let input: Vec<Vec<f32>> = (0..c.channels).map(|ch| (c.signal)(c.rate, n, ch)).collect();
let cfg = Config { sample_rate: c.rate, channels: c.channels, bitrate_kbps: c.kbps, stereo: c.stereo };
let bytes = encode(cfg, &input);
let path = fixtures().join(format!("{}_{}_{}ch_{}k.mp3", c.name, c.rate, c.channels, c.kbps));
std::fs::write(&path, &bytes).unwrap();
let (dec, err) = ffmpeg_decode(ff, &path, c.channels);
assert!(err.trim().is_empty(), "{}: ffmpeg reported: {err}", path.display());
let d = DELAY as usize;
assert!(dec[0].len() >= n + d, "{}: decoded {} samples, need {}", c.name, dec[0].len(), n + d);
// the bit rate is exact: every frame has the nominal size
let frame_samples = if c.rate >= 32_000 { 1152 } else { 576 };
let frames = dec[0].len() / frame_samples;
let nominal = c.kbps as f64 * 1000.0 / 8.0 * (frames * frame_samples) as f64 / c.rate as f64;
assert!((bytes.len() as f64 / nominal - 1.0).abs() < 0.01, "{}: {} bytes vs {nominal}", c.name, bytes.len());
if let Some(probe) = crate::testkit::oracle::find_tool("ffprobe") {
let out = Command::new(probe)
.args(["-v", "error", "-show_entries", "stream=codec_name,sample_rate,channels:format=duration", "-of", "default=nw=1"])
.arg(&path)
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
let field = |k: &str| text.lines().find_map(|l| l.strip_prefix(&format!("{k}="))).unwrap_or("").to_string();
assert_eq!(field("codec_name"), "mp3", "{text}");
assert_eq!(field("sample_rate"), c.rate.to_string(), "{text}");
assert_eq!(field("channels"), c.channels.to_string(), "{text}");
let dur: f64 = field("duration").parse().unwrap();
let expect = (frames * frame_samples) as f64 / c.rate as f64;
assert!((dur - expect).abs() < 2.0 * frame_samples as f64 / c.rate as f64, "{}: duration {dur} vs {expect}", c.name);
}
let s = (0..c.channels).map(|ch| snr(&input[ch], &dec[ch][d..d + n])).sum::<f64>() / c.channels as f64;
println!("{:<8} {:>5} Hz {}ch {:>3} kbps {:?}: SNR {s:.2} dB", c.name, c.rate, c.channels, c.kbps, c.stereo);
s
}
#[test]
fn ffmpeg_decodes_our_streams_at_every_rate() {
let ff = require_ffmpeg!();
use StereoMode::*;
let cases = [
Case { name: "chords", rate: 44_100, channels: 2, kbps: 128, stereo: JointStereo, signal: sig_chords, min_snr: 42.0 },
Case { name: "chords", rate: 44_100, channels: 2, kbps: 320, stereo: JointStereo, signal: sig_chords, min_snr: 60.0 },
Case { name: "sweep", rate: 48_000, channels: 2, kbps: 192, stereo: Stereo, signal: sig_sweep, min_snr: 22.0 },
Case { name: "noise", rate: 48_000, channels: 2, kbps: 128, stereo: JointStereo, signal: sig_noise, min_snr: 2.5 },
Case { name: "chords", rate: 32_000, channels: 1, kbps: 64, stereo: JointStereo, signal: sig_chords, min_snr: 40.0 },
Case { name: "chords", rate: 22_050, channels: 2, kbps: 64, stereo: JointStereo, signal: sig_chords, min_snr: 33.0 },
Case { name: "sweep", rate: 24_000, channels: 1, kbps: 48, stereo: JointStereo, signal: sig_sweep, min_snr: 24.0 },
Case { name: "chords", rate: 16_000, channels: 1, kbps: 32, stereo: JointStereo, signal: sig_chords, min_snr: 30.0 },
];
let mut failed = Vec::new();
for c in &cases {
let s = run(&ff, c);
if s < c.min_snr {
failed.push(format!("{} {} Hz {} kbps: {s:.2} < {}", c.name, c.rate, c.kbps, c.min_snr));
}
}
assert!(failed.is_empty(), "{failed:#?}");
}