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() + } }