diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index e380d9e96..9c6c1d22a 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "craft-audio" -description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders." +description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, AC-3/E-AC-3 decoder and encoder, Opus and MP1/MP2 decoders." version.workspace = true edition.workspace = true license.workspace = true @@ -22,8 +22,8 @@ decode = ["decode-native", "symphonia"] decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"] # The symphonia fallback for the formats with no native decoder yet. symphonia = ["decode-native", "dep:symphonia"] -# Every encoder: the lossless file writers, the AAC encoder and the ALAC packet encoder. -encode = ["lossless-encode", "aac-encode", "alac-encode"] +# Every encoder: the lossless file writers and the AAC, ALAC and AC-3/E-AC-3 packet encoders. +encode = ["lossless-encode", "aac-encode", "alac-encode", "ac3-encode", "eac3-encode"] # Encode WAV/BWF/RF64, AIFF/AIFF-C and FLAC files. lossless-encode = ["pcm-encode", "flac-encode"] # Encode WAV/BWF/RF64 and AIFF/AIFF-C files. @@ -39,6 +39,8 @@ mpa-decode = ["craft-media/bitstream"] # ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation. alac-decode = [] alac-encode = [] +ac3-encode = [] +eac3-encode = [] # Multi-resolution waveform overviews (`peaks`). peaks = [] # Encode AAC channels in parallel (with `aac-encode`). diff --git a/crates/audio/README.md b/crates/audio/README.md index ee550a9d8..1b67e4ea0 100644 --- a/crates/audio/README.md +++ b/crates/audio/README.md @@ -1,7 +1,7 @@ # craft-audio Audio file I/O and codecs for the Craft apps. Native implementations: WAV and AIFF read/write, -FLAC encoder, AAC and ALAC decoders/encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders. +FLAC encoder, AAC and ALAC decoders/encoders, AC-3/E-AC-3 decoder/encoder, Opus and MP1/MP2 decoders. Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced by native decoders. @@ -28,7 +28,7 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | `decode` | `decode`, `probe`, `Decoder`, `tracks`, `TrackDecoder`; every format in [Format support](#format-support) | `decode-native`, `symphonia` | | `decode-native` | the same, native formats only (others are `Unsupported`) | the five decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` | | `symphonia` | the fallback for formats with no native decoder; turning it on is the same as `decode` | `decode-native`, symphonia | -| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC and ALAC packet encoders | `lossless-encode`, `aac-encode`, `alac-encode` | +| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC, ALAC and AC-3/E-AC-3 packet encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode` | | `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` | | `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header` | none | | `flac-encode` | `encode`, `FileEncoder` for FLAC | craft-media `bitstream` | @@ -36,7 +36,8 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | `alac-decode` | `alac::Decoder` | none | | `alac-encode` | `alac::Encoder` | none | | `opus-decode` | `opus::Decoder`/`StreamDecoder` | none | -| `ac3-decode` | `ac3::Decoder` | none | +| `ac3-decode` | `ac3::Decoder`, `ac3::ProgrammeDecoder` | none | +| `ac3-encode`, `eac3-encode` | `ac3::encoder::Encoder` | none | | `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` | | `peaks` | `peaks::Peaks` | none | | `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` | @@ -302,7 +303,7 @@ match craft_audio::decode(&bytes, None) { | MP1, MP2 (MPEG audio Layer I/II) | native | — | | MP3 (Layer III) | symphonia | — | | Opus | native | — | -| AC-3, E-AC-3 | native | — | +| AC-3, E-AC-3 | native | native packet encoder | | Vorbis, ADPCM, A-law, µ-law | symphonia | — | | Container | Demuxer | Timing | @@ -335,8 +336,10 @@ match craft_audio::decode(&bytes, None) { the end a file declares is not presented. - A Matroska track's end comes from its timestamps, so it is known to one timestamp tick (usually 1 ms) unless the last block has `DiscardPadding`. -- E-AC-3: 7.1 programs decode as their 5.1 core. Adaptive hybrid transform and enhanced coupling - return `Unsupported`. +- E-AC-3: `ac3::Decoder` and `TrackDecoder` retain independent-substream decoding, so a + 7.1 programme is presented as its 5.1 compatibility downmix. Use `ac3::ProgrammeDecoder` + with an explicit expected layout for complete six-block 7.1 access units; see below. + Adaptive hybrid transform and enhanced coupling return `Unsupported`. - E-AC-3 with syncframes of fewer than six blocks: a seek decodes from the start of the stream, because such syncframes may reuse exponents from any syncframe before them. A seek in Matroska E-AC-3 reads the syncframe headers of the packets before its target to find the packets' places. @@ -401,10 +404,77 @@ Each codec module's rustdoc lists the coding tools it implements. | FLAC encoder | RFC 9639 | 16, 24, 32 bits; 1–8 channels, layouts decoders would not assume for the count as a `WAVEFORMATEXTENSIBLE_CHANNEL_MASK` comment; up to 655,350 Hz; LPC, stereo decorrelation, wasted bits, STREAMINFO MD5 | bit-exact through ffmpeg, STREAMINFO MD5 checked against ffmpeg | | AAC (`aac`) | ISO/IEC 14496-3 | AAC-LC decode and encode (CBR, VBR, ADTS), 1–8 channels; HE-AAC: see [Limits](#limits) | ffmpeg: ~135 dB SNR; PNS noise differs per sample at equal band energy | | Opus (`opus`) | RFC 6716, 8251, 7845 | decoder: SILK, hybrid, CELT, all frame sizes, PLC and FEC, multistream (`Decoder`) and single stream (`StreamDecoder`), 8–48 kHz output | all 36 RFC 8251 conformance cases | -| AC-3/E-AC-3 (`ac3`) | ATSC A/52:2012 | all AC-3 channel modes and frame sizes; E-AC-3 independent substream 0 with spectral extension; output in WAV order, no downmix | ffmpeg: 65–94 dB SNR on tones, 27–48 dB on white noise (see [Limits](#limits)) | +| AC-3/E-AC-3 (`ac3`) | decoder: ATSC A/52:2012; encoder/programme API: ETSI TS 102 366 V1.4.1 | single-syncframe and complete-programme decoding; packet encoding through discrete 7.1 (see below) | decoder comparisons and encoder output through ffmpeg, CRC/channel/timing/bit-budget regressions; synthetic quality limitations below | | MP1/MP2 (`mpa`) | ISO/IEC 11172-3, 13818-3 | Layer I and II, MPEG-1/2/2.5, 8–48 kHz; also the Layer II rate/mode pairs the spec disallows (ffmpeg's default mono 384 kbit/s); no free format | ffmpeg-encoded streams, 73–84 dB SNR | | ALAC (`alac`) | Apple's reference implementation | decode and encode, 1–8 channels, 16/20/24/32 bits (decode: any depth 1–32), fast mode, magic cookie in MP4/Matroska and CAF form | Apple's reference: same packets from the encoder, same samples from both decoders on 766 configurations; ffmpeg: bit-exact both ways | +### AC-3 and E-AC-3 encoding + +`ac3::encoder` is implemented from **ETSI TS 102 366 V1.4.1 (September 2017)**, +with the existing first-party decoder's normative allocation tables/math shared internally. +No third-party codec implementation source is used. Decoder executables are interoperability +oracles, not linked dependencies. This is a basic encoder implementation, not a Dolby-certified +product or a claim of perceptual parity with mature commercial encoders. + +The public `Config` chooses `Codec`, sample rate, programme bitrate, `CodingMode` and LFE. +`Encoder::new` validates these before taking PCM. Both codecs accept 32, 44.1 and 48 kHz, +all eight AC-3 coding modes (including distinct dual mono), and optional LFE. AC-3 uses +its standardized 32–640 kb/s frame-size table; this implementation requires at least +32 kb/s per full-bandwidth channel. E-AC-3 accepts 32–1024 kb/s programme rates, subject +to that same minimum and the per-substream 2048-word limit. E-AC-3 discrete 7.1 requires +LFE and at least 384 kb/s. Illegal rate/layout combinations return errors rather than +changing the requested codec or silently reducing channel count. + +PCM planes have equal lengths, finite values in ±1, and the explicit speaker order returned +by `channel_layout()`. Values outside this input contract are rejected. Ordinary 5.1 uses +**L, R, C, LFE, Ls, Rs**; discrete 7.1 uses WAVE order **L, R, C, LFE, Lrs, Rrs, Ls, Rs**. +Dual mono is an unordered layout of programme 1, programme 2 and optional LFE. Mono surround +and other intermediate modes retain their distinct speaker masks rather than using only a +channel count. + +The encoder performs windowed long/short transforms with transient switching, adaptive +D15/D25/D45 exponent grouping and reuse, standardized masking/bit allocation, grouped and +asymmetric mantissa quantization, and CRC generation. Rate control enforces both AC-3 +regional bit budgets as well as the total frame size; sparse frames use bounded skip fields +so auxiliary padding cannot violate the final-three-eighths constraint. LFE receives a +stateful six-pole 120 Hz low-pass before analysis. The initial implementation does not use +channel coupling, stereo rematrixing, AHT, spectral extension, enhanced coupling or VBR; +these are optional coding tools whose absence limits efficiency, particularly at low rates. +The emitted dialogue normalization is 31 and no dynamic-range gain processing is requested. + +A 7.1 E-AC-3 packet contains an independent 5.1 compatibility downmix followed by a dependent +substream carrying the original side and rear pairs (`chanmap = 0x1a00`). The compatibility +surrounds average side and rear inputs, **−6 dB per input**, so correlated full-scale signals +retain headroom. This differs from the optional Annex H `dmixtyp=1` −3 dB mix; that metadata +is not signalled. Full 7.1 decoding restores the original surrounds. The `dec3` descriptor +reports one dependent substream and the added rear pair, not the replacement side pair. + +`push` accepts arbitrary chunk sizes and returns complete 1536-sample access units. +`finish` drains overlap and returns final packets plus **256 priming samples** and exact +trailing padding; an empty stream returns no packets and zero trimming. A muxer should use +these counts once in its presentation timing. Raw elementary streams do not carry the +original arbitrary sample length, so raw-file consumers cannot recover this trimming by +inspecting the bitstream alone. `codec_configuration()` returns the `dac3` or `dec3` payload, +without its box header, for `ac-3`/`ec-3` sample entries. Dependent syncframes are part of the +same timed packet, not separately timestamped samples. Input validation errors consume no +PCM; an internal encoding error terminates that encoder, and its partial stream must be +discarded rather than retrying input with ambiguous consumption. + +`ac3::ProgrammeDecoder::new(expected_layout)` provides a separate, strict complete-access-unit +path for the encoder's six-block E-AC-3 topology. It checks CRCs, bounds, substream order, +layout, block counts and dependent channel maps before publishing PCM. Its explicit +full-range policy does not apply metadata DRC independently to component substreams. +Unsupported topology is rejected rather than reported as a successful reduced-channel decode. +The older single-syncframe decoder's contract is preserved. + +Validation includes all sample rates/coding modes, channel isolation, CRC and actual decoder +bit-position checks, short/long transition reconstruction, LFE response, chunk invariance, +invalid input and exact trim accounting. Independent executable checks cover original +impulse/tone/noise boundary stimuli and 7.1 channel identities, plus externally +produced 7.1 programme metadata. Synthetic SNR is a diagnostic, not a perceptual quality +certificate: broad dialogue/music listening, platform player behaviour and peak-memory +qualification remain separate checks. Lossy ringing can exceed the source PCM peak. + ## symphonia symphonia 0.6 (MPL-2.0, used unmodified as the Craft licensing rules require) decodes the formats @@ -508,8 +578,8 @@ The codec modules' public items keep their names, so apps can re-export them `Speaker::ALL` is a slice. - `TrackInfo::sample_rate` and `channels` are `Option`s. - Features are per codec. SoundCraft's `encode` (WAV, AIFF, FLAC) is now `lossless-encode`, made - of `pcm-encode` (WAV, AIFF) and `flac-encode`. `encode` turns on every encoder, AAC - included. Writing a format whose feature is off returns `AudioError::Unsupported`. + of `pcm-encode` (WAV, AIFF) and `flac-encode`. `encode` turns on every encoder, including AAC, + ALAC and AC-3/E-AC-3. Writing a format whose feature is off returns `AudioError::Unsupported`. - AAC encoder settings it cannot use are `aac::Error::InvalidSettings` (`AudioError::Encode`). - `TrackDecoder::decode` returns an error for a corrupt Opus packet, as for the other codecs; call `conceal`. diff --git a/crates/audio/src/ac3/eac3.rs b/crates/audio/src/ac3/eac3.rs index ce8d765bb..1b2ef1951 100644 --- a/crates/audio/src/ac3/eac3.rs +++ b/crates/audio/src/ac3/eac3.rs @@ -116,6 +116,10 @@ pub(crate) struct Bsi { /// reduced rates. pub fscod: usize, pub numblkscod: usize, + /// Explicit dependent-substream channel assignments (Table E.1.4). + pub chanmap: Option, + /// Presence of compression metadata also marks the last dependent substream. + pub compre: bool, } /// Read syncinfo and bsi (§E2.2.1-2), leaving `r` at the audio frame. @@ -138,7 +142,8 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result { let acmod = r.read(3) as usize; let lfeon = r.bit(); r.skip(5 + 5); // bsid, dialnorm - if r.bit() { + let compre = r.bit(); + if compre { r.skip(8); // compr } if acmod == 0 { @@ -147,9 +152,7 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result { r.skip(8); // compr2 } } - if strmtyp == 1 && r.bit() { - r.skip(16); // chanmap - } + let chanmap = if strmtyp == 1 && r.bit() { Some(r.read(16) as u16) } else { None }; if r.bit() { // mixing metadata if acmod > 2 { @@ -235,7 +238,7 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result { let l = r.read(6) as usize; r.skip((l + 1) * 8); // addbsi } - Ok(Bsi { strmtyp, fscod: rate_code, numblkscod }) + Ok(Bsi { strmtyp, fscod: rate_code, numblkscod, chanmap, compre }) } /// The audio frame (§E2.2.3) and the state that runs from block to block within a syncframe. diff --git a/crates/audio/src/ac3/encoder.rs b/crates/audio/src/ac3/encoder.rs new file mode 100644 index 000000000..573ad9f10 --- /dev/null +++ b/crates/audio/src/ac3/encoder.rs @@ -0,0 +1,325 @@ +//! Standards-derived AC-3 and E-AC-3 encoding (ETSI TS 102 366 V1.4.1). +//! +//! Input uses explicit WAVE speaker order. Each packet represents 1536 samples, +//! including all dependent substreams for a 7.1 programme. The encoder uses the +//! conventional transform/quantization tools; AHT and spectral extension are not used. +//! Dual mono uses programme 1, programme 2, then optional LFE rather than a +//! speaker mask. The 7.1 compatibility downmix averages each side/rear pair +//! (−6 dB per input) to retain headroom; dependent data restores the originals. +mod bitstream; +mod transform; + +use crate::ChannelLayout; + +/// The output bitstream family. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Codec { + /// AC-3 (`bsid = 8`). + Ac3, + /// Enhanced AC-3 (`bsid = 16`). + Eac3, +} + +/// Explicit speaker topology. Dual mono is not stereo. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum CodingMode { + /// Two independently controlled mono programmes. + DualMono = 0, + /// Centre. + Mono = 1, + /// Left and right. + Stereo = 2, + /// Left, centre and right. + ThreeFront = 3, + /// Left, right and mono surround. + StereoSurround = 4, + /// Left, centre, right and mono surround. + ThreeFrontSurround = 5, + /// Left, right and a surround pair. + StereoTwoSurround = 6, + /// Left, centre, right and a surround pair. + ThreeFrontTwoSurround = 7, + /// E-AC-3: independent 5.1 downmix plus dependent original side/rear channels. + Surround71 = 8, +} + +impl CodingMode { + /// Full-bandwidth channels, excluding LFE. + pub fn channels(self) -> usize { + if self == Self::Surround71 { 7 } else { super::NFCHANS[self as usize] } + } +} + +/// Constant-rate encoding settings. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Config { + /// AC-3 or E-AC-3. + pub codec: Codec, + /// PCM sampling frequency: 32000, 44100 or 48000 Hz. + pub sample_rate: u32, + /// Programme data rate, including any dependent substream, in kilobits/second. + pub bitrate_kbps: u32, + /// Speaker topology. + pub mode: CodingMode, + /// LFE channel present. Required for `Surround71`. + pub lfe: bool, +} + +/// Invalid configuration, PCM or lifecycle use. +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + /// The requested configuration cannot be encoded. + #[error("invalid AC-3 encoder settings: {0}")] + InvalidSettings(&'static str), + /// PCM channel lengths/counts or values are invalid. + #[error("invalid AC-3 encoder input: {0}")] + InvalidInput(&'static str), + /// No more samples can be submitted after finalization. + #[error("AC-3 encoder is already finished")] + Finished, + /// The configured frame cannot hold mandatory syntax. + #[error("AC-3 frame bit budget exhausted")] + BitBudget, +} + +impl Config { + /// Check format limits and supported configurations before accepting PCM. + pub fn validate(&self) -> Result<(), Error> { + if !matches!(self.sample_rate, 32_000 | 44_100 | 48_000) { + return Err(Error::InvalidSettings("sample rate must be 32000, 44100 or 48000 Hz")); + } + if self.mode == CodingMode::Surround71 && (self.codec != Codec::Eac3 || !self.lfe) { + return Err(Error::InvalidSettings("7.1 requires E-AC-3 and LFE")); + } + match self.codec { + Codec::Ac3 if !super::BITRATES.contains(&self.bitrate_kbps) => { + return Err(Error::InvalidSettings("bit rate is not an AC-3 frame-size table entry")); + } + Codec::Eac3 if !(32..=1024).contains(&self.bitrate_kbps) => { + return Err(Error::InvalidSettings("E-AC-3 programme bit rate must be 32..1024 kb/s")); + } + _ => {} + } + if self.codec == Codec::Eac3 { + let words = (u64::from(self.bitrate_kbps) * 96_000).div_ceil(u64::from(self.sample_rate)); + let largest = if self.mode == CodingMode::Surround71 { words * 3 / 5 } else { words }; + if largest > 2048 { + return Err(Error::InvalidSettings("E-AC-3 syncframe exceeds its 11-bit word count")); + } + } + let minimum = if self.mode == CodingMode::Surround71 { 384 } else { 32 * self.mode.channels() as u32 }; + if self.bitrate_kbps < minimum { + return Err(Error::InvalidSettings("bit rate is too low for the selected encoder channel topology")); + } + Ok(()) + } + + /// Number of PCM planes. + pub fn channels(&self) -> usize { + self.mode.channels() + usize::from(self.lfe) + } + + /// Required planar PCM speaker order (ascending WAVE mask bits). + pub fn channel_layout(&self) -> ChannelLayout { + if self.mode == CodingMode::DualMono { + return ChannelLayout::unordered(self.channels() as u16); + } + let mask = match self.mode { + CodingMode::Mono => 0x4, + CodingMode::Stereo => 0x3, + CodingMode::ThreeFront => 0x7, + CodingMode::StereoSurround => 0x103, + CodingMode::ThreeFrontSurround => 0x107, + CodingMode::StereoTwoSurround => 0x603, + CodingMode::ThreeFrontTwoSurround => 0x607, + CodingMode::Surround71 => 0x637, + CodingMode::DualMono => 0, + }; + ChannelLayout::from_mask(mask | if self.lfe { 8 } else { 0 }) + } +} + +/// One complete timed access unit, not an individual dependent syncframe. +#[derive(Clone, Debug)] +pub struct Packet { + /// Raw syncframes in independent/dependent order. + pub data: Vec, + /// Decoded samples per channel, including any encoder priming or padding. + pub samples: u32, +} + +/// Final packets and exact container trimming information. +#[derive(Clone, Debug)] +pub struct Finish { + /// Access units emitted while draining transform overlap. + pub packets: Vec, + /// Samples to trim at the beginning of the decoded stream. + pub priming_samples: u32, + /// Samples to trim at the end of the decoded stream. + pub padding_samples: u32, +} + +/// Streaming encoder with bounded transform history. +pub struct Encoder { + config: Config, + pending: Vec>, + history: Vec<[f32; 256]>, + frame_index: u64, + input_samples: u64, + finished: bool, + lfe_filter: Option<(usize, transform::LowPass)>, +} + +impl Encoder { + /// Construct a validated encoder; no PCM is consumed on error. + pub fn new(config: Config) -> Result { + config.validate()?; + Ok(Self { + pending: vec![Vec::new(); config.channels()], + history: vec![[0.0; 256]; config.channels()], + config, + frame_index: 0, + input_samples: 0, + finished: false, + lfe_filter: config.lfe.then(|| { + let index = if config.mode == CodingMode::Mono { + 1 + } else if matches!( + config.mode, + CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround | CodingMode::Surround71 + ) { + 3 + } else { + 2 + }; + (index, transform::LowPass::new(config.sample_rate)) + }), + }) + } + + /// Required input speaker assignments. + pub fn channel_layout(&self) -> ChannelLayout { + self.config.channel_layout() + } + + /// The analysis window introduces 256 decoded priming samples. + pub fn priming_samples(&self) -> u32 { + 256 + } + + /// Payload of an ISO BMFF `dac3` or `dec3` configuration box, without its box header. + pub fn codec_configuration(&self) -> Vec { + bitstream::configuration(self.config) + } + + /// Accept equally sized finite PCM planes in the documented WAVE order. + /// Values outside ±1 are rejected rather than silently clipped. Input errors + /// leave the encoder untouched. A frame-encoding error is terminal; discard + /// the stream rather than retrying input whose consumption is then unspecified. + pub fn push(&mut self, planes: &[&[f32]]) -> Result, Error> { + if self.finished { + return Err(Error::Finished); + } + if planes.len() != self.config.channels() { + return Err(Error::InvalidInput("channel count differs from configuration")); + } + let count = planes.first().map_or(0, |p| p.len()); + if planes.iter().any(|p| p.len() != count || p.iter().any(|v| !v.is_finite() || v.abs() > 1.0)) { + return Err(Error::InvalidInput("PCM must be finite, normalized and have equal plane lengths")); + } + self.input_samples = + self.input_samples.checked_add(count as u64).filter(|&n| n <= u64::MAX - 1791).ok_or(Error::InvalidInput("sample count overflow"))?; + let mut packets = Vec::new(); + let mut offset = 0; + while offset < count { + let take = (1536 - self.pending[0].len()).min(count - offset); + for (ch, (dst, src)) in self.pending.iter_mut().zip(planes).enumerate() { + if let Some((lfe, filter)) = &mut self.lfe_filter + && ch == *lfe + { + dst.extend(src[offset..offset + take].iter().map(|&value| filter.process(value))); + } else { + dst.extend_from_slice(&src[offset..offset + take]); + } + } + offset += take; + if self.pending[0].len() == 1536 { + packets.push(self.emit()?); + } + } + Ok(packets) + } + + /// Drain the last overlap and return exact priming/padding counts. Empty input emits no packets. + pub fn finish(&mut self) -> Result { + if self.finished { + return Err(Error::Finished); + } + if self.input_samples == 0 { + self.finished = true; + return Ok(Finish { packets: Vec::new(), priming_samples: 0, padding_samples: 0 }); + } + let padded = self.input_samples.checked_add(256).ok_or(Error::InvalidInput("sample count overflow"))?; + let required = padded.div_ceil(1536); + let decoded_samples = required.checked_mul(1536).ok_or(Error::InvalidInput("sample count overflow"))?; + let mut packets = Vec::new(); + while self.frame_index < required { + for (ch, p) in self.pending.iter_mut().enumerate() { + if let Some((lfe, filter)) = &mut self.lfe_filter + && ch == *lfe + { + while p.len() < 1536 { + p.push(filter.process(0.0)); + } + } else { + p.resize(1536, 0.0); + } + } + packets.push(self.emit()?); + } + self.finished = true; + Ok(Finish { packets, priming_samples: 256, padding_samples: (decoded_samples - padded) as u32 }) + } + + fn emit(&mut self) -> Result { + let mut spectra = vec![Vec::with_capacity(6); self.config.channels()]; + let mut short = [false; 6]; + for (block, short_block) in short.iter_mut().enumerate() { + let windows: Vec<[f32; 512]> = (0..self.config.channels()) + .map(|ch| { + let mut window = [0.0; 512]; + if block == 0 { + window[..256].copy_from_slice(&self.history[ch]); + } else { + window[..256].copy_from_slice(&self.pending[ch][(block - 1) * 256..block * 256]); + } + window[256..].copy_from_slice(&self.pending[ch][block * 256..(block + 1) * 256]); + window + }) + .collect(); + *short_block = + windows.iter().enumerate().any(|(ch, w)| self.lfe_filter.as_ref().is_none_or(|(lfe, _)| ch != *lfe) && transform::has_transient(w)); + for (ch, window) in windows.iter().enumerate() { + let full_bandwidth = self.lfe_filter.as_ref().is_none_or(|(lfe, _)| ch != *lfe); + spectra[ch].push(transform::analysis(window, *short_block && full_bandwidth)); + } + } + let data = match bitstream::encode(self.config, self.frame_index, &spectra, &short) { + Ok(data) => data, + Err(error) => { + self.finished = true; + return Err(error); + } + }; + for (history, pending) in self.history.iter_mut().zip(&mut self.pending) { + history.copy_from_slice(&pending[1280..1536]); + pending.clear(); + } + self.frame_index += 1; + Ok(Packet { data, samples: 1536 }) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/audio/src/ac3/encoder/bitstream.rs b/crates/audio/src/ac3/encoder/bitstream.rs new file mode 100644 index 000000000..9a0af9deb --- /dev/null +++ b/crates/audio/src/ac3/encoder/bitstream.rs @@ -0,0 +1,631 @@ +//! Syntax writer and bounded rate control; ETSI TS 102 366 §§4, 6, 7 and Annex E/F. +use super::super::{AllocParams, Globals, bit_allocation}; +use super::{Codec, CodingMode, Config, Error}; + +#[derive(Default, Clone)] +struct Writer { + bytes: Vec, + bits: usize, +} +impl Writer { + fn put(&mut self, value: u32, count: u32) { + for bit in (0..count).rev() { + if self.bits.is_multiple_of(8) { + self.bytes.push(0); + } + if value & (1 << bit) != 0 { + self.bytes[self.bits / 8] |= 1 << (7 - self.bits % 8); + } + self.bits += 1; + } + } + fn append(&mut self, other: &Self) { + for bit in 0..other.bits { + self.put(u32::from((other.bytes[bit / 8] >> (7 - bit % 8)) & 1), 1); + } + } + fn flag(&mut self, value: bool) { + self.put(u32::from(value), 1); + } +} + +fn rate_code(rate: u32) -> usize { + match rate { + 48_000 => 0, + 44_100 => 1, + _ => 2, + } +} + +pub(super) fn configuration(config: Config) -> Vec { + let mut w = Writer::default(); + let acmod = if config.mode == CodingMode::Surround71 { 7 } else { config.mode as u32 }; + match config.codec { + Codec::Ac3 => { + w.put(rate_code(config.sample_rate) as u32, 2); + w.put(8, 5); + w.put(0, 3); + w.put(acmod, 3); + w.flag(config.lfe); + w.put(super::super::BITRATES.iter().position(|&b| b == config.bitrate_kbps).unwrap_or(0) as u32, 5); + w.put(0, 5); + } + Codec::Eac3 => { + // Ceiling frame rate describes the maximum rate when 44.1 kHz needs alternating sizes. + let words = (u64::from(config.bitrate_kbps) * 96_000).div_ceil(u64::from(config.sample_rate)); + let max_rate = (words * 16 * u64::from(config.sample_rate)).div_ceil(1_536_000); + w.put(max_rate as u32, 13); + w.put(0, 3); // one independent substream + w.put(rate_code(config.sample_rate) as u32, 2); + w.put(16, 5); + w.put(0, 2); + w.put(0, 3); + w.put(acmod, 3); + w.flag(config.lfe); + w.put(0, 3); + let dependent = config.mode == CodingMode::Surround71; + w.put(u32::from(dependent), 4); + if dependent { + w.put(2, 9); + } else { + w.put(0, 1); + } + } + } + w.bytes +} + +pub(super) fn encode(config: Config, frame_index: u64, spectra: &[Vec<[f32; 256]>], short: &[bool; 6]) -> Result, Error> { + if config.mode == CodingMode::Surround71 { + // Input WAVE order: L R C LFE Lrs Rrs Ls Rs. Independent stream is a + // compatible 5.1 downmix; the dependent stream restores original surrounds. + // Averaging (−6 dB per input) keeps correlated full-scale sides/rears + // bounded. This intentionally is not Annex H dmixtyp=1's −3 dB mix; + // that optional metadata is not emitted. + let mut primary = + vec![spectra[0].clone(), spectra[2].clone(), spectra[1].clone(), spectra[6].clone(), spectra[7].clone(), spectra[3].clone()]; + for block in 0..6 { + for bin in 0..256 { + primary[3][block][bin] = (spectra[6][block][bin] + spectra[4][block][bin]) * 0.5; + primary[4][block][bin] = (spectra[7][block][bin] + spectra[5][block][bin]) * 0.5; + } + } + let dependent = vec![spectra[6].clone(), spectra[7].clone(), spectra[4].clone(), spectra[5].clone()]; + let total_words = frame_words(config, frame_index); + let primary_words = total_words * 3 / 5; + let mut output = syncframe(config, 7, true, 0, primary_words, &primary, short)?; + output.extend(syncframe(config, 6, false, 1, total_words - primary_words, &dependent, short)?); + return Ok(output); + } + let nf = config.mode.channels(); + let fronts = match config.mode { + CodingMode::Mono => 1, + CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround => 3, + _ => 2, + }; + let mut order: Vec = (0..nf).map(|ch| if config.lfe && ch >= fronts { ch + 1 } else { ch }).collect(); + if fronts == 3 { + order.swap(1, 2); + } + if config.lfe { + order.push(fronts); + } + let coded: Vec<_> = order.iter().map(|&ch| spectra[ch].clone()).collect(); + syncframe(config, config.mode as u8, config.lfe, 0, frame_words(config, frame_index), &coded, short) +} + +fn frame_words(config: Config, index: u64) -> usize { + let numerator = u64::from(config.bitrate_kbps) * 96_000; + let denominator = u64::from(config.sample_rate); + // An accumulator without unbounded state drift or long-stream multiplication overflow. + let floor = numerator / denominator; + let remainder = numerator % denominator; + let phase = (index % denominator) * remainder % denominator; + (floor + u64::from(phase + remainder >= denominator)) as usize +} + +struct Channel { + coefficients: Vec<[f32; 256]>, + exponents: [[u8; 256]; 6], + codes: [Vec; 6], + strategies: [u8; 6], + end: usize, +} + +fn channels(spectra: &[Vec<[f32; 256]>], nf: usize, bandwidth: usize, short: &[bool; 6]) -> Vec { + spectra + .iter() + .enumerate() + .map(|(index, blocks)| { + let end = if index == nf { 7 } else { 73 + 3 * bandwidth }; + let raw: Vec<[u8; 256]> = blocks + .iter() + .map(|block| { + std::array::from_fn(|bin| { + let peak = block[bin].abs(); + if peak > 0.0 { (-peak.log2()).floor().clamp(0.0, 24.0) as u8 } else { 24 } + }) + }) + .collect(); + let mut starts = vec![0]; + let mut start = 0; + for block in 1..6 { + let difference: u32 = (0..end).map(|bin| u32::from(raw[block][bin].abs_diff(raw[start][bin]))).sum(); + if (index < nf && short[block] != short[block - 1]) || difference > end as u32 * 2 { + starts.push(block); + start = block; + } + } + starts.push(6); + let mut channel = Channel { + coefficients: blocks.clone(), + exponents: [[24; 256]; 6], + codes: std::array::from_fn(|_| Vec::new()), + strategies: [0; 6], + end, + }; + for range in starts.windows(2) { + let (first, last) = (range[0], range[1]); + let strategy = if index == nf || last - first >= 4 { + 1 + } else if last - first >= 2 { + 2 + } else { + 3 + }; + let size = 1usize << (strategy - 1); + let groups = (end - 1 + 3 * size - 3) / (3 * size); + let mut envelope = [24u8; 256]; + for bin in 0..end { + envelope[bin] = (first..last).map(|block| raw[block][bin]).min().unwrap_or(24); + } + let mut codes = Vec::with_capacity(1 + groups * 3); + codes.push(envelope[0].min(15)); + for group in 0..groups * 3 { + codes.push((0..size).map(|i| envelope[(1 + group * size + i).min(end - 1)]).min().unwrap_or(24)); + } + for i in 1..codes.len() { + codes[i] = codes[i].min(codes[i - 1] + 2); + } + for i in (0..codes.len() - 1).rev() { + codes[i] = codes[i].min(codes[i + 1] + 2); + } + let reconstructed: [u8; 256] = + std::array::from_fn(|bin| if bin == 0 { codes[0] } else { codes.get(1 + (bin - 1) / size).copied().unwrap_or(24) }); + for block in first..last { + channel.exponents[block] = reconstructed; + } + channel.strategies[first] = strategy; + channel.codes[first] = codes; + } + channel + }) + .collect() +} + +#[allow(clippy::too_many_arguments)] +fn syncframe( + config: Config, + acmod: u8, + lfe: bool, + stream_type: u32, + words: usize, + spectra: &[Vec<[f32; 256]>], + short: &[bool; 6], +) -> Result, Error> { + if !(64..=2048).contains(&words) && config.codec == Codec::Eac3 { + return Err(Error::BitBudget); + } + let nf = super::super::NFCHANS[acmod as usize]; + let per_channel = config.bitrate_kbps / config.mode.channels() as u32; + let cutoff = if per_channel < 48 { + 12_000 + } else if per_channel < 64 { + 16_000 + } else { + 20_000 + }; + let target_bin = (cutoff * 512 / config.sample_rate).min(253) as usize; + let max_bw = target_bin.saturating_sub(73) / 3; + for bandwidth in (0..=max_bw.min(60)).rev() { + let channels = channels(spectra, nf, bandwidth, short); + // Rate control uses exactly the reconstructed exponent envelope and the + // decoder's standardized allocation math, not the preprocessed spectrum. + let mut low = 0; + let mut high = 1023; + let mut best = None; + while low <= high { + let snr = (low + high) / 2; + match serialize(config, acmod, lfe, stream_type, words, bandwidth, &channels, snr, short) { + Some(frame) => { + best = Some(frame); + low = snr + 1; + } + None => { + if snr == 0 { + break; + } + high = snr - 1; + } + } + } + if let Some(frame) = best { + return Ok(frame); + } + } + Err(Error::BitBudget) +} + +#[allow(clippy::too_many_arguments)] +fn serialize( + config: Config, + acmod: u8, + lfe: bool, + stream_type: u32, + words: usize, + bandwidth: usize, + channels: &[Channel], + snr: u32, + short: &[bool; 6], +) -> Option> { + let enhanced = config.codec == Codec::Eac3; + let mut w = Writer::default(); + w.put(0x0b77, 16); + if enhanced { + eac3_header(&mut w, config, acmod, lfe, stream_type, words, channels, snr); + } else { + ac3_header(&mut w, config, acmod, lfe, words); + } + let globals = Globals { fscod: rate_code(config.sample_rate), sdcycod: 2, fdcycod: 1, sgaincod: 1, dbpbcod: 2, floorcod: 7 }; + let baps: Vec> = (0..6) + .map(|block| { + channels + .iter() + .map(|channel| { + let mut bap = [0; 256]; + if snr != 0 { + let params = AllocParams { + start: 0, + end: channel.end, + fgain: super::super::FASTGAIN[4], + snroffset: (snr as i32 - 240) * 4, + leak: None, + dba: None, + }; + bit_allocation(&channel.exponents[block], ¶ms, &globals, &mut bap); + } + bap + }) + .collect() + }) + .collect(); + let split = (words / 2 + words / 8) * 16; + let mut headers: Vec = Vec::with_capacity(6); + let mut mantissas: Vec = Vec::with_capacity(6); + for block in 0..6 { + let mut header = Writer::default(); + block_header(&mut header, enhanced, acmod, lfe, stream_type, bandwidth, channels, snr, block, short[block]); + let mut mantissa = Writer::default(); + write_mantissas(&mut mantissa, channels, &baps[block], block); + headers.push(header); + mantissas.push(mantissa); + } + let skips = if enhanced { [None; 6] } else { plan_skips(w.bits, &headers, &mantissas, words)? }; + for block in 0..6 { + w.append(&headers[block]); + if !enhanced { + w.flag(skips[block].is_some()); + if let Some(bytes) = skips[block] { + w.put(bytes as u32, 9); + for _ in 0..bytes { + w.put(0, 8); + } + } + if block == 5 && w.bits < split { + return None; + } + } + w.append(&mantissas[block]); + if !enhanced && block == 1 && w.bits > split { + return None; + } + if w.bits + 18 > words * 16 { + return None; + } + } + w.bytes.resize(words * 2, 0); // auxiliary padding, auxdatae and crcrsv/encinfo are zero + let size = w.bytes.len(); + if !enhanced { + let split_bytes = split / 8; + let residue = crc(&w.bytes[2..split_bytes]); + let mut prefix = residue; + for _ in 0..(split_bytes - 2) * 8 { + prefix = reverse_zero_bit(prefix); + } + w.bytes[2..4].copy_from_slice(&prefix.to_be_bytes()); + } + let mut tail = crc(&w.bytes[2..size - 2]); + if !enhanced && tail == 0x0b77 { + w.bytes[size - 3] ^= 1; // reserved bit prevents a false syncword in CRC2 + tail = crc(&w.bytes[2..size - 2]); + } + w.bytes[size - 2..].copy_from_slice(&tail.to_be_bytes()); + Some(w.bytes) +} + +/// §4.5 limits the complete final region, including unused auxiliary padding. +/// Move excess padding into bounded skip fields before block 5's mantissas. +fn plan_skips(prefix: usize, headers: &[Writer], mantissas: &[Writer], words: usize) -> Option<[Option; 6]> { + let total = words * 16; + let split = (words / 2 + words / 8) * 16; + let original = prefix + headers.iter().map(|h| h.bits + 1).sum::() + mantissas.iter().map(|m| m.bits).sum::() + 18; + if original > total || mantissas[5].bits + 18 > total - split { + return None; + } + let before_last = original - mantissas[5].bits - 18; + let mut missing = split.saturating_sub(before_last); + let mut skips = [None; 6]; + let mut added = 0; + for block in (0..6).rev() { + if missing == 0 { + break; + } + let bytes = missing.saturating_sub(9).div_ceil(8).min(511); + let bits = 9 + bytes * 8; + skips[block] = Some(bytes); + added += bits; + missing = missing.saturating_sub(bits); + } + if missing != 0 || original + added > total { + return None; + } + Some(skips) +} + +#[allow(clippy::too_many_arguments)] +fn block_header( + w: &mut Writer, + enhanced: bool, + acmod: u8, + lfe: bool, + stream_type: u32, + bandwidth: usize, + channels: &[Channel], + snr: u32, + block: usize, + short: bool, +) { + let nf = super::super::NFCHANS[acmod as usize]; + // Block switches are common to full-bandwidth channels; LFE always uses long blocks. + for _ in 0..nf { + w.flag(short); + } + for _ in 0..nf { + w.flag(false); + } + for _ in 0..if acmod == 0 { 2 } else { 1 } { + w.flag(false); + } // dynrng + if enhanced { + if block == 0 { + w.flag(false); + } + } + // spxinu, later spxstre + else { + w.flag(block == 0); + if block == 0 { + w.flag(false); + } + } + if enhanced && block > 0 { + w.flag(false); + } // spxstre + if acmod == 2 { + if !enhanced || block > 0 { + w.flag(block == 0); + } + if block == 0 { + w.put(0, 4); + } + } + if !enhanced { + for channel in &channels[..nf] { + w.put(u32::from(channel.strategies[block]), 2); + } + if lfe { + w.flag(channels[nf].strategies[block] != 0); + } + } + for channel in &channels[..nf] { + if channel.strategies[block] != 0 { + w.put(bandwidth as u32, 6); + } + } + for (ch, channel) in channels.iter().enumerate() { + if channel.strategies[block] == 0 { + continue; + } + let codes = &channel.codes[block]; + w.put(u32::from(codes[0]), 4); + for first in (1..codes.len()).step_by(3) { + let mut previous = i32::from(codes[first - 1]); + let mut packed = 0; + for &exponent in &codes[first..first + 3] { + packed = packed * 5 + (i32::from(exponent) - previous + 2) as u32; + previous = i32::from(exponent); + } + w.put(packed, 7); + } + if ch < nf { + w.put(0, 2); + } + } + if !enhanced { + w.flag(block == 0); + if block == 0 { + w.put(2, 2); + w.put(1, 2); + w.put(1, 2); + w.put(2, 2); + w.put(7, 3); + } + w.flag(block == 0); + if block == 0 { + w.put(snr / 16, 6); + for _ in channels { + w.put(snr % 16, 4); + w.put(4, 3); + } + } + w.flag(false); // delta allocation + } else if stream_type == 0 { + w.flag(false); + } // convsnroffste +} + +fn ac3_header(w: &mut Writer, config: Config, acmod: u8, lfe: bool, words: usize) { + w.put(0, 16); // CRC1, patched after allocation + w.put(rate_code(config.sample_rate) as u32, 2); + let index = super::super::BITRATES.iter().position(|&b| b == config.bitrate_kbps).unwrap_or(0); + let base = (u64::from(config.bitrate_kbps) * 96_000 / u64::from(config.sample_rate)) as usize; + w.put((index * 2 + usize::from(words > base)) as u32, 6); + w.put(8, 5); + w.put(0, 3); + w.put(u32::from(acmod), 3); + if acmod & 1 != 0 && acmod != 1 { + w.put(1, 2); + } + if acmod & 4 != 0 { + w.put(1, 2); + } + if acmod == 2 { + w.put(0, 2); + } + w.flag(lfe); + for _ in 0..if acmod == 0 { 2 } else { 1 } { + w.put(31, 5); + w.put(0, 3); + } + w.put(0, 1); + w.put(1, 1); + w.put(0, 3); +} + +#[allow(clippy::too_many_arguments)] +fn eac3_header(w: &mut Writer, config: Config, acmod: u8, lfe: bool, stream_type: u32, words: usize, channels: &[Channel], snr: u32) { + let nf = super::super::NFCHANS[acmod as usize]; + w.put(stream_type, 2); + w.put(0, 3); + w.put((words - 1) as u32, 11); + w.put(rate_code(config.sample_rate) as u32, 2); + w.put(3, 2); + w.put(u32::from(acmod), 3); + w.flag(lfe); + w.put(16, 5); + w.put(31, 5); + w.flag(stream_type == 1); + if stream_type == 1 { + w.put(0, 8); + } + if acmod == 0 { + w.put(31, 5); + w.flag(false); + } + if stream_type == 1 { + w.flag(true); + w.put(0x1a00, 16); + } + w.flag(false); + w.flag(false); + w.flag(false); // mixmdate, infomdate, addbsie + w.flag(true); + w.flag(false); // per-block exponents, AHT disabled + w.put(0, 2); // frame-level SNR + w.flag(false); + w.flag(true); + w.flag(true); // transproce, blkswe, dithflage + w.put(0, 5); // bamode, frmfgaincode, dbaflde, skipflde, spxattene + if acmod > 1 { + w.flag(false); + for _ in 1..6 { + w.flag(false); + } + } + for block in 0..6 { + for channel in &channels[..nf] { + w.put(u32::from(channel.strategies[block]), 2); + } + } + if lfe { + for block in 0..6 { + w.flag(channels[nf].strategies[block] != 0); + } + } + if stream_type == 0 { + for _ in 0..nf { + w.put(0, 5); + } + } // converter exponents + w.put(snr / 16, 6); + w.put(snr % 16, 4); + w.flag(false); // block-start information omitted +} + +fn write_mantissas(w: &mut Writer, channels: &[Channel], baps: &[[u8; 256]], block: usize) { + let values: Vec<(u8, f32)> = channels + .iter() + .zip(baps) + .flat_map(|(channel, bap)| { + (0..channel.end).map(move |bin| (bap[bin], channel.coefficients[block][bin] * 2f32.powi(i32::from(channel.exponents[block][bin])))) + }) + .collect(); + let mut grouped = vec![None; values.len()]; + for (bap, levels, group_size, bits) in [(1, 3, 3, 5), (2, 5, 3, 7), (4, 11, 2, 7)] { + let indexes: Vec<_> = values.iter().enumerate().filter_map(|(i, &(b, _))| (b == bap).then_some(i)).collect(); + for group in indexes.chunks(group_size) { + let mut code = 0; + for slot in 0..group_size { + let value = group.get(slot).map_or(0.0, |&i| values[i].1); + code = code * levels + quantize_symmetric(value, levels); + } + grouped[group[0]] = Some((code, bits)); + } + } + for (index, (bap, value)) in values.into_iter().enumerate() { + if let Some((code, bits)) = grouped[index] { + w.put(code, bits); + continue; + } + match bap { + 0 | 1 | 2 | 4 => {} + 3 => w.put(quantize_symmetric(value, 7), 3), + 5 => w.put(quantize_symmetric(value, 15), 4), + _ => { + let bits = super::super::QNTZTAB[usize::from(bap)]; + let scale = 1i32 << (bits - 1); + let code = (value * scale as f32).round().clamp(-scale as f32, (scale - 1) as f32) as i32; + w.put(code as u32 & ((1 << bits) - 1), bits); + } + } + } +} + +fn quantize_symmetric(value: f32, levels: u32) -> u32 { + ((value * levels as f32 + (levels - 1) as f32) * 0.5).round().clamp(0.0, (levels - 1) as f32) as u32 +} + +pub(super) fn crc(bytes: &[u8]) -> u16 { + let mut value = 0u16; + for &byte in bytes { + value ^= u16::from(byte) << 8; + for _ in 0..8 { + value = if value & 0x8000 != 0 { (value << 1) ^ 0x8005 } else { value << 1 }; + } + } + value +} +fn reverse_zero_bit(value: u16) -> u16 { + let high = value & 1; + ((value ^ if high != 0 { 0x8005 } else { 0 }) >> 1) | (high << 15) +} diff --git a/crates/audio/src/ac3/encoder/tests.rs b/crates/audio/src/ac3/encoder/tests.rs new file mode 100644 index 000000000..3545f48ca --- /dev/null +++ b/crates/audio/src/ac3/encoder/tests.rs @@ -0,0 +1,388 @@ +use super::*; + +#[test] +fn analysis_synthesis_reconstructs_overlap() { + let input: Vec = (0..1024).map(|n| (n as f32 * 0.071).sin() * 0.4).collect(); + let mut overlap = [0.0; 256]; + for offset in (0..768).step_by(256) { + let mut block = [0.0; 512]; + block.copy_from_slice(&input[offset..offset + 512]); + let coeffs = transform::analysis(&block, false); + let mut decoded = [0.0; 512]; + super::super::imdct(&coeffs, false, &mut decoded); + if offset > 0 { + for i in 0..256 { + assert!((2.0 * (decoded[i] + overlap[i]) - input[offset + i]).abs() < 1e-5); + } + } + overlap.copy_from_slice(&decoded[256..]); + } +} + +fn config(codec: Codec, mode: CodingMode, lfe: bool) -> Config { + Config { codec, sample_rate: 48_000, bitrate_kbps: 448, mode, lfe } +} + +#[test] +fn encode_tone_headers_crc_and_level() -> Result<(), Box> { + for codec in [Codec::Ac3, Codec::Eac3] { + for (mode, lfe) in [(CodingMode::Mono, false), (CodingMode::Stereo, false), (CodingMode::ThreeFrontTwoSurround, true)] { + let cfg = config(codec, mode, lfe); + let input: Vec> = (0..cfg.channels()) + .map(|ch| (0..3072).map(|n| (n as f32 * (if lfe && ch == 3 { 0.01 } else { 0.04 + ch as f32 * 0.015 })).sin() * 0.2).collect()) + .collect(); + let mut encoder = Encoder::new(cfg)?; + let mut packets = encoder.push(&input.iter().map(Vec::as_slice).collect::>())?; + let finish = encoder.finish()?; + packets.extend(finish.packets); + let mut decoder = super::super::Decoder::new(); + let mut decoded = vec![Vec::new(); cfg.channels()]; + for packet in &packets { + let header = super::super::parse_header(&packet.data)?; + assert_eq!(header.channels(), cfg.channels()); + assert_eq!(header.samples(), 1536); + assert_eq!(bitstream::crc(&packet.data[2..]), 0); + if codec == Codec::Ac3 { + let words = packet.data.len() / 2; + let split = (words / 2 + words / 8) * 2; + assert_eq!(bitstream::crc(&packet.data[2..split]), 0); + } + let frame = decoder.decode(&packet.data)?; + for (dst, src) in decoded.iter_mut().zip(frame.channels) { + dst.extend(src); + } + } + for ch in 0..cfg.channels() { + if lfe && ch == 3 { + continue; + } // The causal LFE low-pass intentionally changes phase. + let mse = input[ch].iter().zip(&decoded[ch][256..]).map(|(&a, &b)| (a - b).powi(2)).sum::() / input[ch].len() as f32; + assert!(mse < 0.0001, "{codec:?} {mode:?} ch{ch}: mse={mse}"); + } + } + } + Ok(()) +} + +#[test] +fn long_short_transitions_reconstruct_without_gain_or_phase_steps() { + let mut seed = 23u32; + let input: Vec = (0..2048) + .map(|_| { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + (seed as f32 / u32::MAX as f32 - 0.5) * 0.8 + }) + .collect(); + for switches in [[false; 6], [true; 6], [false, true, false, true, true, false]] { + let mut overlap = [0.0; 256]; + for (block, short) in switches.into_iter().enumerate() { + let offset = block * 256; + let mut samples = [0.0; 512]; + samples.copy_from_slice(&input[offset..offset + 512]); + let coefficients = transform::analysis(&samples, short); + let mut decoded = [0.0; 512]; + super::super::imdct(&coefficients, short, &mut decoded); + if block > 0 { + for i in 0..256 { + assert!((2.0 * (decoded[i] + overlap[i]) - input[offset + i]).abs() < 1e-5); + } + } + overlap.copy_from_slice(&decoded[256..]); + } + } +} + +#[test] +fn lfe_lowpass_preserves_bass_and_rejects_full_bandwidth_content() { + for rate in [32000, 44100, 48000] { + for (frequency, minimum, maximum) in [(60.0, 0.98, 1.01), (120.0, 0.69, 0.72), (1000.0, 0.0, 0.00001)] { + let mut filter = transform::LowPass::new(rate); + let mut power = 0.0; + let mut source_power = 0.0; + for n in 0..rate { + let input = (n as f32 * std::f32::consts::TAU * frequency / rate as f32).sin() * 0.5; + let output = filter.process(input); + if n > rate / 2 { + power += f64::from(output).powi(2); + source_power += f64::from(input).powi(2); + } + } + let gain = (power / source_power).sqrt(); + assert!((minimum..=maximum).contains(&gain), "rate={rate} frequency={frequency} gain={gain}"); + } + } +} + +#[test] +fn rates_modes_and_crc_boundaries() -> Result<(), Box> { + for codec in [Codec::Ac3, Codec::Eac3] { + for rate in [32000, 44100, 48000] { + for mode in [ + CodingMode::DualMono, + CodingMode::Mono, + CodingMode::Stereo, + CodingMode::ThreeFront, + CodingMode::StereoSurround, + CodingMode::ThreeFrontSurround, + CodingMode::StereoTwoSurround, + CodingMode::ThreeFrontTwoSurround, + ] { + for lfe in [false, true] { + let cfg = Config { sample_rate: rate, ..config(codec, mode, lfe) }; + let mut encoder = Encoder::new(cfg)?; + let planes = vec![vec![0.0; 1537]; cfg.channels()]; + let mut packets = encoder.push(&planes.iter().map(Vec::as_slice).collect::>())?; + let finish = encoder.finish()?; + packets.extend(finish.packets); + assert_eq!(packets.len(), 2); + assert_eq!(finish.padding_samples, 1279); + for packet in packets { + let h = super::super::parse_header(&packet.data)?; + assert_eq!(h.sample_rate, rate); + assert_eq!(h.acmod, mode as u8); + assert_eq!(h.lfeon, lfe); + assert_eq!(h.frame_bytes, packet.data.len()); + assert_eq!(bitstream::crc(&packet.data[2..]), 0); + let decoded = super::super::Decoder::new().decode(&packet.data)?; + assert!(decoded.channels.iter().flatten().all(|&v| v == 0.0)); + if codec == Codec::Ac3 { + let words = packet.data.len() / 2; + assert_eq!(bitstream::crc(&packet.data[2..(words / 2 + words / 8) * 2]), 0); + } + } + } + } + } + } + Ok(()) +} + +#[test] +fn input_validation_and_terminal_lifecycle() -> Result<(), Box> { + let cfg = config(Codec::Ac3, CodingMode::Stereo, false); + let mut encoder = Encoder::new(cfg)?; + assert!(encoder.push(&[&[0.0]]).is_err()); + assert!(encoder.push(&[&[0.0], &[]]).is_err()); + for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 1.01, -1.01] { + assert!(encoder.push(&[&[value], &[0.0]]).is_err()); + } + assert_eq!(encoder.input_samples, 0); + assert!(encoder.push(&[&[], &[]])?.is_empty()); + let finish = encoder.finish()?; + assert!(finish.packets.is_empty()); + assert_eq!(finish.priming_samples, 0); + assert_eq!(finish.padding_samples, 0); + assert_eq!(encoder.push(&[&[], &[]]).err(), Some(Error::Finished)); + assert_eq!(encoder.finish().err(), Some(Error::Finished)); + // Force an internal impossible budget to exercise terminal handling without + // relying on a public configuration that new() should have rejected. + let mut failing = Encoder::new(cfg)?; + failing.config.bitrate_kbps = 1; + assert_eq!(failing.push(&[&[0.0; 1536], &[0.0; 1536]]).err(), Some(Error::BitBudget)); + assert_eq!(failing.push(&[&[], &[]]).err(), Some(Error::Finished)); + assert_eq!(failing.finish().err(), Some(Error::Finished)); + let mut overflow = Encoder::new(cfg)?; + overflow.input_samples = u64::MAX - 2000; + assert!(overflow.push(&[&[0.0; 1000], &[0.0; 1000]]).is_err()); + assert_eq!(overflow.input_samples, u64::MAX - 2000); + Ok(()) +} + +#[test] +fn chunk_boundaries_and_exact_trim_counts() -> Result<(), Box> { + for codec in [Codec::Ac3, Codec::Eac3] { + for length in [1, 255, 256, 1280, 1535, 1536, 1537, 3071] { + let cfg = config(codec, CodingMode::Stereo, false); + let input: Vec = (0..length).map(|n| (n as f32 * 0.071).sin() * 0.2).collect(); + let mut contiguous = Encoder::new(cfg)?; + let mut whole = contiguous.push(&[&input, &input])?; + let finish = contiguous.finish()?; + whole.extend(finish.packets); + assert_eq!(whole.len() as u64 * 1536, length as u64 + u64::from(finish.priming_samples) + u64::from(finish.padding_samples)); + let mut chunked = Encoder::new(cfg)?; + let mut pieces = Vec::new(); + for chunk in input.chunks(37) { + pieces.extend(chunked.push(&[chunk, chunk])?); + } + let end = chunked.finish()?; + pieces.extend(end.packets); + assert_eq!(whole.iter().map(|p| &p.data).collect::>(), pieces.iter().map(|p| &p.data).collect::>()); + assert_eq!(finish.padding_samples, end.padding_samples); + } + } + Ok(()) +} + +#[test] +fn configuration_validates_real_frame_bounds_and_explicit_layout() { + for rate in [32000, 44100] { + assert!(Encoder::new(Config { codec: Codec::Eac3, sample_rate: rate, bitrate_kbps: 1024, mode: CodingMode::Stereo, lfe: false }).is_err()); + } + assert!(Encoder::new(Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 1024, mode: CodingMode::Stereo, lfe: false }).is_ok()); + assert!(Encoder::new(config(Codec::Ac3, CodingMode::Surround71, true)).is_err()); + assert!(Encoder::new(config(Codec::Eac3, CodingMode::Surround71, false)).is_err()); + assert_eq!(config(Codec::Eac3, CodingMode::Surround71, true).channel_layout(), ChannelLayout::SURROUND_7_1); + assert!(!config(Codec::Ac3, CodingMode::DualMono, false).channel_layout().is_ordered()); +} + +#[test] +fn dec3_describes_dependent_rear_pair_and_aggregate_rate() -> Result<(), Box> { + // Independently verified against a Dolby-hosted 48k/768k 7.1 sample's + // six-byte dec3 descriptor and ETSI Annex F; media is not redistributed. + let encoder = Encoder::new(Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 768, mode: CodingMode::Surround71, lfe: true })?; + assert_eq!(encoder.codec_configuration(), [0x18, 0x00, 0x20, 0x0f, 0x02, 0x02]); + for rate in [32000, 44100, 48000] { + let cfg = Config { codec: Codec::Eac3, sample_rate: rate, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true }; + let mut encoder = Encoder::new(cfg)?; + let meta = encoder.codec_configuration(); + let declared = (u32::from(meta[0]) << 5) | (u32::from(meta[1]) >> 3); + let pcm = vec![vec![0.0; 3072]; 8]; + for packet in encoder.push(&pcm.iter().map(Vec::as_slice).collect::>())? { + let actual = (packet.data.len() as u64 * 8 * u64::from(rate)).div_ceil(1_536_000); + assert!(u64::from(declared) >= actual); + let first = super::super::parse_header(&packet.data)?; + let second = super::super::parse_header(&packet.data[first.frame_bytes..])?; + assert_eq!((first.strmtyp, first.acmod, first.lfeon), (0, 7, true)); + assert_eq!((second.strmtyp, second.acmod, second.lfeon), (1, 6, false)); + assert_eq!(first.frame_bytes + second.frame_bytes, packet.data.len()); + } + } + Ok(()) +} + +#[test] +fn actual_decoder_positions_satisfy_all_three_ac3_bit_budgets() -> Result<(), Box> { + for rate in [32000, 44100, 48000] { + for bitrate in super::super::BITRATES { + for mode in [CodingMode::Mono, CodingMode::ThreeFrontTwoSurround] { + let cfg = + Config { codec: Codec::Ac3, sample_rate: rate, bitrate_kbps: bitrate, mode, lfe: mode == CodingMode::ThreeFrontTwoSurround }; + if cfg.validate().is_err() { + continue; + } + for stimulus in 0..3 { + let mut seed = 17u32; + let input: Vec> = (0..cfg.channels()) + .map(|_| { + (0..1536) + .map(|n| { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + match stimulus { + 0 => 0.0, + 1 => { + if n == 513 { + 0.8 + } else { + 0.0 + } + } + _ => ((seed >> 8) as f32 / 16777216.0 - 0.5) * 0.9, + } + }) + .collect() + }) + .collect(); + let mut encoder = Encoder::new(cfg)?; + for packet in encoder.push(&input.iter().map(Vec::as_slice).collect::>())? { + let mut decoder = super::super::Decoder::new(); + decoder.decode(&packet.data)?; + assert_eq!(decoder.block_bit_positions.len(), 6); + let words = packet.data.len() / 2; + let split = (words / 2 + words / 8) * 16; + assert!(decoder.block_bit_positions[1].1 <= split, "first5/8: {cfg:?} stimulus{stimulus}"); + assert!(decoder.block_bit_positions[5].0 >= split, "final3/8: {cfg:?} stimulus{stimulus}"); + assert!(decoder.block_bit_positions[5].1 + 18 <= words * 16, "wholeframe: {cfg:?} stimulus{stimulus}"); + assert_eq!(bitstream::crc(&packet.data[2..split / 8]), 0); + assert_eq!(bitstream::crc(&packet.data[2..]), 0); + } + } + } + } + } + Ok(()) +} + +#[test] +fn lfe_final_nonzero_input_is_chunk_invariant() -> Result<(), Box> { + let cfg = config(Codec::Eac3, CodingMode::ThreeFrontTwoSurround, true); + let mut input = vec![vec![0.0; 1537]; 6]; + input[3][1536] = 0.75; + let mut one = Encoder::new(cfg)?; + let mut a = one.push(&input.iter().map(Vec::as_slice).collect::>())?; + a.extend(one.finish()?.packets); + let mut two = Encoder::new(cfg)?; + let mut b = Vec::new(); + for range in [0..1, 1..255, 255..1536, 1536..1537] { + b.extend(two.push(&input.iter().map(|p| &p[range.clone()]).collect::>())?); + } + b.extend(two.finish()?.packets); + assert_eq!(a.iter().map(|p| &p.data).collect::>(), b.iter().map(|p| &p.data).collect::>()); + Ok(()) +} + +#[test] +fn transient_adjacent_tone_refreshes_exponents_within_the_frame() -> Result<(), Box> { + // Independent QC localized a previous shared-six-block exponent envelope's + // degradation to the first 32 ms after this onset. Preserve that regression. + let rate = 32000u32; + let input: Vec = (0..rate) + .map(|n| { + if n == 3200 || n == 1535 { + 0.85 + } else if (4800..11200).contains(&n) { + (std::f32::consts::TAU * 301.0 * n as f32 / rate as f32).sin() * 0.4 + } else { + 0.0 + } + }) + .collect(); + let mut encoder = Encoder::new(Config { codec: Codec::Ac3, sample_rate: rate, bitrate_kbps: 96, mode: CodingMode::Mono, lfe: false })?; + let mut packets = encoder.push(&[&input])?; + packets.extend(encoder.finish()?.packets); + let mut decoder = super::super::Decoder::new(); + let mut decoded = Vec::new(); + for packet in packets { + decoded.extend(decoder.decode(&packet.data)?.channels.remove(0)); + } + let mut signal = 0.0f64; + let mut error = 0.0f64; + for n in 5440..10560 { + signal += f64::from(input[n]).powi(2); + error += f64::from(input[n] - decoded[n + 256]).powi(2); + } + let snr = 10.0 * (signal / error).log10(); + assert!(snr > 55.0, "transient-adjacent tone SNR {snr} dB"); + Ok(()) +} + +#[test] +fn surround_compatibility_downmix_retains_correlated_signal_headroom() -> Result<(), Box> { + let cfg = Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true }; + let mut pcm = vec![vec![0.0; 3072]; 8]; + for ch in [4, 5, 6, 7] { + for n in 0..3072 { + pcm[ch][n] = (n as f32 * std::f32::consts::TAU * 440.0 / 48000.0).sin() * 0.99; + } + } + let mut encoder = Encoder::new(cfg)?; + let mut packets = encoder.push(&pcm.iter().map(Vec::as_slice).collect::>())?; + packets.extend(encoder.finish()?.packets); + let mut decoder = super::super::Decoder::new(); + let mut left = Vec::new(); + for packet in packets { + let h = super::super::parse_header(&packet.data)?; + let frame = decoder.decode(&packet.data[..h.frame_bytes])?; + left.extend_from_slice(&frame.channels[4]); + } + let mse = (256..3072).map(|n| (left[n + 256] - pcm[6][n]).powi(2)).sum::() / (3072 - 256) as f32; + assert!(mse < 0.0002, "compatibility downmix mse={mse}"); + assert!(left.iter().all(|v| v.abs() < 1.05)); + Ok(()) +} + +#[test] +fn encoder_errors_convert_to_audio_errors() { + let bad = Config { codec: Codec::Ac3, sample_rate: 22_050, bitrate_kbps: 192, mode: CodingMode::Stereo, lfe: false }; + let made: crate::Result = Encoder::new(bad).map_err(Into::into); + assert!(matches!(made, Err(crate::AudioError::Encode(m)) if m.contains("sample rate"))); +} diff --git a/crates/audio/src/ac3/encoder/transform.rs b/crates/audio/src/ac3/encoder/transform.rs new file mode 100644 index 000000000..8c3f2299f --- /dev/null +++ b/crates/audio/src/ac3/encoder/transform.rs @@ -0,0 +1,97 @@ +//! Long-block analysis transform, ETSI TS 102 366 §7.2.3. +use std::sync::OnceLock; + +/// Analysis matrix includes the window and −2/N normalization (N=512). +/// Cached rather than evaluating trigonometric functions for each audio block. +fn matrix() -> &'static Vec<[f32; 512]> { + static MATRIX: OnceLock> = OnceLock::new(); + MATRIX.get_or_init(|| { + let window = super::super::window(); + (0..256) + .map(|k| { + std::array::from_fn(|n| { + let w = if n < 256 { window[n] } else { window[511 - n] }; + let phase = std::f64::consts::PI / 1024.0 * ((2 * n + 1) * (2 * k + 1)) as f64 + std::f64::consts::PI / 4.0 * (2 * k + 1) as f64; + (-phase.cos() * f64::from(w) / 256.0) as f32 + }) + }) + .collect() + }) +} + +pub(super) fn analysis_long(input: &[f32; 512]) -> [f32; 256] { + std::array::from_fn(|k| matrix()[k].iter().zip(input).map(|(&a, &b)| f64::from(a) * f64::from(b)).sum::() as f32) +} + +/// Six-pole Butterworth low-pass at 120 Hz, before LFE analysis/downsampling. +/// Cascaded second-order sections retain stability and streaming continuity. +pub(super) struct LowPass { + sections: [Biquad; 3], +} +struct Biquad { + b: [f64; 3], + a: [f64; 2], + z: [f64; 2], +} +impl LowPass { + pub(super) fn new(rate: u32) -> Self { + let omega = std::f64::consts::TAU * 120.0 / f64::from(rate); + Self { + sections: [0.5176380902050415, std::f64::consts::FRAC_1_SQRT_2, 1.9318516525781366].map(|q| { + let alpha = omega.sin() / (2.0 * q); + let a0 = 1.0 + alpha; + let b0 = (1.0 - omega.cos()) * 0.5 / a0; + Biquad { b: [b0, 2.0 * b0, b0], a: [-2.0 * omega.cos() / a0, (1.0 - alpha) / a0], z: [0.0; 2] } + }), + } + } + pub(super) fn process(&mut self, input: f32) -> f32 { + let mut value = f64::from(input); + for section in &mut self.sections { + let output = section.b[0] * value + section.z[0]; + section.z[0] = section.b[1] * value - section.a[0] * output + section.z[1]; + section.z[1] = section.b[2] * value - section.a[1] * output; + value = output; + } + value as f32 + } +} + +/// Short transforms use the same 512-sample window but two 256-point +/// cosine transforms with alpha = -1, +1, interleaved in the bitstream. +fn short_matrix() -> &'static Vec<[f32; 256]> { + static MATRIX: OnceLock> = OnceLock::new(); + MATRIX.get_or_init(|| { + let window = super::super::window(); + (0..256) + .map(|index| { + let half = index % 2; + let k = index / 2; + std::array::from_fn(|n| { + let w = if half == 0 { window[n] } else { window[255 - n] }; + let phase = std::f64::consts::PI / 512.0 * ((2 * n + 1) * (2 * k + 1)) as f64 + + std::f64::consts::PI / 2.0 * (2 * k + 1) as f64 * half as f64; + (-phase.cos() * f64::from(w) / 128.0) as f32 + }) + }) + .collect() + }) +} + +pub(super) fn analysis(input: &[f32; 512], short: bool) -> [f32; 256] { + if !short { + return analysis_long(input); + } + std::array::from_fn(|k| { + let offset = (k % 2) * 256; + short_matrix()[k].iter().zip(&input[offset..offset + 256]).map(|(&a, &b)| f64::from(a) * f64::from(b)).sum::() as f32 + }) +} + +/// Detect a substantial onset within the analysis window. Decisions are shared +/// by full-bandwidth channels so a compatible surround downmix can be formed in +/// the transform domain without mixing coefficients from different transforms. +pub(super) fn has_transient(input: &[f32; 512]) -> bool { + let energy: Vec = input.as_chunks::<32>().0.iter().map(|chunk| chunk.iter().map(|&v| f64::from(v).powi(2)).sum::()).collect(); + energy.windows(2).any(|e| e[1] > 0.001 && e[1] > (e[0] + 1e-8) * 8.0) +} diff --git a/crates/audio/src/ac3/mod.rs b/crates/audio/src/ac3/mod.rs index 708c5cca7..e04142b83 100644 --- a/crates/audio/src/ac3/mod.rs +++ b/crates/audio/src/ac3/mod.rs @@ -1,4 +1,5 @@ -//! Clean-room AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) audio decoder, written from +//! Specification-derived AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) codec tools. +//! The syncframe decoder is written from //! ATSC A/52:2012 ("Digital Audio Compression Standard", §5 bit stream syntax, §6-7 decoding, //! Annex E for E-AC-3). //! @@ -12,12 +13,18 @@ //! - 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 +//! [`Decoder`] handles 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. //! +//! [`ProgrammeDecoder`] separately assembles complete six-block E-AC-3 access units, including +//! a supported discrete 7.1 dependent-stream topology, with an explicit expected speaker layout +//! and full-range output. It retains the syncframe decoder's unsupported-tool limits. +//! The optional encoder follows ETSI TS 102 366 V1.4.1; see the crate README for supported +//! encoding tools and the distinction between raw-stream padding and container presentation. +//! //! From FilmCraft's `filmcraft-ac3` (storytold/filmcraft@8fda2f2). Public items keep their names, so //! FilmCraft can re-export this module. @@ -25,6 +32,10 @@ #![allow(clippy::needless_range_loop)] mod eac3; +mod program; +pub use program::{ProgrammeDecoder, ProgrammeFrame}; +#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] +pub mod encoder; mod tables; use std::sync::OnceLock; @@ -685,6 +696,11 @@ pub struct Decoder { delay: [[f32; 256]; 6], dither: u32, seed: u32, + /// Apply the legacy per-syncframe dynamic-range metadata policy. + apply_dynamic_range: bool, + /// Actual parser positions, used to validate encoder allocation-region constraints. + #[cfg(test)] + block_bit_positions: Vec<(usize, usize)>, /// 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)>, @@ -714,7 +730,15 @@ pub struct Frame { 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 } + Decoder { + delay: [[0.0; 256]; 6], + dither: 0, + seed: 0x1234_5678, + apply_dynamic_range: true, + carry: None, + #[cfg(test)] + block_bit_positions: Vec::new(), + } } /// Seed the dither generator (dither is decoder-specific noise; tests compare two seeds). @@ -726,6 +750,8 @@ impl Decoder { pub fn reset(&mut self) { self.delay = [[0.0; 256]; 6]; self.carry = None; + #[cfg(test)] + self.block_bit_positions.clear(); } fn dither(&mut self) -> f32 { @@ -802,6 +828,11 @@ impl Decoder { if !h.is_primary() { return Ok(Frame { header: h, channels: Vec::new() }); } + self.decode_eac3_substream(frame, h) + } + + // The programme assembler validates the dependent topology before calling this helper. + fn decode_eac3_substream(&mut self, frame: &[u8], h: Header) -> Result { // 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(); @@ -817,7 +848,10 @@ impl Decoder { }; 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) }) + // A dependent channel map assigns positions in coded order, overriding acmod's + // conventional speaker order. The programme assembler maps those channels once. + let channels = if h.strmtyp == 1 { pcm } else { wav_order(pcm, acmod, lfeon) }; + Ok(Frame { header: h, channels }) } /// Decode `nblocks` audio blocks and synthesize them (inverse transform, overlap-add), in coded @@ -1241,6 +1275,8 @@ impl Decoder { } } // mantissas + #[cfg(test)] + let mantissa_start = r.pos; let mut gr = Groups::default(); let mut cpl = [0f32; 256]; let mut got_cpl = false; @@ -1266,6 +1302,8 @@ impl Decoder { coefs[LFE][bin] = m * 2f32.powi(-(f.exps[LFE][bin] as i32)); } } + #[cfg(test)] + self.block_bit_positions.push((mantissa_start, r.pos)); // decoupling if f.cplinu { for ch in 0..nf { @@ -1307,6 +1345,11 @@ impl Decoder { } } } + // Programme assembly requests full-range PCM rather than applying the independent + // downmix's DRC to channels that will subsequently be replaced by a dependent stream. + if !self.apply_dynamic_range { + return Ok(()); + } // dynamic range for ch in 0..nf { let g = if acmod == 0 && ch == 1 { f.dynrng[1] } else { f.dynrng[0] }; diff --git a/crates/audio/src/ac3/program.rs b/crates/audio/src/ac3/program.rs new file mode 100644 index 000000000..89dacd53b --- /dev/null +++ b/crates/audio/src/ac3/program.rs @@ -0,0 +1,309 @@ +//! Complete six-block E-AC-3 programme access units, including the conventional 7.1 topology. +//! +//! The existing syncframe decoder deliberately ignores dependent substreams. This API instead +//! accepts one complete 1536-sample access unit: independent programme zero, optionally followed +//! by its four-channel dependent replacement/extension. It returns full-range PCM (no dialnorm, +//! RF compression or dynamic-range metadata gain), so an independent downmix's gain is never +//! incorrectly applied to the assembled programme. It does not claim support for every E-AC-3 +//! dependent channel map or shorter-frame grouping. + +use super::{Bits, Decoder, Error, Header, Result, eac3, parse_header}; +use crate::ChannelLayout; + +/// Decoded complete programme, in WAVE speaker-mask order. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ProgrammeFrame { + /// Samples per second, shared by every substream. + pub sample_rate: u32, + /// Speaker assignments; dual mono remains unordered. + pub layout: ChannelLayout, + /// Exactly 1536 samples in each channel. + pub channels: Vec>, +} + +/// Stateful decoder for complete six-block E-AC-3 programme-zero access units. +/// +/// For 7.1 the independent 3/2+LFE downmix is followed by one four-channel dependent stream whose +/// explicit map is Ls, Rs, Lrs, Rrs (0x1A00). The first pair replaces the independent surrounds; +/// the second pair adds rear speakers. Output order is L, R, C, LFE, Lrs, Rrs, Ls, Rs. +/// Unsupported topologies fail explicitly. After any error all overlap state is reset. +pub struct ProgrammeDecoder { + independent: Decoder, + dependent: Decoder, + topology: Option<(u32, u8, bool, bool)>, + expected_layout: ChannelLayout, +} + +impl ProgrammeDecoder { + /// Start with empty overlap state and full-range output. + /// + /// The expected layout comes from the container or the caller's programme description. + /// Requiring it prevents a missing dependent frame from silently downgrading 7.1 to 5.1. + pub fn new(expected_layout: ChannelLayout) -> Self { + let mut independent = Decoder::new(); + independent.apply_dynamic_range = false; + let mut dependent = Decoder::new(); + dependent.apply_dynamic_range = false; + Self { independent, dependent, topology: None, expected_layout } + } + + /// Clear the programme's overlap state before seeking or switching streams. + pub fn reset(&mut self) { + self.independent.reset(); + self.dependent.reset(); + self.topology = None; + } + + /// Decode one complete access unit, never publishing a partial programme. + /// + /// Input is bounded to two syncframes of at most 4096 bytes each. Both CRCs, frame bounds, + /// programme IDs, rates, block counts and the dependent channel map are checked first. + pub fn decode(&mut self, access_unit: &[u8]) -> Result { + let result = self.decode_inner(access_unit); + if result.is_err() { + self.reset(); + } + result + } + + fn decode_inner(&mut self, au: &[u8]) -> Result { + let (primary, dependent) = validate(au)?; + let layout = if dependent.is_some() { ChannelLayout::SURROUND_7_1 } else { primary_layout(primary) }; + if layout != self.expected_layout { + return Err(Error::Invalid("programme does not match the expected speaker layout")); + } + let topology = (primary.sample_rate, primary.acmod, primary.lfeon, dependent.is_some()); + if self.topology != Some(topology) { + self.reset(); + } + let mut channels = self.independent.decode_eac3_substream(&au[..primary.frame_bytes], primary)?.channels; + if let Some(header) = dependent { + let more = self.dependent.decode_eac3_substream(&au[primary.frame_bytes..], header)?.channels; + channels = assemble_7_1(channels, more)?; + } + self.topology = Some(topology); + Ok(ProgrammeFrame { sample_rate: primary.sample_rate, layout, channels }) + } +} + +fn primary_layout(h: Header) -> ChannelLayout { + let mask = match h.acmod { + 0 => return ChannelLayout::unordered(if h.lfeon { 3 } else { 2 }), + 1 => 0x4, + 2 => 0x3, + 3 => 0x7, + 4 => 0x103, + 5 => 0x107, + 6 => 0x603, + _ => 0x607, + }; + ChannelLayout::from_mask(mask | if h.lfeon { 8 } else { 0 }) +} + +fn validate(au: &[u8]) -> Result<(Header, Option
)> { + if au.len() > 8192 { + return Err(Error::Invalid("programme access unit exceeds two syncframes")); + } + let first = checked_frame(au)?; + if first.strmtyp != 0 || first.substreamid != 0 { + return Err(Error::Unsupported("programme must start with independent substream zero".into())); + } + if first.frame_bytes == au.len() { + return Ok((first, None)); + } + let remainder = &au[first.frame_bytes..]; + let second = checked_frame(remainder)?; + if second.frame_bytes != remainder.len() { + return Err(Error::Unsupported("more than one dependent substream".into())); + } + if second.strmtyp != 1 || second.substreamid != 0 { + return Err(Error::Invalid("expected dependent substream zero")); + } + if second.sample_rate != first.sample_rate || second.blocks != first.blocks { + return Err(Error::Invalid("dependent rate or block count differs from independent")); + } + if first.acmod != 7 || !first.lfeon || !matches!(second.acmod, 5 | 6) || second.lfeon { + return Err(Error::Unsupported("dependent topology is not the supported 7.1 programme".into())); + } + let mut bits = Bits::new(remainder); + let bsi = eac3::read_bsi(&mut bits)?; + if bits.overrun() { + return Err(Error::Invalid("truncated dependent bit stream information")); + } + if !bsi.compre { + return Err(Error::Invalid("last dependent substream must signal compre")); + } + if bsi.chanmap != Some(0x1A00) { + return Err(Error::Unsupported("dependent channel map must be Ls Rs Lrs Rrs".into())); + } + Ok((first, Some(second))) +} + +fn checked_frame(bytes: &[u8]) -> Result
{ + let h = parse_header(bytes)?; + if !h.is_eac3() || h.bsid != 16 || h.blocks != 6 || !matches!(h.sample_rate, 32_000 | 44_100 | 48_000) { + return Err(Error::Unsupported("programme decoder requires full-rate six-block E-AC-3 bsid 16".into())); + } + let frame = bytes.get(..h.frame_bytes).ok_or(Error::Invalid("truncated programme syncframe"))?; + if crc16(&frame[2..]) != 0 { + return Err(Error::Invalid("programme syncframe CRC mismatch")); + } + Ok(h) +} + +// ETSI TS 102 366 E.1.6: polynomial x^16+x^15+x^2+1; callers exclude only the syncword. +fn crc16(bytes: &[u8]) -> u16 { + let mut crc = 0u16; + for &byte in bytes { + crc ^= u16::from(byte) << 8; + for _ in 0..8 { + crc = if crc & 0x8000 != 0 { (crc << 1) ^ 0x8005 } else { crc << 1 }; + } + } + crc +} + +fn assemble_7_1(mut primary: Vec>, mut dependent: Vec>) -> Result>> { + if primary.len() != 6 || dependent.len() != 4 || primary.iter().chain(&dependent).any(|c| c.len() != 1536) { + return Err(Error::Invalid("decoded programme dimensions differ from signalled channels")); + } + // Keep L R C LFE; discard downmixed Ls/Rs. Dependent coded order is Ls Rs Lrs Rrs. + primary.truncate(4); + let rear = dependent.split_off(2); + primary.extend(rear); + primary.extend(dependent); + Ok(primary) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Original independent-spec oracle envelopes: valid BSI/CRC, deliberately no audio blocks. + const PRIMARY: [u8; 32] = [ + 0x0b, 0x77, 0x00, 0x0f, 0x3f, 0x87, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x87, + ]; + const DEPENDENT: [u8; 32] = [ + 0x0b, 0x77, 0x40, 0x0f, 0x3c, 0x87, 0xe0, 0x11, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x93, + ]; + + #[test] + fn seven_one_rejects_valid_independent_prefix_before_audio_decode() -> Result<()> { + assert!(validate(&PRIMARY)?.1.is_none()); + let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1); + assert_eq!(decoder.decode(&PRIMARY).err(), Some(Error::Invalid("programme does not match the expected speaker layout"))); + assert!(decoder.topology.is_none()); + Ok(()) + } + + #[test] + fn validates_dependency_order_frame_bounds_and_crc() -> Result<()> { + let mut au = PRIMARY.to_vec(); + au.extend_from_slice(&DEPENDENT); + assert!(validate(&au)?.1.is_some()); + assert!(validate(&DEPENDENT).is_err()); + assert!(validate(&au[..63]).is_err()); + au.push(0); + assert!(validate(&au).is_err()); + au.pop(); + au[55] ^= 1; + assert!(validate(&au).is_err()); + let mut duplicate = PRIMARY.to_vec(); + duplicate.extend_from_slice(&PRIMARY); + assert!(validate(&duplicate).is_err()); + Ok(()) + } + + fn changed_dependent(bit: usize, width: usize, value: u16) -> Vec { + let mut frame = DEPENDENT; + for i in 0..width { + let at = bit + i; + let mask = 1 << (7 - at % 8); + frame[at / 8] = (frame[at / 8] & !mask) | if value & (1 << (width - 1 - i)) != 0 { mask } else { 0 }; + } + let crc = crc16(&frame[2..30]); + frame[30..].copy_from_slice(&crc.to_be_bytes()); + let mut au = PRIMARY.to_vec(); + au.extend_from_slice(&frame); + au + } + + #[test] + fn rejects_crc_valid_but_inconsistent_programme_descriptions() { + // Fields are normative bit offsets in the independent structural BSI fixture. + let wrong_map = changed_dependent(60, 16, 0x1800); + assert_eq!(validate(&wrong_map).err(), Some(Error::Unsupported("dependent channel map must be Ls Rs Lrs Rrs".into()))); + let missing_last_marker = changed_dependent(50, 1, 0); + assert_eq!(validate(&missing_last_marker).err(), Some(Error::Invalid("last dependent substream must signal compre"))); + let wrong_rate = changed_dependent(32, 2, 2); + assert_eq!(validate(&wrong_rate).err(), Some(Error::Invalid("dependent rate or block count differs from independent"))); + let short_frame = changed_dependent(34, 2, 0); + assert_eq!(validate(&short_frame).err(), Some(Error::Unsupported("programme decoder requires full-rate six-block E-AC-3 bsid 16".into()))); + let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1); + decoder.reset(); + assert_eq!(decoder.expected_layout, ChannelLayout::SURROUND_7_1); + } + + #[cfg(feature = "eac3-encode")] + #[test] + fn explicit_map_overrides_dependent_acmod_speaker_order() -> std::result::Result<(), Box> { + use super::super::encoder::{Codec, CodingMode, Config, Encoder}; + let config = Config { codec: Codec::Eac3, mode: CodingMode::Surround71, lfe: true, sample_rate: 48_000, bitrate_kbps: 640 }; + let mut encoder = Encoder::new(config)?; + let source: Vec> = (0..8).map(|c| (0..1536).map(|n| ((n as f32) * (0.01 + c as f32 * 0.017)).sin() * 0.2).collect()).collect(); + let packets = encoder.push(&source.iter().map(Vec::as_slice).collect::>())?; + let packet = packets.first().ok_or("encoder did not emit the complete frame")?; + let expected = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&packet.data)?; + let mut alternate = packet.data.clone(); + let offset = parse_header(&alternate)?.frame_bytes; + let dependent = &mut alternate[offset..]; + // Both acmod5 and6 code four FBW channels. The explicit map overrides their default + // positions, so changing only acmod (with no coupling/rematrix/mix metadata) is valid. + dependent[4] = (dependent[4] & 0xf1) | (5 << 1); + let end = dependent.len() - 2; + let crc = crc16(&dependent[2..end]); + dependent[end..].copy_from_slice(&crc.to_be_bytes()); + let actual = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&alternate)?; + assert_eq!(expected.layout, actual.layout); + assert_eq!(expected.channels, actual.channels); + Ok(()) + } + + #[test] + fn corrupt_envelopes_and_truncations_never_panic_or_publish_partial_pcm() { + let mut original = PRIMARY.to_vec(); + original.extend_from_slice(&DEPENDENT); + for length in 0..original.len() { + let result = std::panic::catch_unwind(|| ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&original[..length])); + assert!(matches!(result, Ok(Err(_))), "truncated length {length}"); + } + for bit in 0..original.len() * 8 { + let mut corrupt = original.clone(); + corrupt[bit / 8] ^= 1 << (bit % 8); + let result = std::panic::catch_unwind(|| ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&corrupt)); + assert!(matches!(result, Ok(Err(_))), "corrupt bit {bit}"); + } + } + + #[test] + fn dependent_replaces_downmix_and_labels_rears() -> Result<()> { + let primary = (0..6).map(|c| vec![c as f32; 1536]).collect(); + let dependent = (10..14).map(|c| vec![c as f32; 1536]).collect(); + let result = assemble_7_1(primary, dependent)?; + assert_eq!(result.iter().map(|c| c[0]).collect::>(), [0., 1., 2., 3., 12., 13., 10., 11.]); + assert_eq!(ChannelLayout::SURROUND_7_1.mask(), Some(0x63f)); + Ok(()) + } + + #[test] + fn incomplete_programme_never_produces_partial_channels() { + assert!(assemble_7_1(vec![vec![0.; 1536]; 6], vec![vec![0.; 1535]; 4]).is_err()); + let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1); + decoder.topology = Some((48_000, 7, true, true)); + assert!(decoder.decode(&[0; 8193]).is_err()); + assert!(decoder.topology.is_none()); + } +} diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index f5bf0a100..4285ca105 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -28,7 +28,7 @@ #[cfg(any(feature = "aac-decode", feature = "aac-encode"))] pub mod aac; -#[cfg(feature = "ac3-decode")] +#[cfg(any(feature = "ac3-decode", feature = "ac3-encode", feature = "eac3-encode"))] pub mod ac3; #[cfg(any(feature = "alac-decode", feature = "alac-encode"))] pub mod alac; @@ -128,6 +128,13 @@ impl From for AudioError { } } +#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] +impl From for AudioError { + fn from(e: ac3::encoder::Error) -> Self { + AudioError::Encode(e.to_string()) + } +} + #[cfg(feature = "mpa-decode")] impl From for AudioError { fn from(e: mpa::Error) -> Self { diff --git a/crates/audio/tests/codecs/ac3_encoder_oracle.rs b/crates/audio/tests/codecs/ac3_encoder_oracle.rs new file mode 100644 index 000000000..77607aaf8 --- /dev/null +++ b/crates/audio/tests/codecs/ac3_encoder_oracle.rs @@ -0,0 +1,180 @@ +//! Original synthetic PCM encoded here and decoded by an external FFmpeg executable only. +//! Covers every public acmod/LFE combination at all three rates, plus genuine dependent-stream +//! 7.1. These are framing/routing/level regressions, not a perceptual quality certification. +//! Uses the shared oracle discovery rules; set CRAFT_MEDIA_REQUIRE_ORACLES=1 to require FFmpeg. + +use std::f64::consts::TAU; +use std::path::Path; +use std::process::Command; + +use craft_audio::ac3::encoder::{Codec, CodingMode, Config, Encoder, Packet}; + +const INPUT_SAMPLES: usize = 4097; +const ANALYSIS_START: usize = 1024; +const ANALYSIS_SAMPLES: usize = 2048; +const AMPLITUDE: f64 = 0.2; + +/// Bit-by-bit polynomial division, independent of the encoder's CRC implementation. +fn crc16(bytes: &[u8]) -> u16 { + bytes.iter().fold(0u16, |mut remainder, &byte| { + for bit in (0..8).rev() { + let feedback = ((remainder >> 15) ^ u16::from((byte >> bit) & 1)) != 0; + remainder <<= 1; + if feedback { + remainder ^= 0x8005; + } + } + remainder + }) +} + +fn lfe_index(config: Config) -> Option { + config.lfe.then_some(match config.mode { + CodingMode::Mono => 1, + CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround | CodingMode::Surround71 => 3, + _ => 2, + }) +} + +fn run_case(ffmpeg: &Path, config: Config) { + let label = format!("{:?}/{:?}/lfe={}/{}Hz", config.codec, config.mode, config.lfe, config.sample_rate); + let channels = config.channels(); + // Integer cycles in the analysis window make the channel probes mutually orthogonal. + // LFE stays below 47 Hz at all rates, avoiding the encoder's intentional 120 Hz roll-off. + let bins: Vec = (0..channels).map(|ch| if Some(ch) == lfe_index(config) { 2 } else { [29, 43, 61, 79, 101, 127, 151, 173][ch] }).collect(); + let source: Vec> = bins + .iter() + .map(|&bin| { + (0..INPUT_SAMPLES) + .map(|i| (AMPLITUDE * (i as f64 / 256.0).min(1.0) * (TAU * bin as f64 * i as f64 / ANALYSIS_SAMPLES as f64).sin()) as f32) + .collect() + }) + .collect(); + let mut encoder = Encoder::new(config).unwrap_or_else(|e| panic!("{label}: construct: {e}")); + let mut packets = encoder.push(&source.iter().map(Vec::as_slice).collect::>()).unwrap_or_else(|e| panic!("{label}: encode: {e}")); + let finish = encoder.finish().unwrap_or_else(|e| panic!("{label}: finish: {e}")); + assert_eq!(finish.priming_samples, 256, "{label}: priming"); + packets.extend(finish.packets); + let decoded_samples = packets.len() * 1536; + assert_eq!(decoded_samples, INPUT_SAMPLES + 256 + finish.padding_samples as usize, "{label}: trim accounting"); + let stream = check_packets(&packets, config, &label); + let pcm = external_decode(ffmpeg, config, &label, &stream, decoded_samples); + check_tones(&pcm, &bins, config, &label); +} + +fn check_packets(packets: &[Packet], config: Config, label: &str) -> Vec { + let mut stream = Vec::new(); + for (index, packet) in packets.iter().enumerate() { + assert_eq!(packet.samples, 1536, "{label}: packet {index} duration"); + let mut remaining = packet.data.as_slice(); + let mut frames = 0; + while !remaining.is_empty() { + let header = craft_audio::ac3::parse_header(remaining).unwrap_or_else(|e| panic!("{label}: packet {index} header: {e}")); + assert_eq!(header.sample_rate, config.sample_rate, "{label}: sample rate"); + assert_eq!(header.samples(), 1536, "{label}: block count"); + assert_eq!(header.is_eac3(), config.codec == Codec::Eac3, "{label}: family"); + assert_eq!(header.substreamid, 0, "{label}: programme id"); + assert_eq!(header.strmtyp, u8::from(frames != 0), "{label}: independent/dependent ordering"); + assert!(header.frame_bytes <= remaining.len(), "{label}: truncated frame"); + let frame = &remaining[..header.frame_bytes]; + assert_eq!(crc16(&frame[2..]), 0, "{label}: packet {index} frame {frames} CRC"); + if config.codec == Codec::Ac3 { + let boundary = (frame.len() / 2 / 2 + frame.len() / 2 / 8) * 2; + assert_eq!(crc16(&frame[2..boundary]), 0, "{label}: packet {index} CRC1"); + } + if config.mode == CodingMode::Surround71 { + assert_eq!(header.channels(), if frames == 0 { 6 } else { 4 }, "{label}: substream channel count"); + } else { + assert_eq!(header.acmod, config.mode as u8, "{label}: acmod"); + assert_eq!(header.lfeon, config.lfe, "{label}: LFE flag"); + } + remaining = &remaining[header.frame_bytes..]; + frames += 1; + } + assert_eq!(frames, if config.mode == CodingMode::Surround71 { 2 } else { 1 }, "{label}: complete access unit"); + stream.extend_from_slice(&packet.data); + } + + stream +} + +fn external_decode(ffmpeg: &Path, config: Config, label: &str, stream: &[u8], decoded_samples: usize) -> Vec { + let channels = config.channels(); + let format = if config.codec == Codec::Ac3 { "ac3" } else { "eac3" }; + let path = crate::testkit::fixtures_dir("ac3-encoder-oracle").join(format!( + "{}-{:?}-lfe{}-{}-{}.{}", + std::process::id(), + config.mode, + config.lfe, + config.sample_rate, + config.bitrate_kbps, + format + )); + std::fs::write(&path, stream).unwrap_or_else(|e| panic!("{label}: write fixture: {e}")); + // Explicit format matters for three-frame files, which can be too short for auto-probing. + // Do not request -ac or -ar: output must retain the bitstream's actual channel count/rate. + let result = Command::new(ffmpeg) + .args(["-nostdin", "-v", "error", "-xerror", "-err_detect", "explode", "-f", format, "-i"]) + .arg(&path) + .args(["-map", "0:a:0", "-c:a", "pcm_f32le", "-f", "f32le", "pipe:1"]) + .output() + .unwrap_or_else(|e| panic!("{label}: launch FFmpeg: {e}")); + assert!(result.status.success(), "{label}: FFmpeg failed: {}", String::from_utf8_lossy(&result.stderr)); + assert!(result.stderr.is_empty(), "{label}: FFmpeg reported: {}", String::from_utf8_lossy(&result.stderr)); + assert_eq!(result.stdout.len(), decoded_samples * channels * 4, "{label}: decoded channel/sample count"); + let pcm: Vec = result.stdout.as_chunks::<4>().0.iter().map(|bytes| f32::from_le_bytes(*bytes)).collect(); + assert!(pcm.iter().all(|v| v.is_finite()), "{label}: non-finite PCM"); + std::fs::remove_file(path).unwrap_or_else(|e| panic!("{label}: remove generated fixture: {e}")); + pcm +} + +fn check_tones(pcm: &[f32], bins: &[usize], config: Config, label: &str) { + let channels = config.channels(); + for channel in 0..channels { + for (probe_channel, &bin) in bins.iter().enumerate() { + let (mut sine, mut cosine) = (0.0, 0.0); + for i in ANALYSIS_START..ANALYSIS_START + ANALYSIS_SAMPLES { + let value = f64::from(pcm[(i + 256) * channels + channel]); + let angle = TAU * bin as f64 * i as f64 / ANALYSIS_SAMPLES as f64; + sine += value * angle.sin(); + cosine += value * angle.cos(); + } + // Quadrature measures LFE gain without mistaking the low-pass phase shift for loss. + let amplitude = 2.0 * sine.hypot(cosine) / ANALYSIS_SAMPLES as f64; + if channel == probe_channel { + assert!((0.18..=0.22).contains(&litude), "{label}: channel {channel} lost/rerouted/scaled: amplitude {amplitude}"); + if Some(channel) != lfe_index(config) { + assert!(sine > 0.0 && cosine.abs() < sine * 0.05, "{label}: channel {channel} polarity/priming alignment"); + } + } else { + assert!(amplitude < 0.01, "{label}: channel {channel} contains channel {probe_channel}: amplitude {amplitude}"); + } + } + } +} + +#[test] +fn ffmpeg_decodes_encoder_modes_rates_and_channels() { + let ffmpeg = require_ffmpeg!(); + let modes = [ + CodingMode::DualMono, + CodingMode::Mono, + CodingMode::Stereo, + CodingMode::ThreeFront, + CodingMode::StereoSurround, + CodingMode::ThreeFrontSurround, + CodingMode::StereoTwoSurround, + CodingMode::ThreeFrontTwoSurround, + ]; + for sample_rate in [32_000, 44_100, 48_000] { + for codec in [Codec::Ac3, Codec::Eac3] { + for mode in modes { + for lfe in [false, true] { + // Dual mono's channels are independent programmes 1 and 2, not L/R speakers. + run_case(&ffmpeg, Config { codec, sample_rate, bitrate_kbps: 448, mode, lfe }); + } + } + } + run_case(&ffmpeg, Config { codec: Codec::Eac3, sample_rate, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true }); + } +} diff --git a/crates/audio/tests/codecs/main.rs b/crates/audio/tests/codecs/main.rs index 910cf3516..030f3deb9 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_encoder_oracle; mod ac3_oracle; mod alac; mod concealment;