From b1c64766e699deb825ae31387b36e8754ccf097d Mon Sep 17 00:00:00 2001 From: Aleksander Helgaker Date: Wed, 11 Nov 2026 13:52:06 +0100 Subject: [PATCH] craft-audio: AC-3 and E-AC-3 decoder (9/10) (#447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * craft-audio: AC-3 and E-AC-3 decoder FilmCraft's AC-3 decoder as `craft_audio::ac3` behind `ac3-decode`: every AC-3 channel mode and frame size, and E-AC-3 independent substream 0 with spectral extension, in WAV order without downmix. `TrackDecoder` and the native read path take AC-3 and E-AC-3 in MP4, Matroska and raw streams. Not bit-exact: zero-bit mantissas are filled with decoder-specific dither (A/52 §7.3.4), so tests compare at 65-94 dB SNR on tones and 27-48 dB on white noise. 7.1 E-AC-3 decodes as its 5.1 core. Co-Authored-By: Claude Opus 5.5 * craft-audio: review fixes, round 1 (#447) An AC-3 / E-AC-3 packet is as long as all the primary syncframes in it. Before the first decode, TrackDecoder::frames_per_packet gave 1536 for any AC-3 track, and conceal used the first syncframe's header only. An E-AC-3 stream of one-, two- or three-block syncframes then got the wrong MP4 length and seek positions and the wrong Matroska positions. A packet that groups several such syncframes concealed at a fraction of its length. Seeks in AC-3 and E-AC-3 now have to match the whole-file decode bit for bit, as every other codec's do. The dither is seeded from each syncframe, so the old 25 dB bound was looser than the decoder needs. Co-Authored-By: Claude Opus 5.5 * craft-audio: review fixes, round 2 (#447) - AC-3 / E-AC-3 packets that decode to different lengths (in the packets read ahead, or any decoded since) have no frames_per_packet, so a Matroska seek finds its packet by timestamp rather than by index times the first packet's length. - E-AC-3 syncframes of fewer than six blocks may reuse exponents from any syncframe before them: once one is seen, a seek decodes from the start of the stream instead of 1536 frames before the target. Co-Authored-By: Claude Opus 5.5 * craft-audio: review fixes, round 3 (#447) - Matroska E-AC-3: the first seek reads every packet's syncframe headers (only the headers, header stripping undone) and places packets by the frames their primary syncframes hold. Packets that started to differ in length after the eight read ahead were placed as the first packet's length times their index. - A packet whose later syncframe fails no longer sets the length lost packets are concealed at, nor marks the packets' lengths as differing: it decoded only in part. Co-Authored-By: Claude Opus 5.5 * Merge main; E-AC-3 in Matroska: read packet headers only up to the seek target, and count a packet of dependent syncframes as zero frames --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Dylan M. Taylor --- .github/workflows/audio.yml | 2 +- README.md | 6 +- crates/audio/Cargo.toml | 9 +- crates/audio/README.md | 33 +- crates/audio/src/ac3/eac3.rs | 635 +++++++++ crates/audio/src/ac3/mod.rs | 1348 +++++++++++++++++++ crates/audio/src/ac3/tables.rs | 71 + crates/audio/src/ac3/tests.rs | 274 ++++ crates/audio/src/layout.rs | 2 +- crates/audio/src/lib.rs | 24 +- crates/audio/src/read/mod.rs | 7 +- crates/audio/src/read/native.rs | 102 +- crates/audio/src/track.rs | 215 ++- crates/audio/tests/codecs/ac3_oracle.rs | 303 +++++ crates/audio/tests/codecs/main.rs | 1 + crates/audio/tests/codecs/routing_oracle.rs | 88 +- crates/audio/tests/codecs/testkit.rs | 5 + 17 files changed, 3074 insertions(+), 51 deletions(-) create mode 100644 crates/audio/src/ac3/eac3.rs create mode 100644 crates/audio/src/ac3/mod.rs create mode 100644 crates/audio/src/ac3/tables.rs create mode 100644 crates/audio/src/ac3/tests.rs create mode 100644 crates/audio/tests/codecs/ac3_oracle.rs diff --git a/.github/workflows/audio.yml b/.github/workflows/audio.yml index 8636c2595..d406b37ed 100644 --- a/.github/workflows/audio.yml +++ b/.github/workflows/audio.yml @@ -39,7 +39,7 @@ jobs: # Each feature on its own, so code that needs another feature fails here. - name: Clippy, each feature alone run: | - for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode serde; do + for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode ac3-decode serde; do echo "::group::features: ${f:-none}" cargo clippy -p craft-audio --all-targets --locked --no-default-features ${f:+--features "$f"} -- -D warnings echo "::endgroup::" diff --git a/README.md b/README.md index 8ac832dc8..a2fe39150 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,9 @@ craft- = { git = "https://github.com/storytold/craft-libs", rev = "`. -- `decode` tries native decoders first. AAC and Opus no longer go through symphonia, except AAC in +- `decode` tries native decoders first. AAC, Opus and AC-3 no longer go through symphonia, except AAC in CAF. MP4 and Matroska files decode to presentation length. A packet that fails is concealed, not dropped. A failed source read is `AudioError::Io`. diff --git a/crates/audio/src/ac3/eac3.rs b/crates/audio/src/ac3/eac3.rs new file mode 100644 index 000000000..ce8d765bb --- /dev/null +++ b/crates/audio/src/ac3/eac3.rs @@ -0,0 +1,635 @@ +//! E-AC-3 (ATSC A/52:2012 Annex E): the syncframe header and bit stream information (§E2.2.2), +//! the audio frame (§E2.2.3) and spectral extension (§E2.2.4, §E3.6). The audio blocks share the +//! AC-3 decoder (`Decoder::audio_block`), which reads the E-AC-3 differences from [`FrameInfo`]. + +use crate::ac3::{Bits, Error, Header, NFCHANS, Result}; + +/// Sample rates by fscod, and by fscod2 when fscod is 3 (Tables E2.2, E2.3). +const RATES: [u32; 3] = [48_000, 44_100, 32_000]; +const REDUCED_RATES: [u32; 3] = [24_000, 22_050, 16_000]; +/// Audio blocks per syncframe by numblkscod (Table E2.4). +const BLOCKS: [u8; 4] = [1, 2, 3, 6]; + +/// Exponent strategies of the six blocks for each frame-based strategy code (Table E2.10): +/// 0 reuse, 1 D15, 2 D25, 3 D45. +const FRAME_EXPSTR: [[u8; 6]; 32] = [ + [1, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 3], + [1, 0, 0, 0, 2, 0], + [1, 0, 0, 0, 3, 3], + [2, 0, 0, 2, 0, 0], + [2, 0, 0, 2, 0, 3], + [2, 0, 0, 3, 2, 0], + [2, 0, 0, 3, 3, 3], + [2, 0, 1, 0, 0, 0], + [2, 0, 2, 0, 0, 3], + [2, 0, 2, 0, 2, 0], + [2, 0, 2, 0, 3, 3], + [2, 0, 3, 2, 0, 0], + [2, 0, 3, 2, 0, 3], + [2, 0, 3, 3, 2, 0], + [2, 0, 3, 3, 3, 3], + [3, 1, 0, 0, 0, 0], + [3, 1, 0, 0, 0, 3], + [3, 2, 0, 0, 2, 0], + [3, 2, 0, 0, 3, 3], + [3, 2, 0, 2, 0, 0], + [3, 2, 0, 2, 0, 3], + [3, 2, 0, 3, 2, 0], + [3, 2, 0, 3, 3, 3], + [3, 3, 1, 0, 0, 0], + [3, 3, 2, 0, 0, 3], + [3, 3, 2, 0, 2, 0], + [3, 3, 2, 0, 3, 3], + [3, 3, 3, 2, 0, 0], + [3, 3, 3, 2, 0, 3], + [3, 3, 3, 3, 2, 0], + [3, 3, 3, 3, 3, 3], +]; + +/// Default coupling band structure by coupling sub-band (Table E2.12, "Default Coupling Banding +/// Structure"; sub-band 0 never starts a combined band). +const DEFAULT_CPLBNDSTRC: [bool; 18] = { + let mut t = [false; 18]; + let ones = [8, 10, 11, 13, 14, 15, 16, 17]; + let mut i = 0; + while i < ones.len() { + t[ones[i]] = true; + i += 1; + } + t +}; + +/// Default spectral extension band structure by sub-band (Table E2.11). +const DEFAULT_SPXBNDSTRC: [bool; 17] = { + let mut t = [false; 17]; + let mut i = 8; + while i < 17 { + t[i] = true; + i += 2; + } + t +}; + +/// First transform coefficient of spectral extension sub-band `k` (Table E3.13, k ≤ 17). +const fn spx_band_bin(k: usize) -> usize { + 25 + 12 * k +} + +/// Spectral extension attenuation (Table E3.14): spxattentab[code][binindex] = 2^(-(code + 1) · +/// (binindex + 1) / 15), which the table lists to nine decimals. +fn spx_atten(code: usize, binindex: usize) -> f32 { + let e = (code as f64 + 1.0) * (binindex as f64 + 1.0) / 15.0; + (-e).exp2() as f32 +} + +/// Parse the header of an E-AC-3 syncframe (bsid 11-16) at the start of `b` (at least 7 bytes). +pub(crate) fn parse_header(b: &[u8], bsid: u8) -> Result
{ + let mut r = Bits::new(b.get(2..).unwrap_or_default()); + let strmtyp = r.read(2) as u8; + let substreamid = r.read(3) as u8; + let words = r.read(11) as usize + 1; + let fscod = r.read(2) as usize; + let (sample_rate, blocks) = match RATES.get(fscod) { + Some(&rate) => (rate, BLOCKS[r.read(2) as usize & 3]), + None => (*REDUCED_RATES.get(r.read(2) as usize).ok_or(Error::Invalid("reserved fscod2"))?, 6), + }; + let acmod = r.read(3) as u8; + let lfeon = r.bit(); + if strmtyp == 3 { + return Err(Error::Unsupported("reserved E-AC-3 stream type 3".into())); + } + let frame_bytes = words * 2; + if frame_bytes < 8 { + return Err(Error::Invalid("E-AC-3 syncframe too short")); + } + // frame bits per second of audio + let bits = frame_bytes as u64 * 8 * u64::from(sample_rate) / (256 * u64::from(blocks)); + let bitrate_kbps = u32::try_from(bits / 1000).unwrap_or(u32::MAX); + Ok(Header { sample_rate, bitrate_kbps, frame_bytes, bsid, acmod, lfeon, blocks, strmtyp, substreamid }) +} + +/// What the audio frame and blocks need from the bit stream information. +pub(crate) struct Bsi { + pub strmtyp: u8, + /// Sample rate code for the bit allocation's hearing threshold: fscod, or fscod2 at the + /// reduced rates. + pub fscod: usize, + pub numblkscod: usize, +} + +/// Read syncinfo and bsi (§E2.2.1-2), leaving `r` at the audio frame. +#[allow(clippy::cognitive_complexity, reason = "the A/52 Annex E bit stream parse, ported as-is from FilmCraft")] +pub(crate) fn read_bsi(r: &mut Bits) -> Result { + r.skip(16); // syncword + let strmtyp = r.read(2) as u8; + r.skip(3 + 11); // substreamid, frmsiz + let fscod = r.read(2) as usize; + let (rate_code, numblkscod) = if fscod == 3 { + let fscod2 = r.read(2) as usize; + if fscod2 == 3 { + return Err(Error::Invalid("reserved fscod2")); + } + (fscod2, 3) + } else { + (fscod, r.read(2) as usize) + }; + let nblocks = BLOCKS[numblkscod & 3] as usize; + let acmod = r.read(3) as usize; + let lfeon = r.bit(); + r.skip(5 + 5); // bsid, dialnorm + if r.bit() { + r.skip(8); // compr + } + if acmod == 0 { + r.skip(5); // dialnorm2 + if r.bit() { + r.skip(8); // compr2 + } + } + if strmtyp == 1 && r.bit() { + r.skip(16); // chanmap + } + if r.bit() { + // mixing metadata + if acmod > 2 { + r.skip(2); // dmixmod + } + if acmod & 1 != 0 && acmod > 2 { + r.skip(6); // ltrtcmixlev, lorocmixlev + } + if acmod & 4 != 0 { + r.skip(6); // ltrtsurmixlev, lorosurmixlev + } + if lfeon && r.bit() { + r.skip(5); // lfemixlevcod + } + if strmtyp == 0 { + if r.bit() { + r.skip(6); // pgmscl + } + if acmod == 0 && r.bit() { + r.skip(6); // pgmscl2 + } + if r.bit() { + r.skip(6); // extpgmscl + } + match r.read(2) { + 1 => r.skip(5), // premixcmpsel, drcsrc, premixcmpscl + 2 => r.skip(12), + // mixdeflen: the mixdata field (through mixdatafill) is mixdeflen + 2 bytes + 3 => { + let len = r.read(5) as usize; + r.skip(8 * (len + 2)); + } + _ => {} + } + if acmod < 2 { + if r.bit() { + r.skip(14); // panmean, paninfo + } + if acmod == 0 && r.bit() { + r.skip(14); // panmean2, paninfo2 + } + } + if r.bit() { + // frame mixing configuration information + if numblkscod == 0 { + r.skip(5); + } else { + for _ in 0..nblocks { + if r.bit() { + r.skip(5); + } + } + } + } + } + } + if r.bit() { + // informational metadata + r.skip(5); // bsmod, copyrightb, origbs + if acmod == 2 { + r.skip(4); // dsurmod, dheadphonmod + } + if acmod >= 6 { + r.skip(2); // dsurexmod + } + if r.bit() { + r.skip(8); // mixlevel, roomtyp, adconvtyp + } + if acmod == 0 && r.bit() { + r.skip(8); // mixlevel2, roomtyp2, adconvtyp2 + } + if fscod < 3 { + r.skip(1); // sourcefscod + } + } + if strmtyp == 0 && numblkscod != 3 { + r.skip(1); // convsync + } + if strmtyp == 2 && (numblkscod == 3 || r.bit()) { + r.skip(6); // blkid → frmsizecod + } + if r.bit() { + let l = r.read(6) as usize; + r.skip((l + 1) * 8); // addbsi + } + Ok(Bsi { strmtyp, fscod: rate_code, numblkscod }) +} + +/// The audio frame (§E2.2.3) and the state that runs from block to block within a syncframe. +pub(crate) struct FrameInfo { + pub strmtyp: u8, + pub blkswe: bool, + pub dithflage: bool, + pub bamode: bool, + pub frmfgaincode: bool, + pub dbaflde: bool, + pub skipflde: bool, + pub snroffststr: u8, + pub frmcsnroffst: i32, + pub frmfsnroffst: i32, + pub cplstre: [bool; 6], + pub cplinu: [bool; 6], + pub cplexpstr: [u8; 6], + pub chexpstr: [[u8; 5]; 6], + pub lfeexpstr: [u8; 6], + /// Spectral extension attenuation code per channel (spxattencod), when in use. + pub spxatten: [Option; 5], + pub firstcplcos: [bool; 5], + pub firstcplleak: bool, + /// Coupling band structure by absolute coupling sub-band. + pub cplbndstrc: [bool; 18], + pub spx: Spx, +} + +impl FrameInfo { + /// Read the audio frame. `words` is the syncframe size in 16-bit words. + pub(crate) fn read(r: &mut Bits, bsi: &Bsi, acmod: usize, lfeon: bool, nblocks: usize, words: usize) -> Result { + let nf = NFCHANS[acmod & 7]; + let six = bsi.numblkscod == 3; + let (expstre, ahte) = if six { (r.bit(), r.bit()) } else { (true, false) }; + let snroffststr = r.read(2) as u8; + if snroffststr == 3 { + return Err(Error::Invalid("reserved SNR offset strategy")); + } + let transproce = r.bit(); + let mut e = FrameInfo { + strmtyp: bsi.strmtyp, + blkswe: r.bit(), + dithflage: r.bit(), + bamode: r.bit(), + frmfgaincode: r.bit(), + dbaflde: r.bit(), + skipflde: r.bit(), + snroffststr, + frmcsnroffst: 0, + frmfsnroffst: 0, + cplstre: [false; 6], + cplinu: [false; 6], + cplexpstr: [0; 6], + chexpstr: [[0; 5]; 6], + lfeexpstr: [0; 6], + spxatten: [None; 5], + firstcplcos: [true; 5], + firstcplleak: true, + cplbndstrc: DEFAULT_CPLBNDSTRC, + spx: Spx::new(), + }; + let spxattene = r.bit(); + if acmod > 1 { + e.cplstre[0] = true; + e.cplinu[0] = r.bit(); + for blk in 1..nblocks { + e.cplstre[blk] = r.bit(); + e.cplinu[blk] = if e.cplstre[blk] { r.bit() } else { e.cplinu[blk - 1] }; + } + } + if expstre { + for blk in 0..nblocks { + if e.cplinu[blk] { + e.cplexpstr[blk] = r.read(2) as u8; + } + for ch in 0..nf { + e.chexpstr[blk][ch] = r.read(2) as u8; + } + } + } else { + // frame-based strategies (six blocks only) + if acmod > 1 && e.cplinu.iter().any(|&c| c) { + e.cplexpstr = FRAME_EXPSTR[r.read(5) as usize & 31]; + } + for ch in 0..nf { + let s = FRAME_EXPSTR[r.read(5) as usize & 31]; + for (blk, &v) in s.iter().enumerate() { + e.chexpstr[blk][ch] = v; + } + } + } + if lfeon { + for blk in 0..nblocks { + e.lfeexpstr[blk] = r.read(1) as u8; + } + } + if bsi.strmtyp == 0 && (six || r.bit()) { + r.skip(5 * nf); // convexpstr + } + if ahte { + // AHT flags exist for channels whose exponents are sent once in the frame (§E3.4.2) + let unsupported = || Err(Error::Unsupported("E-AC-3 adaptive hybrid transform".into())); + let ncplblks = e.cplinu.iter().filter(|&&c| c).count(); + let ncplregs = (0..6).filter(|&b| e.cplstre[b] || e.cplexpstr[b] != 0).count(); + if ncplblks == 6 && ncplregs == 1 && r.bit() { + return unsupported(); + } + for ch in 0..nf { + if (0..6).filter(|&b| e.chexpstr[b][ch] != 0).count() == 1 && r.bit() { + return unsupported(); + } + } + if lfeon && e.lfeexpstr.iter().filter(|&&s| s != 0).count() == 1 && r.bit() { + return unsupported(); + } + } + if snroffststr == 0 { + e.frmcsnroffst = r.read(6) as i32; + e.frmfsnroffst = r.read(4) as i32; + } + if transproce { + // transient pre-noise processing (§E3.7) is not applied + for _ in 0..nf { + if r.bit() { + r.skip(10 + 8); // transprocloc, transproclen + } + } + } + if spxattene { + for ch in 0..nf { + if r.bit() { + e.spxatten[ch] = Some(r.read(5) as usize); + } + } + } + if bsi.numblkscod != 0 && r.bit() { + // blkstrtinfo: (blocks - 1) × (4 + ceil(log2(words per frame))) bits (§E2.3.2.27) + let log2 = (usize::BITS - words.saturating_sub(1).leading_zeros()) as usize; + r.skip((nblocks - 1) * (4 + log2)); + } + Ok(e) + } +} + +/// Spectral extension parameters (§E2.2.4, §E3.6), carried from block to block of a syncframe. +pub(crate) struct Spx { + pub inu: bool, + pub chinspx: [bool; 5], + pub begf: usize, + strtf: usize, + /// First sub-band and one past the last (spx_begin_subbnd, spx_end_subbnd). + begin: usize, + end: usize, + bndstrc: [bool; 17], + /// Band sizes in coefficients (spxbndsztab) and their count (nspxbnds). + sizes: [usize; 17], + nbands: usize, + first: [bool; 5], + co: [[f32; 17]; 5], + nblend: [[f32; 17]; 5], + sblend: [[f32; 17]; 5], +} + +impl Spx { + fn new() -> Spx { + Spx { + inu: false, + chinspx: [false; 5], + begf: 0, + strtf: 0, + begin: 0, + end: 0, + bndstrc: DEFAULT_SPXBNDSTRC, + sizes: [0; 17], + nbands: 0, + first: [true; 5], + co: [[0.0; 17]; 5], + nblend: [[0.0; 17]; 5], + sblend: [[1.0; 17]; 5], + } + } + + /// First coefficient of the extension region. + pub fn begin_bin(&self) -> usize { + spx_band_bin(self.begin) + } + + /// Read the strategy and coordinates of block `blk`. + pub(crate) fn read(&mut self, r: &mut Bits, blk: usize, acmod: usize, nf: usize) -> Result<()> { + if blk == 0 || r.bit() { + self.inu = r.bit(); + if self.inu { + if acmod == 1 { + self.chinspx = [true, false, false, false, false]; + } else { + for ch in 0..nf { + self.chinspx[ch] = r.bit(); + } + } + self.strtf = r.read(2) as usize; + self.begf = r.read(3) as usize; + let endf = r.read(3) as usize; + self.begin = if self.begf < 6 { self.begf + 2 } else { self.begf * 2 - 3 }; + self.end = if endf < 3 { endf + 5 } else { endf * 2 + 3 }; + if r.bit() { + for b in self.begin + 1..self.end { + self.bndstrc[b] = r.bit(); + } + } + if self.begin >= self.end { + return Err(Error::Invalid("spectral extension ends before it begins")); + } + if self.strtf >= self.begin { + return Err(Error::Invalid("spectral extension copies from above its start")); + } + self.nbands = 1; + self.sizes = [0; 17]; + self.sizes[0] = 12; + for b in self.begin + 1..self.end { + if self.bndstrc[b] { + self.sizes[self.nbands - 1] += 12; + } else { + self.sizes[self.nbands] = 12; + self.nbands += 1; + } + } + } else { + self.chinspx = [false; 5]; + self.first = [true; 5]; + } + } + if !self.inu { + return Ok(()); + } + for ch in 0..nf { + if !self.chinspx[ch] { + self.first[ch] = true; + continue; + } + let coe = std::mem::replace(&mut self.first[ch], false) || r.bit(); + if !coe { + continue; + } + let blend = r.read(5) as f32 / 32.0; + let mstr = r.read(2) as i32; + // coordinates (§E3.6.3) and blending factors (§E3.6.4.2.1) + let mut mant = self.begin_bin() as f32; + let end = spx_band_bin(self.end) as f32; + for b in 0..self.nbands { + let e = r.read(4) as i32; + let m = r.read(2) as f32; + let v = if e == 15 { m / 4.0 } else { (m + 4.0) / 8.0 }; + self.co[ch][b] = v * 2f32.powi(-(e + 3 * mstr)); + let size = self.sizes[b] as f32; + let nratio = ((mant + 0.5 * size) / end - blend).clamp(0.0, 1.0); + self.nblend[ch][b] = nratio.sqrt(); + self.sblend[ch][b] = (1.0 - nratio).sqrt(); + mant += size; + } + } + Ok(()) + } + + /// Synthesize the extension region of one channel's coefficients (§E3.6.4): translate the + /// low band up, notch-filter the borders when attenuation is on, blend with `noise` (zero + /// mean, unit variance) scaled to the band's energy, and scale by the coordinates. + pub(crate) fn synthesize(&self, ch: usize, atten: Option, tc: &mut [f32; 256], mut noise: impl FnMut() -> f32) { + let copystart = spx_band_bin(self.strtf); + let copyend = self.begin_bin(); + let sizes = &self.sizes[..self.nbands]; + // translation + let mut wrap = [false; 17]; + let (mut copy, mut insert) = (copystart, copyend); + for (b, &size) in sizes.iter().enumerate() { + if copy + size > copyend { + copy = copystart; + wrap[b] = true; + } + for _ in 0..size { + if copy == copyend { + copy = copystart; + } + if let (Some(&v), true) = (tc.get(copy), insert < tc.len()) { + tc[insert] = v; + } + insert += 1; + copy += 1; + } + } + // banded RMS energy of the translated coefficients + let mut rms = [0f32; 17]; + let mut at = copyend; + for (b, &size) in sizes.iter().enumerate() { + let band = tc.get(at..at + size).unwrap_or_default(); + rms[b] = (band.iter().map(|v| v * v).sum::() / size as f32).sqrt(); + at += size; + } + // notch filter around the baseband / extension border and each wrap point + if let Some(code) = atten { + let g = [spx_atten(code, 0), spx_atten(code, 1), spx_atten(code, 2)]; + let mut notch = |center: usize| { + for (k, gain) in [g[0], g[1], g[2], g[1], g[0]].into_iter().enumerate() { + if let Some(v) = (center + k).checked_sub(2).and_then(|i| tc.get_mut(i)) { + *v *= gain; + } + } + }; + notch(copyend); + let mut start = copyend; + for (b, &size) in sizes.iter().enumerate() { + if b > 0 && wrap[b] { + notch(start); + } + start += size; + } + } + // noise blending and scaling + let mut at = copyend; + for (b, &size) in sizes.iter().enumerate() { + let nscale = rms[b] * self.nblend[ch][b]; + let sscale = self.sblend[ch][b]; + let co = self.co[ch][b] * 32.0; + for _ in 0..size { + let n = noise(); + if let Some(v) = tc.get_mut(at) { + *v = (*v * sscale + n * nscale) * co; + } + at += 1; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn attenuation_matches_table_e3_14() { + // rows 0, 4, 14 and 31 as printed + let rows: [(usize, [f64; 3]); 4] = [ + (0, [0.954841604, 0.911722489, 0.870550563]), + (4, [0.793700526, 0.629960525, 0.500000000]), + (14, [0.500000000, 0.250000000, 0.125000000]), + (31, [0.227930622, 0.051952369, 0.011841536]), + ]; + for (code, want) in rows { + for (i, w) in want.into_iter().enumerate() { + assert!((f64::from(spx_atten(code, i)) - w).abs() < 1e-7, "{code} {i}"); + } + } + } + + #[test] + fn default_band_structures() { + assert_eq!(DEFAULT_CPLBNDSTRC.iter().filter(|&&b| b).count(), 8); + assert!(DEFAULT_CPLBNDSTRC[8] && !DEFAULT_CPLBNDSTRC[9] && !DEFAULT_CPLBNDSTRC[12] && DEFAULT_CPLBNDSTRC[17]); + assert_eq!(DEFAULT_SPXBNDSTRC.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect::>(), vec![8, 10, 12, 14, 16]); + // every frame-based strategy sends new exponents in block 0 + assert!(FRAME_EXPSTR.iter().all(|s| s[0] != 0)); + } + + #[test] + fn spectral_extension_translation_and_scaling() { + // begin at sub-band 2 (bin 49), end at 5 (bin 85), copy from sub-band 0 (bin 25): three + // 12-bin bands, the third wraps back to the copy start + let mut s = Spx::new(); + s.inu = true; + (s.strtf, s.begin, s.end, s.nbands) = (0, 2, 5, 3); + s.sizes[..3].copy_from_slice(&[12, 12, 12]); + s.co[0][..3].copy_from_slice(&[1.0 / 32.0, 0.5 / 32.0, 0.25 / 32.0]); + s.nblend[0] = [0.0; 17]; + s.sblend[0] = [1.0; 17]; + let mut tc = [0f32; 256]; + for (i, v) in tc[25..49].iter_mut().enumerate() { + *v = i as f32 + 1.0; + } + s.synthesize(0, None, &mut tc, || 0.0); + assert_eq!(tc[49], 1.0); + assert_eq!(tc[61], 13.0 * 0.5); + assert_eq!(tc[73], 1.0 * 0.25); + assert_eq!(tc[84], 12.0 * 0.25); + assert_eq!(tc[85], 0.0); + // pure noise: the band gets the energy of its translated coefficients + s.nblend[0] = [1.0; 17]; + s.sblend[0] = [0.0; 17]; + s.co[0] = [1.0 / 32.0; 17]; + let mut tc2 = [0f32; 256]; + tc2[25..49].fill(2.0); + let mut k = 0; + s.synthesize(0, None, &mut tc2, || { + k += 1; + if k % 2 == 0 { 1.0 } else { -1.0 } + }); + assert!(tc2[49..85].iter().all(|v| v.abs() == 2.0)); + } +} diff --git a/crates/audio/src/ac3/mod.rs b/crates/audio/src/ac3/mod.rs new file mode 100644 index 000000000..708c5cca7 --- /dev/null +++ b/crates/audio/src/ac3/mod.rs @@ -0,0 +1,1348 @@ +//! Clean-room AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) audio decoder, written from +//! ATSC A/52:2012 ("Digital Audio Compression Standard", §5 bit stream syntax, §6-7 decoding, +//! Annex E for E-AC-3). +//! +//! - All audio coding modes (1+1, 1/0 … 3/2) with or without LFE, 32 / 44.1 / 48 kHz, every +//! frame size (Table 5.18). +//! - Exponents (D15 / D25 / D45, reuse across blocks), the parametric bit allocation exactly as +//! specified (§7.2, fixed-point integer steps and Tables 7.6-7.16) including delta bit allocation, +//! grouped and asymmetric mantissas, dither for zero-bit mantissas (§7.3.4), channel coupling +//! with phase flags, rematrixing, dynamic range control (dynrng / dynrng2), block switching +//! (512- and 256-sample IMDCTs, §7.9.4) with the Kaiser-Bessel derived window. +//! - Output: planar f32 at full scale ±1, channels in WAV / SMPTE order (L R C LFE Ls Rs); no +//! downmix. +//! +//! E-AC-3 (bsid 11-16, Annex E): independent substream 0 (dependent substreams and further +//! programs are skipped), 1 / 2 / 3 / 6 blocks per syncframe, the reduced sample rates, frame-based +//! exponent strategies and the other frame-level syntax, and spectral extension (§E3.6). The +//! adaptive hybrid transform and enhanced coupling are refused with [`Error::Unsupported`]; +//! transient pre-noise processing data is read and not applied. +//! +//! From FilmCraft's `filmcraft-ac3` (storytold/filmcraft@8fda2f2). Public items keep their names, so +//! FilmCraft can re-export this module. + +// Index loops follow the specification's pseudo-code (FilmCraft allows this lint workspace-wide). +#![allow(clippy::needless_range_loop)] + +mod eac3; +mod tables; + +use std::sync::OnceLock; + +/// Errors from header parsing and decoding. +#[non_exhaustive] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + /// The data does not start with the syncword 0x0B77. + #[error("not an AC-3 frame")] + NoSync, + /// A valid stream uses a feature this decoder does not implement (bsid above 16, E-AC-3 + /// adaptive hybrid transform, enhanced coupling, reserved stream type). + #[error("unsupported AC-3: {0}")] + Unsupported(String), + /// Reserved, out-of-range or truncated syncframe data. + #[error("invalid AC-3 frame: {0}")] + Invalid(&'static str), +} + +/// Result with this module's [`Error`]. +pub type Result = std::result::Result; + +/// Data rate in kb/s by frmsizecod / 2 (Table 5.18). +const BITRATES: [u32; 19] = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640]; + +/// Full-bandwidth channels per acmod (Table 5.8). +const NFCHANS: [usize; 8] = [2, 1, 2, 3, 3, 4, 4, 5]; + +/// syncinfo and the start of bsi. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Header { + /// Sampling rate in Hz (`fscod`, or `fscod2` for E-AC-3 reduced rates). + pub sample_rate: u32, + /// Data rate in kb/s: from `frmsizecod` (Table 5.18) for AC-3, computed from the frame size for + /// E-AC-3. + pub bitrate_kbps: u32, + /// Bytes in the syncframe. + pub frame_bytes: usize, + /// Bit stream identification (`bsid`): up to 10 for AC-3, 11-16 for E-AC-3. + pub bsid: u8, + /// Audio coding mode (`acmod`, Table 5.8), 0-7. + pub acmod: u8, + /// LFE channel present (`lfeon`). + pub lfeon: bool, + /// Audio blocks (of 256 samples per channel) in the syncframe: 6 for AC-3; 1, 2, 3 or 6 for + /// E-AC-3. + pub blocks: u8, + /// E-AC-3 stream type (Table E2.1): 0 independent, 1 dependent, 2 independent converted from + /// AC-3. 0 for AC-3. + pub strmtyp: u8, + /// E-AC-3 substream id (0 for AC-3). + pub substreamid: u8, +} + +impl Header { + /// Output channels (full bandwidth + LFE). + pub fn channels(&self) -> usize { + NFCHANS[self.acmod as usize & 7] + self.lfeon as usize + } + /// Samples per channel the syncframe decodes to. + pub fn samples(&self) -> usize { + 256 * self.blocks as usize + } + /// Whether this is an E-AC-3 syncframe (bsid 11-16). + pub fn is_eac3(&self) -> bool { + self.bsid > 10 + } + /// Whether the syncframe carries the program [`Decoder::decode`] outputs: AC-3, or E-AC-3 + /// independent substream 0 (§E2.3.1.2). [`Decoder::decode`] returns no channels for the + /// others: dependent substreams (the channels beyond 5.1 of a 7.1 program) and further + /// independent programs. + pub fn is_primary(&self) -> bool { + self.strmtyp != 1 && self.substreamid == 0 + } +} + +/// Parse the frame header at the start of `b`. +pub fn parse_header(b: &[u8]) -> Result
{ + if b.len() < 7 || b[0] != 0x0B || b[1] != 0x77 { + return Err(Error::NoSync); + } + let fscod = b[4] >> 6; + let frmsizecod = b[4] & 0x3F; + let bsid = b[5] >> 3; + if (11..=16).contains(&bsid) { + return eac3::parse_header(b, bsid); + } + if bsid > 16 { + return Err(Error::Unsupported(format!("bsid {bsid}"))); + } + if fscod == 3 || frmsizecod >= 38 { + return Err(Error::Invalid("reserved fscod / frmsizecod")); + } + let kbps = BITRATES[(frmsizecod / 2) as usize]; + let words = match fscod { + 0 => kbps * 2, + 1 => kbps * 96_000 / 44_100 + (frmsizecod & 1) as u32, + _ => kbps * 3, + }; + // bsid 9 / 10: half / quarter sample rate (A/52 Annex note on "bsid 9 and 10") + let shift = bsid.saturating_sub(8) as u32; + let sample_rate = [48_000, 44_100, 32_000][fscod as usize] >> shift; + let mut r = Bits::new(&b[6..]); + let acmod = r.read(3) as u8; + if acmod & 1 != 0 && acmod != 1 { + r.skip(2); + } + if acmod & 4 != 0 { + r.skip(2); + } + if acmod == 2 { + r.skip(2); + } + let lfeon = r.read(1) == 1; + Ok(Header { + sample_rate, + bitrate_kbps: kbps >> shift, + frame_bytes: words as usize * 2, + bsid, + acmod, + lfeon, + blocks: 6, + strmtyp: 0, + substreamid: 0, + }) +} + +/// MSB-first bit reader; reads past the end give zeros. +struct Bits<'a> { + d: &'a [u8], + pos: usize, +} + +impl<'a> Bits<'a> { + fn new(d: &'a [u8]) -> Self { + Self { d, pos: 0 } + } + fn read(&mut self, n: u32) -> u32 { + let mut v = 0u32; + for _ in 0..n { + let byte = self.d.get(self.pos >> 3).copied().unwrap_or(0); + v = (v << 1) | ((byte >> (7 - (self.pos & 7))) & 1) as u32; + self.pos += 1; + } + v + } + fn bit(&mut self) -> bool { + self.read(1) == 1 + } + fn skip(&mut self, n: usize) { + self.pos += n; + } + fn overrun(&self) -> bool { + self.pos > self.d.len() * 8 + } +} + +/// Index of the LFE and coupling channels in the per-channel arrays. +const LFE: usize = 5; +const CPL: usize = 6; + +/// Bit allocation parameter tables (Tables 7.6-7.11). +const SLOWDEC: [i32; 4] = [0x0f, 0x11, 0x13, 0x15]; +const FASTDEC: [i32; 4] = [0x3f, 0x53, 0x67, 0x7b]; +const SLOWGAIN: [i32; 4] = [0x540, 0x4d8, 0x478, 0x410]; +const DBPBTAB: [i32; 4] = [0x000, 0x700, 0x900, 0xb00]; +const FLOORTAB: [i32; 8] = [0x2f0, 0x2b0, 0x270, 0x230, 0x1f0, 0x170, 0x0f0, -0x800]; +const FASTGAIN: [i32; 8] = [0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, 0x400]; +/// Mantissa bits of the asymmetric quantizers by bap (Table 7.18). +const QNTZTAB: [u32; 16] = [0, 0, 0, 3, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16]; + +/// The Kaiser-Bessel derived window of §7.9.4 (α = 5, 512 points), whose first half Table 7.33 +/// lists to five decimals. +fn window() -> &'static [f32; 256] { + static W: OnceLock<[f32; 256]> = OnceLock::new(); + W.get_or_init(|| { + let i0 = |x: f64| { + let (mut s, mut t) = (1.0f64, 1.0f64); + for k in 1..50 { + t *= (x / 2.0) / k as f64; + s += t * t; + } + s + }; + let alpha = 5.0 * std::f64::consts::PI; + let k: Vec = (0..=256).map(|j| i0(alpha * (1.0 - ((j as f64 - 128.0) / 128.0).powi(2)).max(0.0).sqrt())).collect(); + let total: f64 = k.iter().sum(); + let mut acc = 0.0; + let mut w = [0f32; 256]; + for n in 0..256 { + acc += k[n]; + w[n] = (acc / total).sqrt() as f32; + } + w + }) +} + +/// Twiddles for the IMDCTs: (xcos1, xsin1) for N = 512 and (xcos2, xsin2) for the 256-sample +/// transforms, and the IFFT kernels. +struct Twiddles { + c1: [f32; 128], + s1: [f32; 128], + c2: [f32; 64], + s2: [f32; 64], + /// e^{j 2π m / 128}, m = 0..128. + e: [(f32, f32); 128], +} + +fn twiddles() -> &'static Twiddles { + static T: OnceLock = OnceLock::new(); + T.get_or_init(|| { + use std::f64::consts::PI; + let n = 512.0; + let mut t = Twiddles { c1: [0.0; 128], s1: [0.0; 128], c2: [0.0; 64], s2: [0.0; 64], e: [(0.0, 0.0); 128] }; + for k in 0..128 { + let a = 2.0 * PI * (8 * k + 1) as f64 / (8.0 * n); + t.c1[k] = -a.cos() as f32; + t.s1[k] = -a.sin() as f32; + let m = 2.0 * PI * k as f64 / 128.0; + t.e[k] = (m.cos() as f32, m.sin() as f32); + } + for k in 0..64 { + let a = 2.0 * PI * (8 * k + 1) as f64 / (4.0 * n); + t.c2[k] = -a.cos() as f32; + t.s2[k] = -a.sin() as f32; + } + t + }) +} + +/// In-place complex inverse DFT of size `n` (128 or 64) by radix-2 decimation in time: +/// z[n] = Σ Z[k] e^{+j 2π k n / size}. +fn ifft(re: &mut [f32], im: &mut [f32]) { + let n = re.len(); + let e = &twiddles().e; + // bit reversal + let bits = n.trailing_zeros(); + for i in 0..n { + let j = i.reverse_bits() >> (usize::BITS - bits); + if j > i { + re.swap(i, j); + im.swap(i, j); + } + } + let mut len = 2; + while len <= n { + let step = 128 / len; + for s in (0..n).step_by(len) { + for k in 0..len / 2 { + let (wr, wi) = e[k * step]; + let (a, b) = (s + k, s + k + len / 2); + let tr = re[b] * wr - im[b] * wi; + let ti = re[b] * wi + im[b] * wr; + re[b] = re[a] - tr; + im[b] = im[a] - ti; + re[a] += tr; + im[a] += ti; + } + } + len *= 2; + } +} + +/// Inverse transform of one block of 256 coefficients into 512 windowed samples (§7.9.4). +fn imdct(x: &[f32; 256], short: bool, out: &mut [f32; 512]) { + let w = window(); + let t = twiddles(); + const N: usize = 512; + if !short { + let (mut zr, mut zi) = ([0f32; 128], [0f32; 128]); + for k in 0..N / 4 { + let (a, b) = (x[N / 2 - 2 * k - 1], x[2 * k]); + zr[k] = a * t.c1[k] - b * t.s1[k]; + zi[k] = b * t.c1[k] + a * t.s1[k]; + } + ifft(&mut zr, &mut zi); + let (mut yr, mut yi) = ([0f32; 128], [0f32; 128]); + for n in 0..N / 4 { + yr[n] = zr[n] * t.c1[n] - zi[n] * t.s1[n]; + yi[n] = zi[n] * t.c1[n] + zr[n] * t.s1[n]; + } + for n in 0..N / 8 { + out[2 * n] = -yi[N / 8 + n] * w[2 * n]; + out[2 * n + 1] = yr[N / 8 - n - 1] * w[2 * n + 1]; + out[N / 4 + 2 * n] = -yr[n] * w[N / 4 + 2 * n]; + out[N / 4 + 2 * n + 1] = yi[N / 4 - n - 1] * w[N / 4 + 2 * n + 1]; + out[N / 2 + 2 * n] = -yr[N / 8 + n] * w[N / 2 - 2 * n - 1]; + out[N / 2 + 2 * n + 1] = yi[N / 8 - n - 1] * w[N / 2 - 2 * n - 2]; + out[3 * N / 4 + 2 * n] = yi[n] * w[N / 4 - 2 * n - 1]; + out[3 * N / 4 + 2 * n + 1] = -yr[N / 4 - n - 1] * w[N / 4 - 2 * n - 2]; + } + return; + } + let mut y = [([0f32; 64], [0f32; 64]); 2]; + for (h, yh) in y.iter_mut().enumerate() { + let (mut zr, mut zi) = ([0f32; 64], [0f32; 64]); + for k in 0..N / 8 { + let (a, b) = (x[2 * (N / 4 - 2 * k - 1) + h], x[2 * (2 * k) + h]); + zr[k] = a * t.c2[k] - b * t.s2[k]; + zi[k] = b * t.c2[k] + a * t.s2[k]; + } + ifft(&mut zr, &mut zi); + for n in 0..N / 8 { + yh.0[n] = zr[n] * t.c2[n] - zi[n] * t.s2[n]; + yh.1[n] = zi[n] * t.c2[n] + zr[n] * t.s2[n]; + } + } + let ((yr1, yi1), (yr2, yi2)) = (y[0], y[1]); + for n in 0..N / 8 { + out[2 * n] = -yi1[n] * w[2 * n]; + out[2 * n + 1] = yr1[N / 8 - n - 1] * w[2 * n + 1]; + out[N / 4 + 2 * n] = -yr1[n] * w[N / 4 + 2 * n]; + out[N / 4 + 2 * n + 1] = yi1[N / 8 - n - 1] * w[N / 4 + 2 * n + 1]; + out[N / 2 + 2 * n] = -yr2[n] * w[N / 2 - 2 * n - 1]; + out[N / 2 + 2 * n + 1] = yi2[N / 8 - n - 1] * w[N / 2 - 2 * n - 2]; + out[3 * N / 4 + 2 * n] = yi2[n] * w[N / 4 - 2 * n - 1]; + out[3 * N / 4 + 2 * n + 1] = -yr2[N / 8 - n - 1] * w[N / 4 - 2 * n - 2]; + } +} + +fn logadd(a: i32, b: i32) -> i32 { + let c = a - b; + let addr = ((c.abs() >> 1) as usize).min(255); + if c >= 0 { a + tables::LATAB[addr] as i32 } else { b + tables::LATAB[addr] as i32 } +} + +fn calc_lowcomp(a: i32, b0: i32, b1: i32, bin: usize) -> i32 { + if bin < 7 { + if b0 + 256 == b1 { + 384 + } else if b0 > b1 { + (a - 64).max(0) + } else { + a + } + } else if bin < 20 { + if b0 + 256 == b1 { + 320 + } else if b0 > b1 { + (a - 64).max(0) + } else { + a + } + } else { + (a - 128).max(0) + } +} + +/// Inputs to the bit allocation of one channel. +struct AllocParams { + start: usize, + end: usize, + fgain: i32, + snroffset: i32, + /// Coupling channel leak initialisation. + leak: Option<(i32, i32)>, + + /// Delta bit allocation segments (offset, length, ba) when in use. + dba: Option>, +} + +/// The parametric bit allocation of §7.2.2 for one exponent set. +fn bit_allocation(exp: &[u8; 256], p: &AllocParams, g: &Globals, bap: &mut [u8; 256]) { + let (start, end) = (p.start, p.end); + if start >= end { + return; + } + let (sdecay, fdecay, sgain, dbknee, floor) = + (SLOWDEC[g.sdcycod], FASTDEC[g.fdcycod], SLOWGAIN[g.sgaincod], DBPBTAB[g.dbpbcod], FLOORTAB[g.floorcod]); + let mut psd = [0i32; 256]; + for bin in start..end { + psd[bin] = 3072 - ((exp[bin] as i32) << 7); + } + // PSD integration + let mut bndpsd = [0i32; 50]; + let mut j = start; + let mut k = tables::MASKTAB[start] as usize; + loop { + let lastbin = (tables::BNDTAB[k] as usize + tables::BNDSZ[k] as usize).min(end); + bndpsd[k] = psd[j]; + j += 1; + while j < lastbin { + bndpsd[k] = logadd(bndpsd[k], psd[j]); + j += 1; + } + k += 1; + if end <= lastbin || k >= 50 { + break; + } + } + // excitation + let bndstrt = tables::MASKTAB[start] as usize; + let bndend = tables::MASKTAB[end - 1] as usize + 1; + let mut excite = [0i32; 50]; + let (mut fastleak, mut slowleak) = p.leak.unwrap_or((0, 0)); + let begin = if bndstrt == 0 { + // the last band of the LFE channel (bndend 7) has no band above it + let lfe_last = |bin: usize| !(bndend == 7 && bin == 6); + let mut lowcomp = 0; + lowcomp = calc_lowcomp(lowcomp, bndpsd[0], bndpsd[1], 0); + excite[0] = bndpsd[0] - p.fgain - lowcomp; + lowcomp = calc_lowcomp(lowcomp, bndpsd[1], bndpsd[2], 1); + excite[1] = bndpsd[1] - p.fgain - lowcomp; + let mut b = 7; + for bin in 2..7 { + if lfe_last(bin) { + lowcomp = calc_lowcomp(lowcomp, bndpsd[bin], bndpsd[bin + 1], bin); + } + fastleak = bndpsd[bin] - p.fgain; + slowleak = bndpsd[bin] - sgain; + excite[bin] = fastleak - lowcomp; + if lfe_last(bin) && bndpsd[bin] <= bndpsd[bin + 1] { + b = bin + 1; + break; + } + } + for bin in b..bndend.min(22) { + if lfe_last(bin) { + lowcomp = calc_lowcomp(lowcomp, bndpsd[bin], bndpsd[bin + 1], bin); + } + fastleak -= fdecay; + fastleak = fastleak.max(bndpsd[bin] - p.fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bndpsd[bin] - sgain); + excite[bin] = (fastleak - lowcomp).max(slowleak); + } + 22 + } else { + bndstrt + }; + for bin in begin..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(bndpsd[bin] - p.fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bndpsd[bin] - sgain); + excite[bin] = fastleak.max(slowleak); + } + // masking curve + let mut mask = [0i32; 50]; + for bin in bndstrt..bndend { + if bndpsd[bin] < dbknee { + excite[bin] += (dbknee - bndpsd[bin]) >> 2; + } + mask[bin] = excite[bin].max(tables::HTH[g.fscod][bin] as i32); + } + // delta bit allocation + if let Some(segs) = &p.dba { + let mut band = 0usize; + for &(off, len, ba) in segs { + band += off as usize; + let delta = if ba >= 4 { (ba as i32 - 3) << 7 } else { (ba as i32 - 4) << 7 }; + for _ in 0..len { + if band < 50 { + mask[band] += delta; + } + band += 1; + } + } + } + // bit allocation pointers + let mut i = start; + let mut j = tables::MASKTAB[start] as usize; + loop { + let lastbin = (tables::BNDTAB[j] as usize + tables::BNDSZ[j] as usize).min(end); + let mut m = mask[j] - p.snroffset - floor; + if m < 0 { + m = 0; + } + m &= 0x1fe0; + m += floor; + while i < lastbin { + let addr = ((psd[i] - m) >> 5).clamp(0, 63) as usize; + bap[i] = tables::BAPTAB[addr]; + i += 1; + } + j += 1; + if end <= lastbin || j >= 50 { + break; + } + } +} + +/// Bit allocation parameters common to all channels of a block. +#[derive(Clone, Copy, Default)] +struct Globals { + fscod: usize, + sdcycod: usize, + fdcycod: usize, + sgaincod: usize, + dbpbcod: usize, + floorcod: usize, +} + +/// State carried from block to block within a syncframe. +struct FrameState { + nfchans: usize, + blksw: [bool; 5], + dithflag: [bool; 5], + dynrng: [f32; 2], + cplinu: bool, + chincpl: [bool; 5], + phsflginu: bool, + cplbegf: usize, + /// cplendf + 3: one past the last coupling sub-band (with spectral extension cplendf may be + /// negative, §E3.3.1). + cplend: usize, + /// Coupling band of each coupling sub-band (relative to cplbegf). + sub_to_band: [usize; 18], + ncplbnd: usize, + cplco: [[f32; 18]; 5], + phsflg: [bool; 18], + rematflg: [bool; 4], + expstr: [u8; 7], + endmant: [usize; 5], + exps: [[u8; 256]; 7], + g: Globals, + csnroffst: i32, + fsnroffst: [i32; 7], + fgaincod: [usize; 7], + cplleak: (i32, i32), + deltbae: [u8; 7], + deltsegs: [Vec<(u8, u8, u8)>; 7], + bap: [[u8; 256]; 7], +} + +impl FrameState { + fn new(nfchans: usize, fscod: usize) -> FrameState { + FrameState { + nfchans, + blksw: [false; 5], + dithflag: [false; 5], + dynrng: [1.0; 2], + cplinu: false, + chincpl: [false; 5], + phsflginu: false, + cplbegf: 0, + cplend: 0, + sub_to_band: [0; 18], + ncplbnd: 0, + cplco: [[0.0; 18]; 5], + phsflg: [false; 18], + rematflg: [false; 4], + expstr: [0; 7], + endmant: [0; 5], + exps: [[0; 256]; 7], + g: Globals { fscod, ..Default::default() }, + csnroffst: 0, + fsnroffst: [0; 7], + fgaincod: [0; 7], + cplleak: (0, 0), + deltbae: [2; 7], + deltsegs: Default::default(), + bap: [[0; 256]; 7], + } + } + fn cplstrtmant(&self) -> usize { + 37 + 12 * self.cplbegf + } + fn cplendmant(&self) -> usize { + 37 + 12 * self.cplend + } +} + +/// dynrng gain (§7.7.1.2): X (3-bit signed) gives 6.02 dB steps, Y a linear factor in [1/2, 1). +fn dynrng_gain(v: u32) -> f32 { + let x = ((v as i32) << 24) >> 29; + let y = (v & 0x1F) as f32; + 2f32.powi(x + 1) * (32.0 + y) / 64.0 +} + +/// Decode exponents of one set (§7.1.3) into `exp[first..]`; `ngrps` groups of 3 mapped values, +/// each expanded to `grpsize` bins after the absolute exponent. +fn decode_exponents(r: &mut Bits, absexp: i32, ngrps: usize, grpsize: usize, exp: &mut [u8; 256], first: usize, skip_abs: bool) -> Result<()> { + let mut prev = absexp; + let mut at = first; + if !skip_abs { + exp[at] = absexp.clamp(0, 24) as u8; + at += 1; + } + for _ in 0..ngrps { + let g = r.read(7) as i32; + if g > 124 { + return Err(Error::Invalid("exponent group out of range")); + } + for d in [g / 25, (g % 25) / 5, g % 5] { + prev += d - 2; + if !(0..=24).contains(&prev) { + return Err(Error::Invalid("exponent out of range")); + } + for _ in 0..grpsize { + if at < 256 { + exp[at] = prev as u8; + } + at += 1; + } + } + } + Ok(()) +} + +/// Grouped-mantissa buffers shared by all exponent sets of a block (§7.3.5). +#[derive(Default)] +struct Groups { + b1: ([f32; 3], usize), + b2: ([f32; 3], usize), + b4: ([f32; 2], usize), +} + +const fn sym(levels: i32, code: i32) -> f32 { + (2 * code - (levels - 1)) as f32 / levels as f32 +} + +/// The mantissa value (before the exponent shift) of a bin with allocation `bap`. +fn mantissa(r: &mut Bits, bap: u8, gr: &mut Groups) -> f32 { + match bap { + 1 => { + if gr.b1.1 == 0 { + let g = r.read(5) as i32; + gr.b1.0 = [sym(3, g / 9), sym(3, (g % 9) / 3), sym(3, g % 3)]; + gr.b1.1 = 3; + } + gr.b1.1 -= 1; + gr.b1.0[2 - gr.b1.1] + } + 2 => { + if gr.b2.1 == 0 { + let g = r.read(7) as i32; + gr.b2.0 = [sym(5, g / 25), sym(5, (g % 25) / 5), sym(5, g % 5)]; + gr.b2.1 = 3; + } + gr.b2.1 -= 1; + gr.b2.0[2 - gr.b2.1] + } + 3 => sym(7, r.read(3) as i32), + 4 => { + if gr.b4.1 == 0 { + let g = r.read(7) as i32; + gr.b4.0 = [sym(11, g / 11), sym(11, g % 11)]; + gr.b4.1 = 2; + } + gr.b4.1 -= 1; + gr.b4.0[1 - gr.b4.1] + } + 5 => sym(15, r.read(4) as i32), + _ => { + let bits = QNTZTAB[bap as usize & 15]; + let v = r.read(bits) as i32; + let v = (v << (32 - bits)) >> (32 - bits); + v as f32 / (1u32 << (bits - 1)) as f32 + } + } +} + +/// A decoder: keeps the overlap of each channel between frames. +pub struct Decoder { + delay: [[f32; 256]; 6], + dither: u32, + seed: u32, + /// E-AC-3: the block state of the last syncframe (acmod, lfeon, sample rate code), whose + /// exponents a syncframe of fewer than six blocks may reuse. + carry: Option<(usize, bool, usize, Box)>, +} + +impl Default for Decoder { + fn default() -> Self { + Self::new() + } +} + +/// The name FilmCraft's `filmcraft-ac3` used, kept so it can re-export this module. +#[doc(hidden)] +pub type Decoded = Frame; + +/// A decoded syncframe: [`Header::samples`] samples per channel (1536 for AC-3), WAV channel +/// order. No channels for an E-AC-3 syncframe that is not [`Header::is_primary`]. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub struct Frame { + /// The syncframe's header. + pub header: Header, + /// Planar samples, one `Vec` per output channel. + pub channels: Vec>, +} + +impl Decoder { + /// A decoder with empty overlap state. + pub fn new() -> Decoder { + Decoder { delay: [[0.0; 256]; 6], dither: 0, seed: 0x1234_5678, carry: None } + } + + /// Seed the dither generator (dither is decoder-specific noise; tests compare two seeds). + pub fn set_dither_seed(&mut self, seed: u32) { + self.seed = seed; + } + + /// Forget the overlap (before decoding from another position). + pub fn reset(&mut self) { + self.delay = [[0.0; 256]; 6]; + self.carry = None; + } + + fn dither(&mut self) -> f32 { + // uniform in [-0.707, 0.707] (§7.3.4; any reasonably random sequence) + self.dither = self.dither.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + ((self.dither >> 8) as f32 / (1u32 << 24) as f32 * 2.0 - 1.0) * 0.707 + } + + /// Zero-mean, unit-variance noise for spectral extension (§E3.6.4.2.4): uniform in ±√3. + fn noise(&mut self) -> f32 { + self.dither() * (3f32.sqrt() / 0.707) + } + + /// Decode one syncframe. + pub fn decode(&mut self, frame: &[u8]) -> Result { + let h = parse_header(frame)?; + if frame.len() < h.frame_bytes { + return Err(Error::Invalid("truncated syncframe")); + } + if h.is_eac3() { + return self.decode_eac3(&frame[..h.frame_bytes], h); + } + let fscod = (frame[4] >> 6) as usize; + // Dither is reseeded per syncframe from its CRC words (plus the configured seed), so a + // frame decodes to the same samples however it was reached (seeking, render caches). + let crcs = u32::from_be_bytes([frame[2], frame[3], frame[h.frame_bytes - 2], frame[h.frame_bytes - 1]]); + self.dither = self.seed ^ crcs.wrapping_mul(0x9E37_79B9); + let mut r = Bits::new(&frame[..h.frame_bytes]); + r.skip(40); // syncinfo + // bsi + r.skip(8); // bsid, bsmod + let acmod = r.read(3) as usize; + if acmod & 1 != 0 && acmod != 1 { + r.skip(2); + } + if acmod & 4 != 0 { + r.skip(2); + } + if acmod == 2 { + r.skip(2); + } + let lfeon = r.bit(); + for k in 0..if acmod == 0 { 2 } else { 1 } { + let _ = k; + r.skip(5); // dialnorm + if r.bit() { + r.skip(8); // compr + } + if r.bit() { + r.skip(8); // langcod + } + if r.bit() { + r.skip(7); // mixlevel, roomtyp + } + } + r.skip(2); // copyrightb, origbs + if r.bit() { + r.skip(14); + } + if r.bit() { + r.skip(14); + } + if r.bit() { + let l = r.read(6) as usize; + r.skip((l + 1) * 8); + } + let mut f = FrameState::new(NFCHANS[acmod], fscod); + let pcm = self.blocks(&mut r, &mut f, acmod, lfeon, 6, None)?; + Ok(Frame { header: h, channels: wav_order(pcm, acmod, lfeon) }) + } + + /// Decode one E-AC-3 syncframe (`frame` is exactly the syncframe). + fn decode_eac3(&mut self, frame: &[u8], h: Header) -> Result { + if !h.is_primary() { + return Ok(Frame { header: h, channels: Vec::new() }); + } + // E-AC-3 has a single CRC, at the end (§E2.2.6): reseed the dither from the last words + let n = frame.len(); + let tail = frame.get(n.saturating_sub(4)..).and_then(|t| <[u8; 4]>::try_from(t).ok()).unwrap_or_default(); + self.dither = self.seed ^ u32::from_be_bytes(tail).wrapping_mul(0x9E37_79B9); + let mut r = Bits::new(frame); + let bsi = eac3::read_bsi(&mut r)?; + let (acmod, lfeon) = (h.acmod as usize & 7, h.lfeon); + let nblocks = h.blocks as usize; + let mut e = eac3::FrameInfo::read(&mut r, &bsi, acmod, lfeon, nblocks, n / 2)?; + let mut f = match self.carry.take() { + Some((a, l, fs, f)) if (a, l, fs) == (acmod, lfeon, bsi.fscod) => f, + _ => Box::new(FrameState::new(NFCHANS[acmod], bsi.fscod)), + }; + let pcm = self.blocks(&mut r, &mut f, acmod, lfeon, nblocks, Some(&mut e))?; + self.carry = Some((acmod, lfeon, bsi.fscod, f)); + Ok(Frame { header: h, channels: wav_order(pcm, acmod, lfeon) }) + } + + /// Decode `nblocks` audio blocks and synthesize them (inverse transform, overlap-add), in coded + /// channel order with the LFE last. + fn blocks( + &mut self, + r: &mut Bits, + f: &mut FrameState, + acmod: usize, + lfeon: bool, + nblocks: usize, + mut x: Option<&mut eac3::FrameInfo>, + ) -> Result>> { + let nfchans = f.nfchans; + let nout = nfchans + lfeon as usize; + let mut pcm: Vec> = vec![Vec::with_capacity(256 * nblocks); nout]; + for blk in 0..nblocks { + let mut coefs = [[0f32; 256]; 6]; + self.audio_block(r, f, acmod, lfeon, blk, &mut coefs, x.as_deref_mut())?; + if r.overrun() { + return Err(Error::Invalid("audio block runs past the frame")); + } + // inverse transform, overlap-add + let mut buf = [0f32; 512]; + for (ch, out) in pcm.iter_mut().enumerate() { + let src = if ch < nfchans { ch } else { LFE }; + let short = ch < nfchans && f.blksw[ch]; + imdct(&coefs[src], short, &mut buf); + let d = &mut self.delay[src]; + for n in 0..256 { + out.push(2.0 * (buf[n] + d[n])); + d[n] = buf[256 + n]; + } + } + } + Ok(pcm) + } + + #[allow(clippy::cognitive_complexity, reason = "the A/52 audio block decode, ported as-is from FilmCraft")] + #[allow(clippy::too_many_arguments)] + fn audio_block( + &mut self, + r: &mut Bits, + f: &mut FrameState, + acmod: usize, + lfeon: bool, + blk: usize, + coefs: &mut [[f32; 256]; 6], + mut x: Option<&mut eac3::FrameInfo>, + ) -> Result<()> { + let nf = f.nfchans; + let eac3 = x.is_some(); + match x.as_deref() { + Some(e) if !e.blkswe => f.blksw = [false; 5], + _ => (0..nf).for_each(|ch| f.blksw[ch] = r.bit()), + } + match x.as_deref() { + // dither on when the flags are not sent (§E2.2.4) + Some(e) if !e.dithflage => f.dithflag = [true; 5], + _ => (0..nf).for_each(|ch| f.dithflag[ch] = r.bit()), + } + for k in 0..if acmod == 0 { 2 } else { 1 } { + if r.bit() { + f.dynrng[k] = dynrng_gain(r.read(8)); + } else if blk == 0 { + f.dynrng[k] = 1.0; + } + } + // E-AC-3 spectral extension strategy and coordinates + if let Some(e) = x.as_deref_mut() { + e.spx.read(r, blk, acmod, nf)?; + } + let spx = x.as_deref().map(|e| &e.spx).filter(|s| s.inu); + let spxbegf = spx.map(|s| s.begf); + let spx_begin = spx.map(|s| s.begin_bin()); + let spx_chans = spx.map(|s| s.chinspx).unwrap_or([false; 5]); + // coupling strategy + let strategy = match x.as_deref() { + Some(e) => e.cplstre[blk].then_some(e.cplinu[blk]), + None => r.bit().then(|| r.bit()), + }; + if let Some(inu) = strategy { + f.cplinu = inu; + if f.cplinu { + if eac3 && r.bit() { + return Err(Error::Unsupported("E-AC-3 enhanced coupling".into())); + } + if eac3 && acmod == 2 { + f.chincpl = [true, true, false, false, false]; + } else { + for ch in 0..nf { + f.chincpl[ch] = r.bit(); + } + } + if acmod == 2 { + f.phsflginu = r.bit(); + } + f.cplbegf = r.read(4) as usize; + f.cplend = match spxbegf { + // the coupling region ends where spectral extension begins (§E2.2.4) + Some(b) if b < 6 => b + 1, + Some(b) => 2 * b - 4, + None => r.read(4) as usize + 3, + }; + if f.cplend <= f.cplbegf { + return Err(Error::Invalid("coupling end below start")); + } + // coupling band structure, by absolute sub-band + let mut strc = [false; 18]; + match x.as_deref_mut() { + Some(e) => { + if r.bit() { + for s in f.cplbegf + 1..f.cplend { + e.cplbndstrc[s] = r.bit(); + } + } + strc = e.cplbndstrc; + } + None => (f.cplbegf + 1..f.cplend).for_each(|s| strc[s] = r.bit()), + } + let mut band = 0; + f.sub_to_band[0] = 0; + for s in 1..f.cplend - f.cplbegf { + if !strc[f.cplbegf + s] { + band += 1; + } + f.sub_to_band[s] = band; + } + f.ncplbnd = band + 1; + } else { + f.chincpl = [false; 5]; + if let Some(e) = x.as_deref_mut() { + e.firstcplcos = [true; 5]; + e.firstcplleak = true; + f.phsflginu = false; + } + } + } else if blk == 0 && !eac3 { + return Err(Error::Invalid("no coupling strategy in block 0")); + } + // coupling coordinates + if f.cplinu { + let mut any = false; + for ch in 0..nf { + if !f.chincpl[ch] { + if let Some(e) = x.as_deref_mut() { + e.firstcplcos[ch] = true; + } + continue; + } + let coe = match x.as_deref_mut() { + Some(e) if e.firstcplcos[ch] => { + e.firstcplcos[ch] = false; + true + } + _ => r.bit(), + }; + if coe { + any = true; + let mstr = r.read(2) as i32; + for bnd in 0..f.ncplbnd { + let e = r.read(4) as i32; + let m = r.read(4) as f32; + let v = if e == 15 { m / 16.0 } else { (m + 16.0) / 32.0 }; + let co = v * 2f32.powi(-(e + 3 * mstr)); + for s in 0..(f.cplend - f.cplbegf) { + if f.sub_to_band[s] == bnd { + f.cplco[ch][s] = co; + } + } + } + } + } + if acmod == 2 && f.phsflginu && any { + for bnd in 0..f.ncplbnd { + let flag = r.bit(); + for s in 0..(f.cplend - f.cplbegf) { + if f.sub_to_band[s] == bnd { + f.phsflg[s] = flag; + } + } + } + } + } + // rematrixing (E-AC-3 always sends the flags in block 0) + if acmod == 2 && ((eac3 && blk == 0) || r.bit()) { + let n = if f.cplinu { + match f.cplbegf { + 0 => 2, + 1 | 2 => 3, + _ => 4, + } + } else { + match spxbegf { + Some(b) if b < 2 => 3, + _ => 4, + } + }; + f.rematflg = [false; 4]; + for k in 0..n { + f.rematflg[k] = r.bit(); + } + } + // exponent strategies (E-AC-3: from the audio frame) + match x.as_deref() { + Some(e) => { + if f.cplinu { + f.expstr[CPL] = e.cplexpstr[blk]; + } + f.expstr[..nf].copy_from_slice(&e.chexpstr[blk][..nf]); + if lfeon { + f.expstr[LFE] = e.lfeexpstr[blk]; + } + } + None => { + if f.cplinu { + f.expstr[CPL] = r.read(2) as u8; + } + for ch in 0..nf { + f.expstr[ch] = r.read(2) as u8; + } + if lfeon { + f.expstr[LFE] = r.read(1) as u8; + } + } + } + for ch in 0..nf { + if f.expstr[ch] != 0 { + if f.chincpl[ch] && f.cplinu { + f.endmant[ch] = f.cplstrtmant(); + } else if let (true, Some(begin)) = (spx_chans[ch], spx_begin) { + // §E3.3.3: the channel ends where spectral extension begins + f.endmant[ch] = begin; + } else { + let bw = r.read(6) as usize; + if bw > 60 { + return Err(Error::Invalid("chbwcod > 60")); + } + f.endmant[ch] = (bw + 12) * 3 + 37; + } + } + } + // exponents + if f.cplinu && f.expstr[CPL] != 0 { + let abs = (r.read(4) as i32) << 1; + let grpsize = 1 << (f.expstr[CPL] - 1); + let ngrps = (f.cplendmant() - f.cplstrtmant()) / (3 * grpsize); + let first = f.cplstrtmant(); + decode_exponents(r, abs, ngrps, grpsize, &mut f.exps[CPL], first, true)?; + } + for ch in 0..nf { + if f.expstr[ch] != 0 { + let abs = r.read(4) as i32; + let grpsize = 1usize << (f.expstr[ch] - 1); + let ngrps = (f.endmant[ch] - 1 + 3 * grpsize - 3) / (3 * grpsize); + decode_exponents(r, abs, ngrps, grpsize, &mut f.exps[ch], 0, false)?; + r.skip(2); // gainrng + } + } + if lfeon && f.expstr[LFE] != 0 { + let abs = r.read(4) as i32; + decode_exponents(r, abs, 2, 1, &mut f.exps[LFE], 0, false)?; + } + // bit allocation parametric information + match x.as_deref() { + Some(e) if !e.bamode => { + // §E2.2.4 defaults + (f.g.sdcycod, f.g.fdcycod, f.g.sgaincod, f.g.dbpbcod, f.g.floorcod) = (2, 1, 1, 2, 7); + } + _ => { + if r.bit() { + f.g.sdcycod = r.read(2) as usize; + f.g.fdcycod = r.read(2) as usize; + f.g.sgaincod = r.read(2) as usize; + f.g.dbpbcod = r.read(2) as usize; + f.g.floorcod = r.read(3) as usize; + } else if blk == 0 && !eac3 { + return Err(Error::Invalid("no bit allocation information in block 0")); + } + } + } + match x.as_deref() { + Some(e) => { + // SNR offset strategies (Table E2.9), fast gain codes + let sets = [CPL, 0, 1, 2, 3, 4, LFE]; + match e.snroffststr { + 0 => { + f.csnroffst = e.frmcsnroffst; + sets.iter().for_each(|&c| f.fsnroffst[c] = e.frmfsnroffst); + } + s if blk == 0 || r.bit() => { + f.csnroffst = r.read(6) as i32; + if s == 1 { + let v = r.read(4) as i32; + sets.iter().for_each(|&c| f.fsnroffst[c] = v); + } else { + if f.cplinu { + f.fsnroffst[CPL] = r.read(4) as i32; + } + for ch in 0..nf { + f.fsnroffst[ch] = r.read(4) as i32; + } + if lfeon { + f.fsnroffst[LFE] = r.read(4) as i32; + } + } + } + _ => {} + } + if e.frmfgaincode && r.bit() { + if f.cplinu { + f.fgaincod[CPL] = r.read(3) as usize; + } + for ch in 0..nf { + f.fgaincod[ch] = r.read(3) as usize; + } + if lfeon { + f.fgaincod[LFE] = r.read(3) as usize; + } + } else { + f.fgaincod = [4; 7]; + } + if e.strmtyp == 0 && r.bit() { + r.skip(10); // convsnroffst + } + } + None => { + if r.bit() { + f.csnroffst = r.read(6) as i32; + if f.cplinu { + f.fsnroffst[CPL] = r.read(4) as i32; + f.fgaincod[CPL] = r.read(3) as usize; + } + for ch in 0..nf { + f.fsnroffst[ch] = r.read(4) as i32; + f.fgaincod[ch] = r.read(3) as usize; + } + if lfeon { + f.fsnroffst[LFE] = r.read(4) as i32; + f.fgaincod[LFE] = r.read(3) as usize; + } + } else if blk == 0 { + return Err(Error::Invalid("no SNR offsets in block 0")); + } + } + } + if f.cplinu { + let leak = match x.as_deref_mut() { + Some(e) if e.firstcplleak => { + e.firstcplleak = false; + true + } + _ => r.bit(), + }; + if leak { + f.cplleak = (((r.read(3) as i32) << 8) + 768, ((r.read(3) as i32) << 8) + 768); + } + } + // delta bit allocation + if blk == 0 { + f.deltbae = [2; 7]; + } + if x.as_deref().is_none_or(|e| e.dbaflde) && r.bit() { + if f.cplinu { + f.deltbae[CPL] = r.read(2) as u8; + } + for ch in 0..nf { + f.deltbae[ch] = r.read(2) as u8; + } + let order: Vec = f.cplinu.then_some(CPL).into_iter().chain(0..nf).collect(); + for ch in order { + if f.deltbae[ch] == 1 { + let n = r.read(3) as usize + 1; + f.deltsegs[ch] = (0..n).map(|_| (r.read(5) as u8, r.read(4) as u8, r.read(3) as u8)).collect(); + } else if f.deltbae[ch] == 3 { + return Err(Error::Invalid("reserved delta bit allocation state")); + } + } + } + // skip field + if x.as_deref().is_none_or(|e| e.skipflde) && r.bit() { + let l = r.read(9) as usize; + r.skip(l * 8); + } + // bit allocation for every exponent set + let all_zero = f.csnroffst == 0 && (0..7).all(|c| f.fsnroffst[c] == 0); + let snr = |fine: i32| (((f.csnroffst - 15) << 4) + fine) << 2; + let mut sets: Vec<(usize, AllocParams)> = Vec::new(); + for ch in 0..nf { + let dba = (f.deltbae[ch] <= 1).then(|| f.deltsegs[ch].clone()); + sets.push(( + ch, + AllocParams { start: 0, end: f.endmant[ch], fgain: FASTGAIN[f.fgaincod[ch]], snroffset: snr(f.fsnroffst[ch]), leak: None, dba }, + )); + } + if f.cplinu { + let dba = (f.deltbae[CPL] <= 1).then(|| f.deltsegs[CPL].clone()); + sets.push(( + CPL, + AllocParams { + start: f.cplstrtmant(), + end: f.cplendmant(), + fgain: FASTGAIN[f.fgaincod[CPL]], + snroffset: snr(f.fsnroffst[CPL]), + leak: Some(f.cplleak), + dba, + }, + )); + } + if lfeon { + sets.push(( + LFE, + AllocParams { start: 0, end: 7, fgain: FASTGAIN[f.fgaincod[LFE]], snroffset: snr(f.fsnroffst[LFE]), leak: None, dba: None }, + )); + } + for (ch, p) in &sets { + f.bap[*ch] = [0; 256]; + if !all_zero { + let exps = f.exps[*ch]; + bit_allocation(&exps, p, &f.g, &mut f.bap[*ch]); + } + } + // mantissas + let mut gr = Groups::default(); + let mut cpl = [0f32; 256]; + let mut got_cpl = false; + for ch in 0..nf { + for bin in 0..f.endmant[ch] { + let b = f.bap[ch][bin]; + let m = if b == 0 { if f.dithflag[ch] { self.dither() } else { 0.0 } } else { mantissa(r, b, &mut gr) }; + coefs[ch][bin] = m * 2f32.powi(-(f.exps[ch][bin] as i32)); + } + if f.cplinu && f.chincpl[ch] && !got_cpl { + got_cpl = true; + for bin in f.cplstrtmant()..f.cplendmant() { + let b = f.bap[CPL][bin]; + // zero-bit coupling mantissas are dithered per channel when decoupling + cpl[bin] = if b == 0 { f32::NAN } else { mantissa(r, b, &mut gr) }; + } + } + } + if lfeon { + for bin in 0..7 { + let b = f.bap[LFE][bin]; + let m = if b == 0 { 0.0 } else { mantissa(r, b, &mut gr) }; + coefs[LFE][bin] = m * 2f32.powi(-(f.exps[LFE][bin] as i32)); + } + } + // decoupling + if f.cplinu { + for ch in 0..nf { + if !f.chincpl[ch] { + continue; + } + for bin in f.cplstrtmant()..f.cplendmant() { + let s = (bin - 37) / 12 - f.cplbegf; + let m = if cpl[bin].is_nan() { if f.dithflag[ch] { self.dither() } else { 0.0 } } else { cpl[bin] }; + let mut v = m * 2f32.powi(-(f.exps[CPL][bin] as i32)) * f.cplco[ch][s] * 8.0; + if acmod == 2 && ch == 1 && f.phsflginu && f.phsflg[s] { + v = -v; + } + coefs[ch][bin] = v; + } + } + } + // rematrixing + if acmod == 2 { + let end = if f.cplinu { f.cplstrtmant() } else { f.endmant[0].min(f.endmant[1]) }; + const B: [usize; 5] = [13, 25, 37, 61, 253]; + for k in 0..4 { + if f.rematflg[k] { + for bin in B[k]..B[k + 1].min(end) { + let (l, rr) = (coefs[0][bin], coefs[1][bin]); + coefs[0][bin] = l + rr; + coefs[1][bin] = l - rr; + } + } + } + } + // spectral extension: synthesize the high band of each channel from its low band + if let Some(e) = x.as_deref() + && e.spx.inu + { + for ch in 0..nf { + if e.spx.chinspx[ch] { + e.spx.synthesize(ch, e.spxatten[ch], &mut coefs[ch], || self.noise()); + } + } + } + // dynamic range + for ch in 0..nf { + let g = if acmod == 0 && ch == 1 { f.dynrng[1] } else { f.dynrng[0] }; + if g != 1.0 { + coefs[ch].iter_mut().for_each(|c| *c *= g); + } + } + if lfeon && f.dynrng[0] != 1.0 { + coefs[LFE].iter_mut().for_each(|c| *c *= f.dynrng[0]); + } + Ok(()) + } +} + +/// Coded channel order (Table 5.8) → WAV / SMPTE order: fronts L R C, LFE, surrounds. +fn wav_order(mut pcm: Vec>, acmod: usize, lfeon: bool) -> Vec> { + let nf = NFCHANS[acmod]; + let lfe = if lfeon { pcm.pop() } else { None }; + let mut fbw = pcm; + // acmod 3, 5, 7 code L C R …: move C after R + if acmod & 1 == 1 && acmod != 1 { + let c = fbw.remove(1); + fbw.insert(2, c); + } + let fronts = match acmod { + 1 => 1, + 3 | 5 | 7 => 3, + _ => 2.min(nf), + }; + let mut out: Vec> = fbw.drain(..fronts).collect(); + if let Some(l) = lfe { + out.push(l); + } + out.extend(fbw); + out +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/crates/audio/src/ac3/tables.rs b/crates/audio/src/ac3/tables.rs new file mode 100644 index 000000000..0128e8960 --- /dev/null +++ b/crates/audio/src/ac3/tables.rs @@ -0,0 +1,71 @@ +// Generated from the tables of ATSC A/52:2012 (Tables 7.12-7.16, 7.33). +pub(crate) const MASKTAB: [u8; 256] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, + 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39, + 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, + 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, 47, + 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 0, 0, 0, +]; + +pub(crate) const LATAB: [u16; 256] = [ + 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 52, 51, 50, 49, 48, 47, 47, 46, 45, 44, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 36, 36, 35, + 35, 34, 33, 33, 32, 32, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22, 21, 21, 21, 20, 20, 19, 19, 19, 18, 18, 18, + 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, + 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, +]; + +pub(crate) const BAPTAB: [u8; 64] = [ + 0, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, + 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, +]; + +pub(crate) const BNDTAB: [u8; 50] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 34, 37, 40, 43, 46, 49, 55, 61, 67, + 73, 79, 85, 97, 109, 121, 133, 157, 181, 205, 229, +]; + +pub(crate) const BNDSZ: [u8; 50] = [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 24, + 24, 24, 24, 24, +]; + +pub(crate) const HTH: [[u16; 50]; 3] = [ + [ + 1232, 1232, 1088, 1024, 992, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 880, 880, 864, 864, 848, 848, 832, 832, 816, + 800, 784, 768, 752, 752, 752, 752, 768, 784, 832, 912, 992, 1056, 1120, 1168, 1184, 1120, 1088, 1088, 1312, 2048, 2112, 2112, + ], + [ + 1264, 1264, 1120, 1040, 992, 976, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 896, 880, 880, 864, 864, 848, 848, 832, + 832, 800, 784, 768, 752, 752, 752, 752, 768, 800, 848, 912, 992, 1056, 1104, 1184, 1168, 1120, 1088, 1152, 1584, 2112, 2112, + ], + [ + 1408, 1408, 1200, 1104, 1056, 1008, 992, 976, 960, 944, 944, 944, 928, 928, 928, 928, 928, 928, 928, 928, 912, 912, 912, 912, 896, 896, 896, + 880, 864, 848, 832, 816, 800, 784, 768, 752, 752, 752, 768, 784, 816, 848, 960, 1040, 1136, 1184, 1120, 1088, 1104, 1248, + ], +]; +/// Table 7.33 as printed (5 decimals), for checking the computed window. +#[cfg(test)] +#[allow(clippy::approx_constant)] +pub(crate) const WINDOW_TABLE: [f32; 256] = [ + 0.00014, 0.00024, 0.00037, 0.00051, 0.00067, 0.00086, 0.00107, 0.0013, 0.00157, 0.00187, 0.0022, 0.00256, 0.00297, 0.00341, 0.0039, 0.00443, + 0.00501, 0.00564, 0.00632, 0.00706, 0.00785, 0.00871, 0.00962, 0.01061, 0.01166, 0.01279, 0.01399, 0.01526, 0.01662, 0.01806, 0.01959, 0.02121, + 0.02292, 0.02472, 0.02662, 0.02863, 0.03073, 0.03294, 0.03527, 0.0377, 0.04025, 0.04292, 0.04571, 0.04862, 0.05165, 0.05481, 0.0581, 0.06153, + 0.06508, 0.06878, 0.07261, 0.07658, 0.08069, 0.08495, 0.08935, 0.09389, 0.09859, 0.10343, 0.10842, 0.11356, 0.11885, 0.12429, 0.12988, 0.13563, + 0.14152, 0.14757, 0.15376, 0.16011, 0.16661, 0.17325, 0.18005, 0.18699, 0.19407, 0.2013, 0.20867, 0.21618, 0.22382, 0.23161, 0.23952, 0.24757, + 0.25574, 0.26404, 0.27246, 0.281, 0.28965, 0.29841, 0.30729, 0.31626, 0.32533, 0.3345, 0.34376, 0.35311, 0.36253, 0.37204, 0.38161, 0.39126, + 0.40096, 0.41072, 0.42054, 0.4304, 0.4403, 0.45023, 0.4602, 0.47019, 0.4802, 0.49022, 0.50025, 0.51028, 0.52031, 0.53033, 0.54033, 0.55031, + 0.56026, 0.57019, 0.58007, 0.58991, 0.5997, 0.60944, 0.61912, 0.62873, 0.63827, 0.64774, 0.65713, 0.66643, 0.67564, 0.68476, 0.69377, 0.70269, + 0.7115, 0.72019, 0.72877, 0.73723, 0.74557, 0.75378, 0.76186, 0.76981, 0.77762, 0.7853, 0.79283, 0.80022, 0.80747, 0.81457, 0.82151, 0.82831, + 0.83496, 0.84145, 0.84779, 0.85398, 0.86001, 0.86588, 0.8716, 0.87716, 0.88257, 0.88782, 0.89291, 0.89785, 0.90264, 0.90728, 0.91176, 0.9161, + 0.92028, 0.92432, 0.92822, 0.93197, 0.93558, 0.93906, 0.9424, 0.9456, 0.94867, 0.95162, 0.95444, 0.95713, 0.95971, 0.96217, 0.96451, 0.96674, + 0.96887, 0.97089, 0.97281, 0.97463, 0.97635, 0.97799, 0.97953, 0.98099, 0.98236, 0.98366, 0.98488, 0.98602, 0.9871, 0.98811, 0.98905, 0.98994, + 0.99076, 0.99153, 0.99225, 0.99291, 0.99353, 0.99411, 0.99464, 0.99513, 0.99558, 0.996, 0.99639, 0.99674, 0.99706, 0.99736, 0.99763, 0.99788, + 0.99811, 0.99831, 0.9985, 0.99867, 0.99882, 0.99895, 0.99908, 0.99919, 0.99929, 0.99938, 0.99946, 0.99953, 0.99959, 0.99965, 0.99969, 0.99974, + 0.99978, 0.99981, 0.99984, 0.99986, 0.99988, 0.9999, 0.99992, 0.99993, 0.99994, 0.99995, 0.99996, 0.99997, 0.99998, 0.99998, 0.99998, 0.99999, + 0.99999, 0.99999, 0.99999, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, +]; diff --git a/crates/audio/src/ac3/tests.rs b/crates/audio/src/ac3/tests.rs new file mode 100644 index 000000000..830347a49 --- /dev/null +++ b/crates/audio/src/ac3/tests.rs @@ -0,0 +1,274 @@ +use super::*; + +#[test] +fn computed_window_matches_table_7_33() { + let w = window(); + for (n, (&a, &b)) in w.iter().zip(tables::WINDOW_TABLE.iter()).enumerate() { + assert!((a - b).abs() <= 6e-6, "w[{n}] = {a}, table {b}"); + } + // princen-bradley: w[n]² + w[255-n]²... (the second half mirrors): w[n]² + w[511-n]² = 1 + for n in 0..256 { + let m = w[255 - n]; + assert!((w[n] * w[n] + m * m - 1.0).abs() < 1e-5); + } +} + +#[test] +fn tables_are_consistent() { + // masktab maps every bin of a band to that band + for b in 0..50 { + for bin in tables::BNDTAB[b] as usize..(tables::BNDTAB[b] + tables::BNDSZ[b]) as usize { + if bin < 253 { + assert_eq!(tables::MASKTAB[bin] as usize, b, "bin {bin}"); + } + } + } + assert_eq!(tables::BNDTAB[49] as usize + tables::BNDSZ[49] as usize, 253); + // latab is non-increasing from 64 to 0 + assert_eq!(tables::LATAB[0], 64); + assert!(tables::LATAB.windows(2).all(|w| w[0] >= w[1])); + assert!(tables::BAPTAB.windows(2).all(|w| w[0] <= w[1])); + assert_eq!((tables::BAPTAB[0], tables::BAPTAB[63]), (0, 15)); +} + +#[test] +fn inverse_fft_matches_the_definition() { + for n in [64usize, 128] { + let re0: Vec = (0..n).map(|i| ((i * 7 + 3) % 11) as f32 - 5.0).collect(); + let im0: Vec = (0..n).map(|i| ((i * 5 + 1) % 13) as f32 - 6.0).collect(); + let (mut re, mut im) = (re0.clone(), im0.clone()); + ifft(&mut re, &mut im); + for t in 0..n { + let (mut sr, mut si) = (0f64, 0f64); + for k in 0..n { + let a = 2.0 * std::f64::consts::PI * (k * t) as f64 / n as f64; + sr += re0[k] as f64 * a.cos() - im0[k] as f64 * a.sin(); + si += re0[k] as f64 * a.sin() + im0[k] as f64 * a.cos(); + } + assert!((sr - re[t] as f64).abs() < 1e-3 && (si - im[t] as f64).abs() < 1e-3, "n {n} t {t}"); + } + } +} + +#[test] +fn headers_and_gains() { + // 48 kHz, 448 kb/s, 3/2 + LFE: bsid 8, bsmod 0, acmod 7 (cmixlev, surmixlev), lfeon + let b = [0x0B, 0x77, 0, 0, 30, 8 << 3, 0b1110_0001, 0b0100_0000]; + let h = parse_header(&b).unwrap(); + assert_eq!((h.sample_rate, h.bitrate_kbps, h.frame_bytes, h.acmod, h.lfeon, h.channels()), (48_000, 448, 1792, 7, true, 6)); + assert_eq!(parse_header(&[0x0B, 0x77, 0, 0, 0, 17 << 3, 0, 0]), Err(Error::Unsupported("bsid 17".into()))); + assert_eq!(dynrng_gain(0), 1.0); + assert!((dynrng_gain(0b1110_0000) - 0.5).abs() < 1e-7); + assert!((dynrng_gain(0b0111_1111) - 16.0 * 63.0 / 64.0).abs() < 1e-5); +} + +#[test] +fn channel_order() { + let ch = |n: usize| (0..n).map(|i| vec![i as f32]).collect::>(); + let ids = |v: Vec>| v.iter().map(|c| c[0] as usize).collect::>(); + // coded L C R Ls Rs LFE → L R C LFE Ls Rs + assert_eq!(ids(wav_order(ch(6), 7, true)), vec![0, 2, 1, 5, 3, 4]); + assert_eq!(ids(wav_order(ch(3), 2, true)), vec![0, 1, 2]); + assert_eq!(ids(wav_order(ch(1), 1, false)), vec![0]); +} + +#[test] +fn garbage_never_panics() { + let mut d = Decoder::new(); + let mut x = 0xACu32; + for len in [0usize, 5, 8, 100, 2000] { + for _ in 0..200 { + let mut f: Vec = (0..len) + .map(|_| { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x as u8 + }) + .collect(); + if len >= 6 { + f[0] = 0x0B; + f[1] = 0x77; + f[4] &= 0x3F; + f[5] = (f[5] & 7) | (8 << 3); + } + let _ = d.decode(&f); + } + } +} + +/// MSB-first bit writer for synthetic E-AC-3 syncframes. +#[derive(Default)] +struct W { + b: Vec, + n: usize, +} + +impl W { + fn put(&mut self, bits: u32, v: u32) { + for i in (0..bits).rev() { + if self.n.is_multiple_of(8) { + self.b.push(0); + } + let bit = ((v >> i) & 1) as u8; + *self.b.last_mut().unwrap() |= bit << (7 - self.n % 8); + self.n += 1; + } + } +} + +/// An E-AC-3 syncframe of `words` 16-bit words: syncinfo and a bsi with no optional metadata +/// (fscod < 3 with `numblkscod`), then `audio` writes the rest; zero padded. +fn eac3_frame(strmtyp: u32, substreamid: u32, words: u32, numblkscod: u32, acmod: u32, audio: impl FnOnce(&mut W)) -> Vec { + let mut w = W::default(); + w.put(16, 0x0B77); + w.put(2, strmtyp); + w.put(3, substreamid); + w.put(11, words - 1); + w.put(2, 0); // 48 kHz + w.put(2, numblkscod); + w.put(3, acmod); + w.put(1, 0); // lfeon + w.put(5, 16); // bsid + w.put(5, 31); // dialnorm + w.put(1, 0); // compre + if strmtyp == 1 { + w.put(1, 0); // chanmape + } + w.put(2, 0); // mixmdate, infomdate + if strmtyp == 0 && numblkscod != 3 { + w.put(1, 0); // convsync + } + w.put(1, 0); // addbsie + audio(&mut w); + let mut b = w.b; + b.resize(words as usize * 2, 0); + b +} + +#[test] +fn eac3_headers() { + let f = eac3_frame(0, 0, 384, 3, 2, |_| {}); + let h = parse_header(&f).unwrap(); + assert_eq!((h.sample_rate, h.blocks, h.samples(), h.frame_bytes, h.bitrate_kbps, h.channels()), (48_000, 6, 1536, 768, 192, 2)); + assert!(h.is_eac3() && h.is_primary()); + // one block per syncframe: 256 samples + let h = parse_header(&eac3_frame(0, 0, 100, 0, 7, |_| {})).unwrap(); + assert_eq!((h.blocks, h.samples(), h.channels(), h.bitrate_kbps), (1, 256, 5, 300)); + // reduced sample rate (fscod 3, fscod2 1): six blocks at 22.05 kHz + let mut f = eac3_frame(0, 0, 384, 0, 2, |_| {}); + f[4] = 0b1101_0100; + let h = parse_header(&f).unwrap(); + assert_eq!((h.sample_rate, h.blocks, h.acmod), (22_050, 6, 2)); + f[4] = 0b1111_0100; + assert_eq!(parse_header(&f), Err(Error::Invalid("reserved fscod2"))); + // a dependent substream and a second independent program are recognised and skipped + let mut d = Decoder::new(); + for (strmtyp, id) in [(1, 0), (0, 1)] { + let f = eac3_frame(strmtyp, id, 64, 3, 2, |_| {}); + let out = d.decode(&f).unwrap(); + assert!(!out.header.is_primary() && out.channels.is_empty()); + } + assert_eq!(parse_header(&eac3_frame(3, 0, 64, 3, 2, |_| {})), Err(Error::Unsupported("reserved E-AC-3 stream type 3".into()))); +} + +#[test] +fn eac3_tools_we_do_not_decode_are_refused() { + // mono, six blocks, AHT enabled and used: chexpstr D15 then reuse, so chahtinu is sent + let aht = eac3_frame(0, 0, 64, 3, 1, |w| { + w.put(1, 1); // expstre + w.put(1, 1); // ahte + w.put(2, 0); // snroffststr + w.put(8, 0); // transproce … spxattene + w.put(2, 1); // chexpstr[0] = D15 + w.put(10, 0); // chexpstr[1..6] = reuse + w.put(5, 0); // convexpstr + w.put(1, 1); // chahtinu + }); + assert_eq!(Decoder::new().decode(&aht).unwrap_err(), Error::Unsupported("E-AC-3 adaptive hybrid transform".into())); + // stereo with coupling in block 0 using enhanced coupling + let ecpl = eac3_frame(0, 0, 64, 3, 2, |w| { + w.put(1, 1); // expstre + w.put(1, 0); // ahte + w.put(2, 0); // snroffststr + w.put(8, 0); + w.put(1, 1); // cplinu[0] + w.put(5, 0); // cplstre[1..6] + w.put(6, 0b01_01_01); // block 0: cplexpstr, chexpstr × 2 + w.put(30, 0); // blocks 1-5: reuse + w.put(10, 0); // convexpstr + w.put(10, 0); // frmcsnroffst, frmfsnroffst + w.put(1, 0); // blkstrtinfoe + w.put(1, 0); // dynrnge + w.put(1, 0); // spxinu + w.put(1, 1); // ecplinu + }); + assert_eq!(Decoder::new().decode(&ecpl).unwrap_err(), Error::Unsupported("E-AC-3 enhanced coupling".into())); +} + +/// A mono E-AC-3 syncframe of one block (256 samples) that sends exponents (all 10, bandwidth +/// code 0) when `new_exps`, else reuses the last ones. Zero SNR offsets allocate no bits: every +/// coefficient is dither. +pub(crate) fn single_block_eac3(new_exps: bool) -> Vec { + eac3_frame(0, 0, 32, 0, 1, |w| { + // (expstre and ahte are not sent with fewer than six blocks) + w.put(2, 0); // snroffststr + w.put(8, 0); // transproce … spxattene + w.put(2, new_exps as u32); // chexpstr[0]: D15 or reuse + w.put(1, 0); // convexpstre + w.put(10, 0); // frmcsnroffst, frmfsnroffst + w.put(1, 0); // dynrnge + w.put(1, 0); // spxinu + if new_exps { + w.put(6, 0); // chbwcod → 73 coefficients + w.put(4, 10); // absolute exponent + for _ in 0..24 { + w.put(7, 62); // three zero deltas + } + w.put(2, 0); // gainrng + } + w.put(1, 0); // convsnroffste + }) +} + +#[test] +fn eac3_single_block_frames_reuse_exponents_across_syncframes() { + // the first sends exponents, the second reuses them + let (first, second) = (single_block_eac3(true), single_block_eac3(false)); + let mut d = Decoder::new(); + let a = d.decode(&first).unwrap(); + assert_eq!((a.header.samples(), a.channels.len(), a.channels[0].len()), (256, 1, 256)); + let b = d.decode(&second).unwrap(); + let peak = |c: &[f32]| c.iter().fold(0f32, |m, v| m.max(v.abs())); + assert!(peak(&b.channels[0]) > 1e-5 && peak(&b.channels[0]) < 0.1, "{}", peak(&b.channels[0])); + // after a reset there are no exponents to reuse: no coefficients, silence + d.reset(); + let c = d.decode(&second).unwrap(); + assert_eq!(peak(&c.channels[0]), 0.0); +} + +#[test] +fn eac3_garbage_never_panics() { + let mut d = Decoder::new(); + let mut x = 0x5EEDu32; + for len in [8usize, 40, 300, 1500, 4096] { + for _ in 0..300 { + let mut f: Vec = (0..len) + .map(|_| { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x as u8 + }) + .collect(); + f[0] = 0x0B; + f[1] = 0x77; + // independent substream 0, frmsiz within the buffer, bsid 11-16 + let words = (len / 2).clamp(4, 2048) - 1; + f[2] = (words >> 8) as u8 & 7; + f[3] = words as u8; + f[5] = (f[5] & 7) | ((11 + x % 6) as u8) << 3; + let _ = d.decode(&f); + } + } +} diff --git a/crates/audio/src/layout.rs b/crates/audio/src/layout.rs index f43249714..19fdc95e6 100644 --- a/crates/audio/src/layout.rs +++ b/crates/audio/src/layout.rs @@ -145,7 +145,7 @@ impl TryFrom for ChannelLayout { } /// A channel count as the API holds it, saturating at `u16::MAX`. -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "pcm-encode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "pcm-encode"))] pub(crate) fn count(n: usize) -> u16 { u16::try_from(n).unwrap_or(u16::MAX) } diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index a80f11b8d..a699b9886 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -8,8 +8,8 @@ //! `wasm32-unknown-unknown`. //! //! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`] -//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus and PCM in MP4/MOV, -//! Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and +//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus, AC-3/E-AC-3 and PCM in +//! MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and //! [`TrackDecoder`]; everything else (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, //! µ-law) through `symphonia`, which is to be replaced by native decoders. Feature //! `decode-native` is the same without symphonia. @@ -21,23 +21,25 @@ //! `lossless-encode` and `aac-encode`. //! - [`peaks`] (feature `peaks`): min/max/RMS pyramid for waveform drawing. //! - Codec modules for packet-level use, each with its own features: [`aac`] (`aac-decode`, -//! `aac-encode`), [`opus`] (`opus-decode`). +//! `aac-encode`), [`opus`] (`opus-decode`), [`ac3`] (`ac3-decode`). //! //! The shared types below build with no features. #[cfg(any(feature = "aac-decode", feature = "aac-encode"))] pub mod aac; +#[cfg(feature = "ac3-decode")] +pub mod ac3; mod buffer; mod layout; #[cfg(feature = "opus-decode")] pub mod opus; -#[cfg(any(feature = "aac-decode", feature = "opus-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] mod pcm; #[cfg(feature = "peaks")] pub mod peaks; #[cfg(feature = "decode-native")] mod read; -#[cfg(any(feature = "aac-decode", feature = "opus-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] mod track; #[cfg(any(feature = "decode-native", feature = "pcm-encode"))] mod wav; @@ -54,7 +56,7 @@ pub use layout::{ChannelLayout, Speaker}; pub use read::SharedSource; #[cfg(feature = "decode-native")] pub use read::{DEFAULT_MAX_SAMPLES, DecodeOptions, Decoder, TrackInfo, decode, decode_with, detect_container, probe, probe_with, tracks, tracks_of}; -#[cfg(any(feature = "aac-decode", feature = "opus-decode"))] +#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))] pub use track::{LOOKAHEAD_PACKETS, TrackDecoder}; #[cfg(feature = "pcm-encode")] pub use write::wav_float_header; @@ -111,6 +113,16 @@ impl From for AudioError { } } +#[cfg(feature = "ac3-decode")] +impl From for AudioError { + fn from(e: ac3::Error) -> Self { + match e { + ac3::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()), + _ => AudioError::Malformed(e.to_string()), + } + } +} + /// The file formats [`encode`] writes. Each needs its feature: WAV and AIFF `pcm-encode`, FLAC /// `flac-encode`; asking for one that is not built returns [`AudioError::Unsupported`]. #[non_exhaustive] diff --git a/crates/audio/src/read/mod.rs b/crates/audio/src/read/mod.rs index 795e87f10..88d3fcea8 100644 --- a/crates/audio/src/read/mod.rs +++ b/crates/audio/src/read/mod.rs @@ -183,9 +183,10 @@ enum Inner<'a> { /// A file's audio, decoded block by block: the streaming counterpart of [`decode`], with seeking /// and track choice. /// -/// Native formats (WAV, AIFF, AAC, Opus and PCM in MP4/MOV, Matroska/WebM and Ogg, and raw ADTS -/// streams) read their sample data from the source packet by packet. The container's packet index -/// is held in memory (tens of bytes per packet), and opening an Ogg or Matroska file or a raw ADTS +/// Native formats (WAV, AIFF, AAC, Opus, AC-3/E-AC-3 and PCM in MP4/MOV, Matroska/WebM and Ogg, and +/// raw ADTS and AC-3 streams) read their sample data from the source packet by packet. The +/// container's packet index is held in memory (tens of bytes per packet), and opening an Ogg or +/// Matroska file or a raw /// stream reads through the whole file to build it; WAV and AIFF need no index. Formats symphonia /// decodes (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) read the whole source into /// memory first; their decoded audio still streams. diff --git a/crates/audio/src/read/native.rs b/crates/audio/src/read/native.rs index 2594010f1..60f7c6909 100644 --- a/crates/audio/src/read/native.rs +++ b/crates/audio/src/read/native.rs @@ -1,5 +1,5 @@ //! The native decode path: craft-media's demuxers (ISO BMFF / QuickTime, Matroska / WebM), raw -//! ADTS streams, and WAV / AIFF PCM, feeding [`TrackDecoder`]. +//! ADTS and AC-3 streams, and WAV / AIFF PCM, feeding [`TrackDecoder`]. //! [`Native::open`] answers `None` for a stream with no native codec (MP1/MP2/MP3, FLAC, ALAC, //! Vorbis, ...) or a container these demuxers do not read; the caller then uses symphonia. //! @@ -13,7 +13,7 @@ use super::id3v2_len; use super::source::{Src, io_error}; use super::wav::{self, WavParse}; use crate::track::{TrackDecoder, adts_format_of, adts_frame_count, adts_stream_header}; -use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac}; +use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac, ac3}; use craft_media::{isobmff, matroska, ogg}; /// Corrupt packets tolerated before a stream that has produced nothing is an error. @@ -196,6 +196,10 @@ enum MkvFrames { Constant(u64), /// PCM: the stored sizes of the track's packets, and the bytes per frame. Pcm { sizes: Vec, frame_bytes: u64 }, + /// E-AC-3, whose packets can differ in length: each packet's length from the headers of its + /// primary syncframes. The packets' places and stored sizes, the bytes header stripping took + /// off each one, and the starts of the packets read so far. + Eac3 { samples: Vec<(u64, u32)>, prefix: Vec, starts: Vec }, /// Opus: each packet's length is in its first bytes (RFC 6716 §3.1); the packets' places and /// sizes, and their starts once read. Opus { samples: Vec<(u64, u32)>, starts: Option> }, @@ -209,6 +213,25 @@ impl MkvFrames { match self { MkvFrames::Constant(n) => Some(k as u64 * *n), MkvFrames::Pcm { sizes, frame_bytes } => Some(sizes.iter().take(k).map(|&s| u64::from(s)).sum::() / *frame_bytes), + MkvFrames::Eac3 { samples, prefix, starts } => { + // Headers are read only as far as packet `k`; a later seek extends the list. A + // packet that cannot be read leaves the rest unknown (the caller then places the + // packet by its timestamp) and is tried again on the next seek. + if k >= samples.len() { + return None; + } + while starts.len() <= k { + let start = match starts.len().checked_sub(1) { + None => 0, + Some(prev) => { + let &(at, size) = samples.get(prev)?; + starts.get(prev)? + eac3_packet_frames(src, prefix, at, size)? as u64 + } + }; + starts.push(start); + } + starts.get(k).copied() + } MkvFrames::Opus { samples, starts } => { if starts.is_none() { // A read that fails leaves the starts unknown (the caller then places the @@ -230,6 +253,31 @@ impl MkvFrames { } } +/// The frames a stored Matroska E-AC-3 packet decodes to, from the headers of its primary +/// syncframes, reading only those headers; `None` when a read fails or a header does not parse. +/// A packet of dependent syncframes alone is zero frames, not a failure. +fn eac3_packet_frames(src: &Src<'_>, prefix: &[u8], at: u64, size: u32) -> Option { + // Enough bytes for any AC-3 / E-AC-3 header ahead of the audio blocks. + const HEADER_BYTES: usize = 32; + let len = prefix.len() + size as usize; + let mut frames = 0; + let mut p = 0; + while p < len { + let n = HEADER_BYTES.min(len - p); + let mut head = prefix.get(p..).unwrap_or_default().iter().copied().take(n).collect::>(); + if head.len() < n { + let stored = (p + head.len() - prefix.len()) as u64; + head.extend(src.bytes(at + stored, n - head.len())?); + } + let h = ac3::parse_header(&head).ok()?; + if h.is_primary() { + frames += h.samples(); + } + p += h.frame_bytes.max(1); + } + (len > 0).then_some(frames) +} + /// The next packet of Matroska track `track` and its timestamp in nanoseconds. fn mkv_next(demux: &mut matroska::Demuxer>, track: usize, rate: u32) -> Option> { loop { @@ -319,7 +367,9 @@ impl<'a> Native<'a> { Some(Container::Ogg) if ogg::sniff(body) => open_ogg(src, track), Some(c @ (Container::Mp4 | Container::QuickTime)) if head.get(4..8) == Some(b"ftyp") => open_mp4(src, c, track), Some(c @ (Container::Matroska | Container::WebM)) if body.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) => open_mkv(src, c, track), - Some(Container::Adts) | None => single(open_adts(src, at)), + Some(Container::Adts) => single(open_adts(src, at)), + Some(Container::Ac3) => single(open_ac3(src, at)), + None => single(open_ac3(src, at).or_else(|| open_adts(src, at))), _ => None, } } @@ -649,6 +699,23 @@ fn open_adts<'a>(src: &Src<'a>, at: u64) -> Option>> { Some(Ok(n)) } +/// A raw AC-3 or E-AC-3 stream; E-AC-3 dependent substreams decode to nothing and count no frames. +fn open_ac3<'a>(src: &Src<'a>, at: u64) -> Option>> { + let frames = match raw_frames(src, at, |rest| { + ac3::parse_header(rest).ok().map(|h| (h.frame_bytes, if h.is_primary() { h.samples() as u32 } else { 0 })) + }) { + Ok(frames) => frames?, + Err(e) => return Some(Err(e)), + }; + let total = frames.iter().map(|&(_, _, f)| u64::from(f)).sum(); + let lookahead = match raw_lookahead(src, &frames, 1) { + Ok(lookahead) => lookahead, + Err(e) => return Some(Err(e)), + }; + native(Container::Ac3, Packets::Raw { frames, src: src.clone(), next: 0 }, &CodecParams::new(CodecId::Ac3), &lookahead, 0, None, Some(total)) + .map(Ok) +} + /// The `n`-th audio track (the first when `None`) among `kinds`, as an index into them. fn audio_track(kinds: impl Iterator, n: Option) -> Option> { let audio: Vec = kinds.enumerate().filter(|&(_, audio)| audio).map(|(i, _)| i).collect(); @@ -766,6 +833,9 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option) -> Op let probe = TrackDecoder::new(¶ms, &lookahead.iter().map(Vec::as_slice).collect::>()).ok()?; let rate = probe.sample_rate(); let frames_of = match (probe.frames_per_packet(), params.pcm) { + _ if probe.codec() == CodecId::Eac3 => { + MkvFrames::Eac3 { samples: t.samples.iter().map(|s| (s.offset, s.size)).collect(), prefix: t.frame_prefix(0), starts: Vec::new() } + } (Some(n), _) => MkvFrames::Constant(n as u64), (None, Some(pcm)) => { let frame_bytes = u64::from(pcm.bits.div_ceil(8)) * u64::from(params.channels.unwrap_or(1).max(1)); @@ -790,8 +860,12 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option) -> Op let duration = if s.duration > 0 { s.duration } else { - // ffmpeg 6 writes AAC without block durations, and records the end in a tag. - let MkvFrames::Constant(n) = frames_of else { return None }; + // ffmpeg 6 writes AAC and E-AC-3 without block durations, and records the end in a tag. + let n = match &frames_of { + MkvFrames::Constant(n) => *n, + MkvFrames::Eac3 { prefix, .. } => eac3_packet_frames(src, prefix, s.offset, s.size)? as u64, + _ => return None, + }; let packet = rescale_round(n.saturating_mul(den), u64::from(rate).saturating_mul(num), 1); tagged_last_duration(track_duration_tag(&file.tags, t.uid)?, s.pts, delay_ticks, packet, t.timebase)? }; @@ -1212,3 +1286,21 @@ mod adts_tests { assert!(matches!(opened, Err(AudioError::Io(_))), "{:?}", opened.err()); } } + +#[cfg(all(test, feature = "ac3-decode"))] +mod ac3_tests { + use crate::ac3::tests::single_block_eac3; + use crate::{DecodeOptions, Decoder}; + + /// One-block E-AC-3 syncframes may reuse exponents sent long before a seek target. + #[test] + fn a_seek_in_one_block_e_ac3_keeps_exponents_sent_before_the_preroll() { + // Exponents in the first syncframe only, reused by the 19 after it (256 frames each). + let file = [single_block_eac3(true), single_block_eac3(false).repeat(19)].concat(); + let mut dec = Decoder::open(file, Some("ac3"), &DecodeOptions::default()).unwrap(); + assert_eq!(dec.seek(3000).unwrap(), 3000); + let block = dec.next_block().unwrap().unwrap(); + let peak = block.channels[0].iter().fold(0f32, |m, v| m.max(v.abs())); + assert!(peak > 1e-5, "the target decodes with the first syncframe's exponents, not as silence"); + } +} diff --git a/crates/audio/src/track.rs b/crates/audio/src/track.rs index 27dce54b9..a9eac30a9 100644 --- a/crates/audio/src/track.rs +++ b/crates/audio/src/track.rs @@ -4,6 +4,8 @@ #[cfg(feature = "aac-decode")] use crate::aac; +#[cfg(feature = "ac3-decode")] +use crate::ac3; #[cfg(feature = "opus-decode")] use crate::opus; use crate::pcm::{PcmKind, deinterleave}; @@ -22,6 +24,9 @@ enum Inner { /// The format ADTS packets must keep, when their headers gave the configuration. adts_format: Option, }, + /// The decoder, and what the packets read so far say of the track's packets. + #[cfg(feature = "ac3-decode")] + Ac3(Box, Ac3Packets), #[cfg(feature = "opus-decode")] Opus { dec: Box, @@ -38,7 +43,7 @@ enum Inner { /// /// It takes the track's [`CodecParams`] (from `isobmff::Track::codec_params`, /// `matroska::Track::codec_params`, `ogg::Stream::codec_params`, or built by hand for another -/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus and PCM. +/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus, AC-3/E-AC-3 and PCM. /// Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: every decoded /// sample is returned. /// @@ -82,7 +87,9 @@ impl std::fmt::Debug for TrackDecoder { impl TrackDecoder { /// A decoder for the track `params` describes. `lookahead` holds the track's first packets /// (up to [`LOOKAHEAD_PACKETS`]; empty when they are not at hand): HE-AAC signalled only - /// inside the stream is found in them. + /// inside the stream is found in them, and AC-3 takes its rate and channel layout from the + /// first frame. Without them those are taken from `params` and set + /// from the first decoded packet. /// /// Streams without codec configuration are accepted where the packets carry it: AAC with /// empty `extradata` takes its configuration from the ADTS headers of the lookahead (MPEG-TS, @@ -96,9 +103,9 @@ impl TrackDecoder { pub fn new(params: &CodecParams, lookahead: &[&[u8]]) -> Result { let rate = params.sample_rate.unwrap_or(0); let channels = usize::from(params.channels.unwrap_or(0)); - // Only AAC reads the lookahead; Opus needs none. - #[cfg(not(feature = "aac-decode"))] - let _ = lookahead; + // AC-3 reads its parameters from the first frame; AAC reads the lookahead; Opus needs none. + #[cfg_attr(not(feature = "ac3-decode"), allow(unused_variables))] + let first = lookahead.first().copied().unwrap_or_default(); let (inner, codec, sample_rate, layout, sample_format) = match params.codec { #[cfg(feature = "aac-decode")] CodecId::Aac | CodecId::HeAac => { @@ -132,6 +139,20 @@ impl TrackDecoder { let order = layout.is_ordered().then(|| aac::wav_order(cfg)).flatten(); (Inner::Aac { dec: Box::new(dec), up, order, adts_format }, codec, out, layout, SampleFormat::Compressed) } + #[cfg(feature = "ac3-decode")] + CodecId::Ac3 | CodecId::Eac3 => match ac3::parse_header(first) { + Ok(h) => { + let codec = if h.is_eac3() { CodecId::Eac3 } else { CodecId::Ac3 }; + (Inner::Ac3(Box::default(), Ac3Packets::of(lookahead)), codec, h.sample_rate, ac3_layout(&h), SampleFormat::Compressed) + } + Err(_) => ( + Inner::Ac3(Box::default(), Ac3Packets::default()), + params.codec, + rate, + ChannelLayout::unordered(crate::layout::count(channels)), + SampleFormat::Compressed, + ), + }, #[cfg(feature = "opus-decode")] CodecId::Opus => { // Opus over RTP or in MPEG-TS has no OpusHead: mono and stereo need none (RFC 7845 @@ -163,7 +184,7 @@ impl TrackDecoder { } /// The codec, refined from the stream: [`CodecId::HeAac`] for AAC with spectral band - /// replication. + /// replication, `Eac3` for Dolby Digital Plus. pub fn codec(&self) -> CodecId { self.codec } @@ -188,9 +209,14 @@ impl TrackDecoder { self.sample_format } - /// Decode one packet. A packet that fails is an error for every codec: call [`TrackDecoder::conceal`] for audio of its length, so what follows keeps + /// Decode one packet. An MP4 or Matroska packet may hold several AC-3 frames; + /// an E-AC-3 dependent substream decodes to no frames. A packet that fails is an error for + /// every codec: call [`TrackDecoder::conceal`] for audio of its length, so what follows keeps /// its timing. pub fn decode(&mut self, packet: &[u8]) -> Result { + // A packet decoded only in part says nothing of how long packets are. + #[cfg_attr(not(feature = "ac3-decode"), allow(unused_mut))] + let mut partial = false; let channels = match &mut self.inner { #[cfg(feature = "aac-decode")] Inner::Aac { dec, up, order, adts_format } => { @@ -217,6 +243,33 @@ impl TrackDecoder { } reorder(out, *order) } + #[cfg(feature = "ac3-decode")] + Inner::Ac3(d, shape) => { + let mut out: Vec> = Vec::new(); + let mut p = 0; + while let Some(rest) = packet.get(p..).filter(|r| !r.is_empty()) { + let f = match d.decode(rest) { + Ok(f) => f, + Err(e) if out.is_empty() => return Err(e.into()), + Err(_) => { + partial = true; + break; + } + }; + p += f.header.frame_bytes.max(1); + shape.short |= f.header.is_primary() && f.header.samples() < AC3_FRAME_SAMPLES; + if !f.channels.is_empty() { + if self.last_frames == 0 { + self.sample_rate = f.header.sample_rate; + self.layout = ac3_layout(&f.header); + } + append(&mut out, f.channels)?; + } + } + let frames = out.first().map_or(0, Vec::len); + shape.varies |= !partial && frames > 0 && shape.first > 0 && frames != shape.first; + out + } #[cfg(feature = "opus-decode")] Inner::Opus { dec, order } => { let out = dec.decode(Some(packet))?; @@ -227,7 +280,7 @@ impl TrackDecoder { deinterleave(packet, *kind, *channels, frames)? } }; - if let Some(n) = channels.iter().map(Vec::len).min().filter(|&n| n > 0) { + if let Some(n) = channels.iter().map(Vec::len).min().filter(|&n| n > 0 && !partial) { self.last_frames = n; if channels.len() != usize::from(self.layout.channels()) { self.layout = ChannelLayout::unordered(crate::layout::count(channels.len())); @@ -265,6 +318,8 @@ impl TrackDecoder { None => Some(per_unit), } } + #[cfg(feature = "ac3-decode")] + Inner::Ac3(..) => Some(ac3_packet_frames(data)), Inner::Pcm { kind, channels } => Some(data.len() / (kind.bytes() * *channels).max(1)), }; let frames = frames.filter(|&n| n > 0).unwrap_or(self.last_frames); @@ -272,12 +327,21 @@ impl TrackDecoder { } /// The frames every packet decodes to, for codecs whose packets all have one length: AAC - /// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this); - /// `None` for Opus and PCM. + /// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this) and + /// AC-3 / E-AC-3 (the frames of the last decoded packet, else of the first packet's primary + /// syncframes, else 1536); `None` for Opus and PCM, and for AC-3 / E-AC-3 whose packets read so + /// far differ in length. pub fn frames_per_packet(&self) -> Option { match &self.inner { #[cfg(feature = "aac-decode")] Inner::Aac { up, .. } => Some(aac::FRAME_LEN * if up.is_some() { 2 } else { 1 }), + #[cfg(feature = "ac3-decode")] + Inner::Ac3(_, shape) if shape.varies => None, + #[cfg(feature = "ac3-decode")] + Inner::Ac3(_, shape) => Some(match (self.last_frames, shape.first) { + (0, 0) => AC3_FRAME_SAMPLES, + (0, n) | (n, _) => n, + }), #[cfg(feature = "opus-decode")] Inner::Opus { .. } => None, Inner::Pcm { .. } => None, @@ -285,11 +349,18 @@ impl TrackDecoder { } /// Frames to decode before a seek target and drop, so the decoder has settled when the target - /// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), none for PCM. + /// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 frame (its filter bank + /// overlaps one frame), none for PCM. E-AC-3 syncframes of fewer than six blocks may reuse + /// exponents from any syncframe before them, so once one has been read the stream decodes from + /// its start (`usize::MAX`). pub fn preroll(&self) -> usize { match &self.inner { #[cfg(feature = "aac-decode")] Inner::Aac { up, .. } => 2 * aac::FRAME_LEN * if up.is_some() { 2 } else { 1 }, + #[cfg(feature = "ac3-decode")] + Inner::Ac3(_, shape) if shape.short => usize::MAX, + #[cfg(feature = "ac3-decode")] + Inner::Ac3(..) => AC3_FRAME_SAMPLES, #[cfg(feature = "opus-decode")] Inner::Opus { .. } => 3840, Inner::Pcm { .. } => 0, @@ -310,6 +381,8 @@ impl TrackDecoder { up.reset(); } } + #[cfg(feature = "ac3-decode")] + Inner::Ac3(d, _) => d.reset(), #[cfg(feature = "opus-decode")] Inner::Opus { dec, .. } => dec.reset(), Inner::Pcm { .. } => {} @@ -320,6 +393,7 @@ impl TrackDecoder { fn unsupported(codec: CodecId) -> AudioError { let feature = match codec { CodecId::Aac | CodecId::HeAac => Some("aac-decode"), + CodecId::Ac3 | CodecId::Eac3 => Some("ac3-decode"), CodecId::Opus => Some("opus-decode"), _ => None, }; @@ -345,7 +419,7 @@ fn reorder(mut channels: Vec>, order: Option<&[usize]>) -> Vec /// Append `more` to `out` channel by channel. A packet whose frames differ in channel count is /// malformed: one buffer cannot hold them. -#[cfg(feature = "aac-decode")] +#[cfg(any(feature = "aac-decode", feature = "ac3-decode"))] fn append(out: &mut Vec>, more: Vec>) -> Result<()> { if out.is_empty() { *out = more; @@ -371,6 +445,51 @@ pub(crate) fn adts_stream_header(headers: &[aac::AdtsHeader]) -> Option Ac3Packets { + let lengths: Vec = lookahead.iter().map(|p| ac3_packet_frames(p)).filter(|&n| n > 0).collect(); + let short = lookahead.iter().any(|p| ac3_syncframes(p).any(|h| h.is_primary() && h.samples() < AC3_FRAME_SAMPLES)); + Ac3Packets { first: lookahead.first().map_or(0, |p| ac3_packet_frames(p)), varies: lengths.windows(2).any(|w| w[0] != w[1]), short } + } +} + +/// The headers of the syncframes `packet` holds, up to the first that does not parse. +#[cfg(feature = "ac3-decode")] +fn ac3_syncframes(packet: &[u8]) -> impl Iterator + '_ { + let mut p = 0; + std::iter::from_fn(move || { + let h = ac3::parse_header(packet.get(p..)?).ok()?; + p += h.frame_bytes.max(1); + Some(h) + }) +} + +/// The frames an AC-3 / E-AC-3 packet decodes to: those of every primary syncframe in it (an MP4 +/// or Matroska packet may hold several, as E-AC-3 frames of fewer than six blocks are grouped), +/// up to the first that does not parse. 0 when none does. +#[cfg(feature = "ac3-decode")] +fn ac3_packet_frames(packet: &[u8]) -> usize { + ac3_syncframes(packet).filter(ac3::Header::is_primary).map(|h| h.samples()).sum() +} + /// The ADTS frames `packet` holds, 0 when it is not ADTS-framed. #[cfg(feature = "decode-native")] pub(crate) fn adts_frame_count(packet: &[u8]) -> usize { @@ -480,6 +599,23 @@ pub(crate) fn aac_layout(channel_config: u8, channels: usize) -> ChannelLayout { } } +#[cfg(feature = "ac3-decode")] +/// The speakers of an AC-3 / E-AC-3 program (A/52 Table 5.8) in the decoder's WAV order: fronts, +/// LFE, surrounds. Dual mono (1+1) has no speaker assignment. +pub(crate) fn ac3_layout(h: &ac3::Header) -> ChannelLayout { + let fronts = match h.acmod & 7 { + 0 => return ChannelLayout::unordered(crate::layout::count(h.channels())), + 1 => 0x4, + 2 => 0x3, + 3 => 0x7, + 4 => 0x103, + 5 => 0x107, + 6 => 0x603, + _ => 0x607, + }; + ChannelLayout::from_mask(fronts | if h.lfeon { 0x8 } else { 0 }) +} + #[cfg(feature = "opus-decode")] /// The speakers of an Opus stream (RFC 7845 §5.1.1): Vorbis order for mapping family 1, reordered /// to WAV order by [`opus::wav_order`]; other families have no speaker assignment. @@ -807,6 +943,61 @@ mod tests { assert!(matches!(TrackDecoder::new(&aac_params(), &[&bare]), Err(AudioError::Malformed(_)))); } + #[cfg(feature = "ac3-decode")] + #[test] + fn e_ac3_packets_are_as_long_as_all_their_syncframes() { + use crate::ac3::tests::single_block_eac3; + let params = CodecParams::new(CodecId::Eac3); + // One block per packet: 256 frames, known before any packet is decoded. + let one = single_block_eac3(true); + let dec = TrackDecoder::new(¶ms, &[&one]).unwrap(); + assert_eq!((dec.codec(), dec.frames_per_packet()), (CodecId::Eac3, Some(256))); + // Six one-block syncframes to a packet (as MP4 groups them): 1536. + let six = [one.clone(), (0..5).flat_map(|_| single_block_eac3(false)).collect()].concat(); + let mut dec = TrackDecoder::new(¶ms, &[&six]).unwrap(); + assert_eq!(dec.frames_per_packet(), Some(1536)); + assert_eq!(dec.decode(&six).unwrap().channels[0].len(), 1536); + // A damaged packet conceals at the length of all its syncframes. + dec.reset(); + assert_eq!(dec.conceal(Some(&six)).unwrap().channels[0].len(), 1536); + assert_eq!(dec.conceal(Some(&one)).unwrap().channels[0].len(), 256); + } + + #[cfg(feature = "ac3-decode")] + #[test] + fn a_packet_decoded_in_part_keeps_the_length_lost_packets_are_concealed_at() { + use crate::ac3::tests::single_block_eac3; + let params = CodecParams::new(CodecId::Eac3); + let one = single_block_eac3(true); + let six = [one.clone(), (0..5).flat_map(|_| single_block_eac3(false)).collect()].concat(); + let mut dec = TrackDecoder::new(¶ms, &[&six, &six]).unwrap(); + assert_eq!(dec.decode(&six).unwrap().channels[0].len(), 1536); + // The second syncframe is damaged: its first one decodes, the rest of the packet is lost. + let damaged = [one.clone(), vec![0x0B, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]].concat(); + assert_eq!(dec.decode(&damaged).unwrap().channels[0].len(), 256); + assert_eq!(dec.conceal(None).unwrap().channels[0].len(), 1536, "a lost packet is as long as the last whole one"); + assert_eq!(dec.frames_per_packet(), Some(1536), "a damaged packet does not make the packets' lengths differ"); + } + + #[cfg(feature = "ac3-decode")] + #[test] + fn e_ac3_packets_of_different_lengths_or_short_syncframes_are_not_indexed() { + use crate::ac3::tests::single_block_eac3; + let params = CodecParams::new(CodecId::Eac3); + let one = single_block_eac3(true); + let two = [single_block_eac3(true), single_block_eac3(false)].concat(); + // Packets of 256 and 512 frames: a packet's index does not give its position. + let dec = TrackDecoder::new(¶ms, &[&one, &one, &two]).unwrap(); + assert_eq!(dec.frames_per_packet(), None); + // Known only once decoded: from then on too. + let mut dec = TrackDecoder::new(¶ms, &[&one]).unwrap(); + assert_eq!(dec.frames_per_packet(), Some(256)); + dec.decode(&two).unwrap(); + assert_eq!(dec.frames_per_packet(), None); + // One-block syncframes may reuse exponents from any syncframe before them. + assert_eq!(TrackDecoder::new(¶ms, &[&one]).unwrap().preroll(), usize::MAX); + } + #[test] fn codecs_without_a_packet_decoder_are_unsupported() { for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::Alac, CodecId::Mp3, CodecId::H264] { diff --git a/crates/audio/tests/codecs/ac3_oracle.rs b/crates/audio/tests/codecs/ac3_oracle.rs new file mode 100644 index 000000000..3d3f319a1 --- /dev/null +++ b/crates/audio/tests/codecs/ac3_oracle.rs @@ -0,0 +1,303 @@ +//! AC-3 and E-AC-3 against ffmpeg: ffmpeg's AC-3 and E-AC-3 encoders write raw syncframes from +//! generated tones and noise; ffmpeg's decode of the same file is the reference. AC-3 output is not bit-exact across +//! decoders (zero-bit mantissas are filled with decoder-specific dither, §7.3.4; float vs fixed +//! arithmetic), so the criterion is SNR against ffmpeg's decode. + +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +struct Spec { + name: &'static str, + /// ffmpeg encoder and raw muxer: "ac3" or "eac3". + codec: &'static str, + input: &'static str, + rate: u32, + channels: u32, + enc: &'static [&'static str], + /// Minimum SNR (dB) against ffmpeg. + min_snr: f64, +} + +const SPECS: &[Spec] = &[ + Spec { + name: "ac3_stereo_192k", + codec: "ac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000", + rate: 48_000, + channels: 2, + enc: &["-b:a", "192k"], + min_snr: 60.0, + }, + // low bitrate stereo: coupling and rematrixing + Spec { + name: "ac3_stereo_96k_coupling", + codec: "ac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.05*sin(2*PI*9100*t)|0.3*sin(2*PI*441*t)+0.05*sin(2*PI*11000*t):s=44100", + rate: 44_100, + channels: 2, + enc: &["-b:a", "96k", "-channel_coupling", "1"], + min_snr: 40.0, + }, + Spec { + name: "ac3_mono_32k", + codec: "ac3", + input: "sine=frequency=1000:sample_rate=32000", + rate: 32_000, + channels: 1, + enc: &["-b:a", "96k"], + min_snr: 60.0, + }, + // 5.1 with LFE + Spec { + name: "ac3_51_448k", + codec: "ac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*300*t)|0.3*sin(2*PI*400*t)|0.3*sin(2*PI*500*t)|0.3*sin(2*PI*60*t)|0.2*sin(2*PI*700*t)|0.2*sin(2*PI*800*t):s=48000:c=5.1", + rate: 48_000, + channels: 6, + enc: &["-b:a", "448k"], + min_snr: 55.0, + }, + // noise (wideband, many zero-bit mantissas): the SNR is limited by dither + Spec { + name: "ac3_noise_stereo", + codec: "ac3", + input: "anoisesrc=color=pink:sample_rate=48000:amplitude=0.3:seed=3,pan=stereo|c0=c0|c1=-0.5*c0", + rate: 48_000, + channels: 2, + enc: &["-b:a", "128k"], + min_snr: 18.0, + }, + // E-AC-3 (Annex E): ffmpeg writes six-block syncframes with frame-based exponent strategies + Spec { + name: "eac3_stereo_192k", + codec: "eac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000", + rate: 48_000, + channels: 2, + enc: &["-b:a", "192k"], + min_snr: 60.0, + }, + // low bitrate stereo: coupling (with the default band structure) and rematrixing + Spec { + name: "eac3_stereo_64k_coupling", + codec: "eac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.05*sin(2*PI*9100*t)|0.3*sin(2*PI*441*t)+0.05*sin(2*PI*11000*t):s=44100", + rate: 44_100, + channels: 2, + enc: &["-b:a", "64k", "-channel_coupling", "1"], + min_snr: 35.0, + }, + Spec { + name: "eac3_mono_32k", + codec: "eac3", + input: "sine=frequency=1000:sample_rate=32000", + rate: 32_000, + channels: 1, + enc: &["-b:a", "64k"], + min_snr: 60.0, + }, + // 5.1 with LFE and mixing / informational metadata in the bsi + Spec { + name: "eac3_51_384k_metadata", + codec: "eac3", + input: "aevalsrc=exprs=0.3*sin(2*PI*300*t)|0.3*sin(2*PI*400*t)|0.3*sin(2*PI*500*t)|0.3*sin(2*PI*60*t)|0.2*sin(2*PI*700*t)|0.2*sin(2*PI*800*t):s=48000:c=5.1", + rate: 48_000, + channels: 6, + enc: &[ + "-b:a", + "384k", + "-dmix_mode", + "loro", + "-ltrt_cmixlev", + "0.707", + "-loro_surmixlev", + "0.5", + "-dsurex_mode", + "on", + "-mixing_level", + "100", + "-room_type", + "small", + ], + min_snr: 55.0, + }, + Spec { + name: "eac3_noise_stereo", + codec: "eac3", + input: "anoisesrc=color=pink:sample_rate=48000:amplitude=0.3:seed=3,pan=stereo|c0=c0|c1=-0.5*c0", + rate: 48_000, + channels: 2, + enc: &["-b:a", "96k", "-dsur_mode", "on", "-dheadphone_mode", "off"], + min_snr: 18.0, + }, +]; + +fn make(ff: &Path, s: &Spec) -> Option { + let out = crate::testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec)); + crate::testkit::fixtures::generate(&out, |tmp| { + let st = Command::new(ff) + .args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i", s.input, "-t", "1"]) + .args(["-c:a", s.codec]) + .args(s.enc) + .args(["-f", s.codec]) + .arg(tmp) + .stdin(Stdio::null()) + .status(); + matches!(st, Ok(x) if x.success()) + }) +} + +fn frames(data: &[u8]) -> Vec<&[u8]> { + let mut out = Vec::new(); + let mut p = 0; + while let Ok(h) = craft_audio::ac3::parse_header(&data[p..]) { + if p + h.frame_bytes > data.len() { + break; + } + out.push(&data[p..p + h.frame_bytes]); + p += h.frame_bytes; + } + out +} + +#[test] +fn ffmpeg_encoded_streams_decode() { + let ff = require_ffmpeg!(); + for s in SPECS { + let f = make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name)); + let data = std::fs::read(&f).unwrap(); + let mut dec = craft_audio::ac3::Decoder::new(); + let mut ours: Vec> = vec![Vec::new(); s.channels as usize]; + for fr in frames(&data) { + let d = dec.decode(fr).unwrap(); + assert_eq!((d.header.sample_rate, d.channels.len() as u32), (s.rate, s.channels), "{}", s.name); + assert_eq!(d.header.is_eac3(), s.codec == "eac3", "{}", s.name); + for (o, c) in ours.iter_mut().zip(d.channels) { + o.extend(c); + } + } + let o = Command::new(&ff).args(["-v", "error", "-i"]).arg(&f).args(["-f", "f32le", "-"]).output().unwrap(); + let want: Vec = o.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect(); + let ch = s.channels as usize; + let n = want.len() / ch; + assert_eq!(n, ours[0].len(), "{}: sample count", s.name); + let mut worst = f64::INFINITY; + for c in 0..ch { + let (mut sig, mut err) = (0f64, 0f64); + for i in 0..n { + let (a, b) = (ours[c][i] as f64, want[i * ch + c] as f64); + sig += b * b; + err += (a - b) * (a - b); + } + let snr = 10.0 * (sig / err.max(1e-30)).log10(); + worst = worst.min(snr); + } + // the same stream decoded with another dither sequence: how far dither alone moves the + // output (the difference to ffmpeg must be of that order) + let mut dec2 = craft_audio::ac3::Decoder::new(); + dec2.set_dither_seed(0xDEAD_BEEF); + let other: Vec = frames(&data).iter().flat_map(|fr| dec2.decode(fr).unwrap().channels.swap_remove(0)).collect(); + let (mut sig, mut err) = (0f64, 0f64); + for i in 0..n { + sig += (ours[0][i] as f64).powi(2); + err += (ours[0][i] as f64 - other[i] as f64).powi(2); + } + let dither_snr = 10.0 * (sig / err.max(1e-30)).log10(); + println!( + "{}: {} Hz, {} ch, {} frames, worst channel SNR vs ffmpeg {worst:.1} dB (two dither seeds: {dither_snr:.1} dB)", + s.name, + s.rate, + ch, + frames(&data).len() + ); + assert!(worst >= s.min_snr, "{}: SNR {worst:.1} dB < {}", s.name, s.min_snr); + } +} + +/// Damaged E-AC-3 syncframes (bit flips, truncation, corrupt sizes) give errors or noise, never +/// a panic. +#[test] +fn damaged_eac3_frames_never_panic() { + let ff = require_ffmpeg!(); + let mut x = 0x00C0_FFEEu32; + let mut rnd = move || { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x + }; + for s in SPECS.iter().filter(|s| s.codec == "eac3") { + let f = make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name)); + let data = std::fs::read(&f).unwrap(); + let mut dec = craft_audio::ac3::Decoder::new(); + for fr in frames(&data).into_iter().take(8) { + for k in 0..300 { + let mut m = fr.to_vec(); + match k % 3 { + 0 => { + for _ in 0..1 + rnd() % 8 { + let bit = rnd() as usize % (m.len() * 8); + m[bit / 8] ^= 0x80 >> (bit % 8); + } + } + 1 => m.truncate(rnd() as usize % m.len()), + _ => { + // keep the sync word, scramble frmsiz / fscod / numblkscod / acmod + m[2] ^= rnd() as u8; + m[3] ^= rnd() as u8; + m[4] ^= rnd() as u8; + } + } + let _ = dec.decode(&m); + } + } + } +} + +#[test] +#[ignore] +fn generate_fixtures() { + let Some(ff) = crate::testkit::ffmpeg_or_skip("ac3 fixtures") else { return }; + for s in SPECS { + let out = crate::testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec)); + crate::testkit::fixtures::generate_and_report(&format!("ac3/{}", s.name), &[out], || make(&ff, s)); + } +} + +/// E-AC-3 in Matroska whose packets change length after the packets read ahead at open (here +/// one syncframe each, then two): a seek finds its packet's place from the packets' own headers, +/// not as the first packet's length times its index. +#[test] +fn a_seek_in_matroska_e_ac3_whose_packets_change_length_lands_exactly() { + use craft_media::matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec}; + let ff = require_ffmpeg!(); + let s = SPECS.iter().find(|s| s.name == "eac3_stereo_192k").unwrap(); + let raw = std::fs::read(make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name))).unwrap(); + let syncframes = frames(&raw); + let mut track = TrackSpec::new(TrackKind::Audio, "A_EAC3"); + track.audio = Some((f64::from(s.rate), u64::from(s.channels), None)); + let mut mkv = MkvWriter::new(std::io::Cursor::new(Vec::new()), vec![track], MuxOptions::default()).unwrap(); + let (mut i, mut pts_ns) = (0, 0i64); + while i < syncframes.len() { + let n = if i < 12 { 1 } else { 2.min(syncframes.len() - i) }; + let packet = syncframes[i..i + n].concat(); + let duration = 32_000_000 * n as u64; + mkv.write_frame(0, pts_ns, true, &packet, Some(duration)).unwrap(); + pts_ns += duration as i64; + i += n; + } + let file = mkv.finish().unwrap().into_inner(); + + let (_, whole) = craft_audio::decode(&file, Some("mkv")).unwrap(); + assert_eq!(whole.frames(), syncframes.len() * 1536); + for target in [20 * 1536 + 700, 30 * 1536] { + let mut dec = craft_audio::Decoder::open(file.clone(), Some("mkv"), &craft_audio::DecodeOptions::default()).unwrap(); + assert_eq!(dec.seek(target as u64).unwrap(), target as u64); + let block = dec.next_block().unwrap().unwrap(); + let n = block.channels[0].len(); + assert!(n > 0); + for (ours, theirs) in block.channels.iter().zip(&whole.channels) { + assert!(ours[..] == theirs[target..target + n], "after a seek to {target}"); + } + } +} diff --git a/crates/audio/tests/codecs/main.rs b/crates/audio/tests/codecs/main.rs index 7b58997f5..02932b108 100644 --- a/crates/audio/tests/codecs/main.rs +++ b/crates/audio/tests/codecs/main.rs @@ -8,6 +8,7 @@ use craft_media_testkit as common; mod testkit; mod aac; +mod ac3_oracle; mod concealment; mod opus; mod routing_oracle; diff --git a/crates/audio/tests/codecs/routing_oracle.rs b/crates/audio/tests/codecs/routing_oracle.rs index 9aa3b54f3..664a7e98f 100644 --- a/crates/audio/tests/codecs/routing_oracle.rs +++ b/crates/audio/tests/codecs/routing_oracle.rs @@ -27,7 +27,7 @@ struct Case { channels: usize, /// Presented frames, or `None` for a raw stream (as many as ffmpeg decodes). frames: Option, - /// Minimum SNR against ffmpeg. + /// Minimum SNR against ffmpeg (AC-3 is limited by decoder-specific dither, §7.3.4). min_snr: f64, } @@ -149,6 +149,71 @@ const CASES: &[Case] = &[ frames: Some(240_000), min_snr: 120.0, }, + Case { + name: "ac3.ac3", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "ac3", "-b:a", "192k"], + container: Container::Ac3, + codec: CodecId::Ac3, + layout: ChannelLayout::STEREO, + channels: 2, + frames: None, + min_snr: 25.0, + }, + Case { + name: "ac3-5.1.ac3", + surround: true, + mono: false, + rate: 48_000, + args: &["-c:a", "ac3", "-b:a", "448k"], + container: Container::Ac3, + codec: CodecId::Ac3, + layout: ChannelLayout::SURROUND_5_1_SIDE, + channels: 6, + frames: None, + min_snr: 25.0, + }, + Case { + name: "eac3.eac3", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "eac3", "-b:a", "192k"], + container: Container::Ac3, + codec: CodecId::Eac3, + layout: ChannelLayout::STEREO, + channels: 2, + frames: None, + min_snr: 25.0, + }, + Case { + name: "ac3.mp4", + surround: false, + mono: false, + rate: 48_000, + args: &["-c:a", "ac3"], + container: Container::Mp4, + codec: CodecId::Ac3, + layout: ChannelLayout::STEREO, + channels: 2, + frames: Some(240_000), + min_snr: 25.0, + }, + Case { + name: "eac3-5.1.mkv", + surround: true, + mono: false, + rate: 48_000, + args: &["-c:a", "eac3"], + container: Container::Matroska, + codec: CodecId::Eac3, + layout: ChannelLayout::SURROUND_5_1_SIDE, + channels: 6, + frames: Some(240_000), + min_snr: 25.0, + }, Case { name: "pcm-s24be.mov", surround: false, @@ -222,6 +287,7 @@ fn ffmpeg_major(ff: &Path) -> Option { fn reference_lead(name: &str) -> usize { match name { "aac.mkv" => 1024, + "eac3-5.1.mkv" => 256, _ => 0, } } @@ -243,6 +309,16 @@ fn snr(ours: &[Vec], reference: &[f32]) -> f64 { if e == 0.0 { f64::INFINITY } else { 10.0 * (s / e).log10() } } +/// How many frames short of the input ffmpeg before 9 ends `name`: its lengths for these tracks +/// leave out about the encoder's delay. The decoder presents what the file declares. +fn early_end(name: &str) -> usize { + match name { + "ac3.mp4" => 288, + "eac3-5.1.mkv" => 240, + _ => 0, + } +} + #[test] fn every_container_and_codec_decodes_home_grown() { let ff = require_ffmpeg!(); @@ -258,7 +334,8 @@ fn every_container_and_codec_decodes_home_grown() { assert_eq!((info.container, info.codec, info.sample_rate, buf.channels.len()), (c.container, c.codec, c.rate, c.channels), "{}", c.name); assert_eq!(info.layout, c.layout, "{}: layout", c.name); let expected = c.frames.unwrap_or(reference.len() / c.channels); - assert_eq!(buf.frames(), expected, "{}: presented frames", c.name); + let frames = buf.frames(); + assert!(frames == expected || frames == expected - early_end(c.name), "{}: presented frames: {frames} of {expected}", c.name); // One alignment, so audio that keeps the priming, or loses more than it, cannot pass. let lead = reference_lead(c.name) * c.channels; let s = match major { @@ -315,8 +392,7 @@ fn snr_planar(ours: &[Vec], reference: &[Vec]) -> f64 { } /// The streaming decoder gives the same audio as `decode`, block by block, and a seek lands on the -/// exact frame: what follows matches `decode` from there (bit for bit for the deterministic -/// codecs; within the codec's bound where the decoder state is reconstructed from a pre-roll). +/// exact frame: what follows matches `decode` from there, bit for bit. #[test] fn streaming_and_seeking_match_the_whole_file_decode() { use craft_audio::{DecodeOptions, Decoder}; @@ -345,7 +421,9 @@ fn streaming_and_seeking_match_the_whole_file_decode() { assert_eq!(after[0].len(), whole.frames() - target, "{}: frames after a seek to {target}", c.name); let expected: Vec> = whole.channels.iter().map(|ch| ch[target..].to_vec()).collect(); let s = snr_planar(&after, &expected); - // Opus rebuilds its predictor state from the pre-roll. + // Opus rebuilds its predictor state from the pre-roll. The rest are exact, AC-3 / E-AC-3 + // too: their dither is seeded from each syncframe, and the pre-roll frame restores the + // overlap. let bound = if c.codec == CodecId::Opus { c.min_snr.min(25.0) } else { f64::INFINITY }; assert!(s >= bound, "{}: SNR {s:.1} dB after a seek to {target}", c.name); } diff --git a/crates/audio/tests/codecs/testkit.rs b/crates/audio/tests/codecs/testkit.rs index 91b7f2ae6..1959fd3ab 100644 --- a/crates/audio/tests/codecs/testkit.rs +++ b/crates/audio/tests/codecs/testkit.rs @@ -58,4 +58,9 @@ pub mod fixtures { std::fs::rename(&tmp, out).ok()?; Some(out.to_path_buf()) } + + /// Run a fixture generator; FilmCraft's version also reports it to `cargo xtask fixtures`. + pub fn generate_and_report(_name: &str, _outputs: &[PathBuf], generate: impl FnOnce() -> Option) -> Option { + generate() + } }