mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-15 03:36:53 -04:00
#647: decode E-AC-3 (Dolby Digital Plus) audio
Problem: files with E-AC-3 audio (MP4 ec-3, MOV, Matroska A_EAC3, MPEG-TS) imported silent: filmcraft-ac3 refused bsid 11-16. Fix: clean-room E-AC-3 decoding from ATSC A/52:2012 Annex E in filmcraft-ac3: bsi and audfrm syntax, 1/2/3/6-block syncframes, reduced sample rates, frame-based exponent strategies (exponents reused across short syncframes), SNR offset strategies, default band structures, spectral extension (translation, notch filter, noise blending). Independent substream 0 is decoded; dependent substreams and other programs are skipped (7.1 plays as its 5.1 core). AHT and enhanced coupling are refused as unsupported. The audio blocks share the AC-3 path; AC-3 output is unchanged. filmcraft-codecs routes E-AC-3 in MP4/MOV, Matroska and TS to the decoder; the TS splitter joins dependent substream frames to their access unit. Tests: ffmpeg oracle for raw E-AC-3 (64.8-91.0 dB SNR) and in MP4, MOV, Matroska and TS (79.5-91.3 dB, random access identical), mutation fuzz, synthetic frames for headers, substream skipping, refusals and exponent reuse across 1-block syncframes, spectral extension unit tests. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ed141c74d0
commit
be98b9d7f7
14 files changed
+1474
-124
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "filmcraft-ac3"
|
||||
description = "Clean-room AC-3 (Dolby Digital, ATSC A/52) audio decoder"
|
||||
description = "Clean-room AC-3 and E-AC-3 (Dolby Digital / Dolby Digital Plus, ATSC A/52) audio decoder"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
+42
-6
@@ -1,6 +1,6 @@
|
||||
# filmcraft-ac3
|
||||
|
||||
Clean-room AC-3 (Dolby Digital) audio decoder in pure Rust. Layer L0: depends only on
|
||||
Clean-room AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) audio decoder in pure Rust. Layer L0: depends only on
|
||||
`thiserror`. Builds for `wasm32-unknown-unknown`; no `unsafe`. No GPL/LGPL code (FFmpeg,
|
||||
liba52) was consulted; FFmpeg is used only as an external fixture generator and test oracle.
|
||||
|
||||
@@ -11,14 +11,17 @@ publicly available standard: §5 bit stream syntax and semantics, §6 decoding o
|
||||
exponents, §7.2 parametric bit allocation (Tables 7.6-7.16 extracted mechanically from the
|
||||
standard's text), §7.3 mantissas and dither, §7.4 coupling, §7.5 rematrixing, §7.7.1 dynamic
|
||||
range, §7.9 transforms and block switching. The window is computed as the Kaiser-Bessel derived
|
||||
window (α = 5) and checked against Table 7.33 to 6·10⁻⁶.
|
||||
window (α = 5) and checked against Table 7.33 to 6·10⁻⁶. E-AC-3: Annex E §E2 bit stream syntax
|
||||
(syncinfo, bsi, audfrm, audblk; Tables E2.1-E2.13, frame exponent strategies from Table E2.10),
|
||||
§E3.3 modified parameters, §E3.4.2 AHT helper variables (to detect AHT), §E3.6 spectral extension
|
||||
(attenuation Table E3.14, which is 2^(-(code + 1)(bin + 1) / 15), checked against the table).
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
let mut dec = filmcraft_ac3::Decoder::new();
|
||||
let h = filmcraft_ac3::parse_header(frame)?; // rate, channels, frame size
|
||||
let out = dec.decode(frame)?; // 1536 samples per channel, WAV channel order
|
||||
let out = dec.decode(frame)?; // h.samples() per channel, WAV channel order
|
||||
dec.reset(); // before decoding from another position
|
||||
```
|
||||
|
||||
@@ -26,7 +29,7 @@ dec.reset(); // before decoding from another
|
||||
|
||||
- Every audio coding mode (1+1, 1/0, 2/0, 3/0, 2/1, 3/1, 2/2, 3/2) with or without LFE; 32 /
|
||||
44.1 / 48 kHz and every frame size; bsid ≤ 8 (9 and 10, the half / quarter-rate variants, are
|
||||
accepted). E-AC-3 (bsid 11-16) is refused with `Error::Unsupported`.
|
||||
accepted).
|
||||
- D15 / D25 / D45 exponents with reuse across blocks; the parametric bit allocation in the
|
||||
standard's fixed-point steps, delta bit allocation, the all-zero SNR offset special case;
|
||||
grouped 3-, 5- and 11-level mantissas (groups shared across exponent sets), 7- and 15-level
|
||||
@@ -43,6 +46,26 @@ dec.reset(); // before decoding from another
|
||||
- Errors (bad exponents, reserved codes, blocks running past the frame) return `Err`; garbage
|
||||
never panics.
|
||||
|
||||
## E-AC-3 (bsid 11-16)
|
||||
|
||||
- Independent substream 0 is decoded (stream types 0 and 2); dependent substreams (the channels
|
||||
beyond 5.1 of a 7.1 program) and further independent programs decode to no channels, so a 7.1
|
||||
stream plays as its 5.1 core. `Header::is_primary` tells them apart.
|
||||
- 1, 2, 3 or 6 blocks per syncframe (`Header::samples`), 32 / 44.1 / 48 kHz and the reduced rates
|
||||
16 / 22.05 / 24 kHz, every acmod with or without LFE.
|
||||
- The whole bsi (mixing and informational metadata, custom channel maps, addbsi) is parsed and
|
||||
skipped; audfrm: per-block or frame-based exponent strategies (exponents may be reused across
|
||||
syncframes of fewer than six blocks; the decoder keeps them until `reset`), the syntax-enable
|
||||
flags and their defaults, SNR offset strategies 1-3, converter fields, block start information.
|
||||
- Audio blocks share the AC-3 path: coupling with the default or transmitted band structure
|
||||
(`firstcplcos` / `firstcplleak` states), rematrixing with the Annex E band count, end mantissas
|
||||
derived from spectral extension.
|
||||
- Spectral extension: band structure, coordinates, blending factors, translation with wrap,
|
||||
banded RMS, the border / wrap-point notch filter, noise blending and scaling (§E3.6.4).
|
||||
- Refused with `Error::Unsupported`: the adaptive hybrid transform (when a channel uses it; `ahte`
|
||||
alone is fine) and enhanced coupling. Transient pre-noise processing data is read and not
|
||||
applied.
|
||||
|
||||
## Accuracy (vs FFmpeg as an external oracle)
|
||||
|
||||
AC-3 output is not bit-exact across decoders: zero-bit mantissas are filled with
|
||||
@@ -56,10 +79,17 @@ FFmpeg's decode (worst channel SNR):
|
||||
| 1/0 32 kHz 96 kb/s | 94.1 dB | 95.7 dB |
|
||||
| 3/2 + LFE 48 kHz 448 kb/s | 81.4 dB | 90.2 dB |
|
||||
| 2/0 pink noise 128 kb/s (many zero-bit mantissas) | 27.7 dB | 27.9 dB |
|
||||
| E-AC-3 2/0 48 kHz 192 kb/s (tones, chirp) | 64.8 dB | 74.7 dB |
|
||||
| E-AC-3 2/0 44.1 kHz 64 kb/s with coupling and rematrixing | 65.5 dB | 66.1 dB |
|
||||
| E-AC-3 1/0 32 kHz 64 kb/s | 91.0 dB | 90.1 dB |
|
||||
| E-AC-3 3/2 + LFE 48 kHz 384 kb/s with mixing / informational metadata | 81.6 dB | 85.3 dB |
|
||||
| E-AC-3 2/0 pink noise 96 kb/s | 48.6 dB | 47.9 dB |
|
||||
|
||||
The noise row shows the difference is dither: two of our own decodes with different dither
|
||||
sequences differ as much as we differ from FFmpeg. In MPEG-2 TS (AVCHD-style `.mts`) and VOB the
|
||||
maximum sample difference to FFmpeg is 2.3·10⁻³.
|
||||
maximum sample difference to FFmpeg is 2.3·10⁻³. E-AC-3 through `filmcraft-codecs`
|
||||
(`tests/eac3_oracle.rs`): 5.1 in MP4 and Matroska 91.3 dB, stereo in QuickTime 87.0 dB and
|
||||
MPEG-TS 79.5 dB against FFmpeg's decode.
|
||||
|
||||
## Gaps
|
||||
|
||||
@@ -67,4 +97,10 @@ maximum sample difference to FFmpeg is 2.3·10⁻³.
|
||||
§7.9.4.2 but not checked against an oracle.
|
||||
- No downmixing, no heavy compression (`compr`) or dialogue normalisation (both are metadata
|
||||
for playback systems), no CRC check (corrupt frames are caught by consistency checks).
|
||||
- E-AC-3 is not decoded.
|
||||
- FFmpeg's E-AC-3 encoder writes six-block syncframes with frame-based exponent strategies,
|
||||
coupling with the default band structure and none of the optional per-block syntax. Spectral
|
||||
extension (no free encoder writes it), fewer blocks per syncframe, the reduced rates and the
|
||||
per-block syntax (shared with AC-3) are implemented from the standard and covered by unit tests
|
||||
on synthetic data, not by an oracle.
|
||||
- E-AC-3: no adaptive hybrid transform, enhanced coupling, transient pre-noise processing, or
|
||||
channels from dependent substreams (7.1 plays as 5.1).
|
||||
@@ -0,0 +1,634 @@
|
||||
//! 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::{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<Header> {
|
||||
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.
|
||||
pub(crate) fn read_bsi(r: &mut Bits) -> Result<Bsi> {
|
||||
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<usize>; 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<FrameInfo> {
|
||||
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<usize>, 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::<f32>() / 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<_>>(), 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));
|
||||
}
|
||||
}
|
||||
+321
-80
@@ -1,5 +1,6 @@
|
||||
//! Clean-room AC-3 (Dolby Digital) audio decoder, written from ATSC A/52:2012 ("Digital Audio
|
||||
//! Compression Standard", §5 bit stream syntax, §6-7 decoding).
|
||||
//! 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).
|
||||
@@ -11,10 +12,15 @@
|
||||
//! - 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) is recognised and refused.
|
||||
//! 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.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod eac3;
|
||||
mod tables;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
@@ -47,12 +53,35 @@ pub struct Header {
|
||||
pub bsid: u8,
|
||||
pub acmod: u8,
|
||||
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] + self.lfeon as 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +93,11 @@ pub fn parse_header(b: &[u8]) -> Result<Header> {
|
||||
let fscod = b[4] >> 6;
|
||||
let frmsizecod = b[4] & 0x3F;
|
||||
let bsid = b[5] >> 3;
|
||||
if bsid > 10 {
|
||||
return Err(Error::Unsupported(format!("bsid {bsid} (E-AC-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"));
|
||||
@@ -91,7 +123,7 @@ pub fn parse_header(b: &[u8]) -> Result<Header> {
|
||||
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 })
|
||||
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.
|
||||
@@ -469,7 +501,9 @@ struct Frame {
|
||||
chincpl: [bool; 5],
|
||||
phsflginu: bool,
|
||||
cplbegf: usize,
|
||||
cplendf: 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,
|
||||
@@ -500,7 +534,7 @@ impl Frame {
|
||||
chincpl: [false; 5],
|
||||
phsflginu: false,
|
||||
cplbegf: 0,
|
||||
cplendf: 0,
|
||||
cplend: 0,
|
||||
sub_to_band: [0; 18],
|
||||
ncplbnd: 0,
|
||||
cplco: [[0.0; 18]; 5],
|
||||
@@ -523,7 +557,7 @@ impl Frame {
|
||||
37 + 12 * self.cplbegf
|
||||
}
|
||||
fn cplendmant(&self) -> usize {
|
||||
37 + 12 * (self.cplendf + 3)
|
||||
37 + 12 * self.cplend
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,6 +656,9 @@ 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<Frame>)>,
|
||||
}
|
||||
|
||||
impl Default for Decoder {
|
||||
@@ -630,7 +667,8 @@ impl Default for Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded syncframe: 1536 samples per channel, WAV channel order.
|
||||
/// 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`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Decoded {
|
||||
pub header: Header,
|
||||
@@ -639,7 +677,7 @@ pub struct Decoded {
|
||||
|
||||
impl Decoder {
|
||||
pub fn new() -> Decoder {
|
||||
Decoder { delay: [[0.0; 256]; 6], dither: 0, seed: 0x1234_5678 }
|
||||
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).
|
||||
@@ -650,6 +688,7 @@ impl Decoder {
|
||||
/// 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 {
|
||||
@@ -658,12 +697,20 @@ impl Decoder {
|
||||
((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<Decoded> {
|
||||
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).
|
||||
@@ -708,39 +755,83 @@ impl Decoder {
|
||||
let l = r.read(6) as usize;
|
||||
r.skip((l + 1) * 8);
|
||||
}
|
||||
let nfchans = NFCHANS[acmod];
|
||||
let mut f = Frame::new(nfchans, fscod);
|
||||
let mut f = Frame::new(NFCHANS[acmod], fscod);
|
||||
let pcm = self.blocks(&mut r, &mut f, acmod, lfeon, 6, None)?;
|
||||
Ok(Decoded { 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<Decoded> {
|
||||
if !h.is_primary() {
|
||||
return Ok(Decoded { 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(Frame::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(Decoded { 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 Frame, acmod: usize, lfeon: bool, nblocks: usize, mut x: Option<&mut eac3::FrameInfo>) -> Result<Vec<Vec<f32>>> {
|
||||
let nfchans = f.nfchans;
|
||||
let nout = nfchans + lfeon as usize;
|
||||
let mut pcm: Vec<Vec<f32>> = vec![Vec::with_capacity(1536); nout];
|
||||
for blk in 0..6 {
|
||||
let mut pcm: Vec<Vec<f32>> = vec![Vec::with_capacity(256 * nblocks); nout];
|
||||
for blk in 0..nblocks {
|
||||
let mut coefs = [[0f32; 256]; 6];
|
||||
self.audio_block(&mut r, &mut f, acmod, lfeon, blk, &mut coefs)?;
|
||||
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 in 0..nout {
|
||||
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 {
|
||||
pcm[ch].push(2.0 * (buf[n] + d[n]));
|
||||
out.push(2.0 * (buf[n] + d[n]));
|
||||
d[n] = buf[256 + n];
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Decoded { header: h, channels: wav_order(pcm, acmod, lfeon) })
|
||||
Ok(pcm)
|
||||
}
|
||||
|
||||
fn audio_block(&mut self, r: &mut Bits, f: &mut Frame, acmod: usize, lfeon: bool, blk: usize, coefs: &mut [[f32; 256]; 6]) -> Result<()> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn audio_block(
|
||||
&mut self,
|
||||
r: &mut Bits,
|
||||
f: &mut Frame,
|
||||
acmod: usize,
|
||||
lfeon: bool,
|
||||
blk: usize,
|
||||
coefs: &mut [[f32; 256]; 6],
|
||||
mut x: Option<&mut eac3::FrameInfo>,
|
||||
) -> Result<()> {
|
||||
let nf = f.nfchans;
|
||||
for ch in 0..nf {
|
||||
f.blksw[ch] = r.bit();
|
||||
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()),
|
||||
}
|
||||
for ch in 0..nf {
|
||||
f.dithflag[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() {
|
||||
@@ -749,26 +840,62 @@ impl Decoder {
|
||||
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
|
||||
if r.bit() {
|
||||
f.cplinu = r.bit();
|
||||
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 {
|
||||
for ch in 0..nf {
|
||||
f.chincpl[ch] = r.bit();
|
||||
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.cplendf = r.read(4) as usize;
|
||||
if f.cplendf + 3 <= f.cplbegf {
|
||||
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"));
|
||||
}
|
||||
let nsub = 3 + f.cplendf - f.cplbegf;
|
||||
// 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..nsub {
|
||||
if !r.bit() {
|
||||
for s in 1..f.cplend - f.cplbegf {
|
||||
if !strc[f.cplbegf + s] {
|
||||
band += 1;
|
||||
}
|
||||
f.sub_to_band[s] = band;
|
||||
@@ -776,15 +903,33 @@ impl Decoder {
|
||||
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 {
|
||||
} 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] && r.bit() {
|
||||
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 {
|
||||
@@ -792,7 +937,7 @@ impl Decoder {
|
||||
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..(3 + f.cplendf - f.cplbegf) {
|
||||
for s in 0..(f.cplend - f.cplbegf) {
|
||||
if f.sub_to_band[s] == bnd {
|
||||
f.cplco[ch][s] = co;
|
||||
}
|
||||
@@ -803,7 +948,7 @@ impl Decoder {
|
||||
if acmod == 2 && f.phsflginu && any {
|
||||
for bnd in 0..f.ncplbnd {
|
||||
let flag = r.bit();
|
||||
for s in 0..(3 + f.cplendf - f.cplbegf) {
|
||||
for s in 0..(f.cplend - f.cplbegf) {
|
||||
if f.sub_to_band[s] == bnd {
|
||||
f.phsflg[s] = flag;
|
||||
}
|
||||
@@ -811,34 +956,55 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
// rematrixing
|
||||
if acmod == 2 && r.bit() {
|
||||
let n = if !f.cplinu || f.cplbegf > 2 {
|
||||
4
|
||||
} else if f.cplbegf > 0 {
|
||||
3
|
||||
// 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 {
|
||||
2
|
||||
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
|
||||
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;
|
||||
// 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 {
|
||||
@@ -870,40 +1036,105 @@ impl Decoder {
|
||||
decode_exponents(r, abs, 2, 1, &mut f.exps[LFE], 0, false)?;
|
||||
}
|
||||
// bit allocation parametric information
|
||||
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 {
|
||||
return Err(Error::Invalid("no bit allocation information in block 0"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
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
|
||||
}
|
||||
}
|
||||
for ch in 0..nf {
|
||||
f.fsnroffst[ch] = r.read(4) as i32;
|
||||
f.fgaincod[ch] = r.read(3) as usize;
|
||||
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 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 && r.bit() {
|
||||
f.cplleak = (((r.read(3) as i32) << 8) + 768, ((r.read(3) as i32) << 8) + 768);
|
||||
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 r.bit() {
|
||||
if x.as_deref().is_none_or(|e| e.dbaflde) && r.bit() {
|
||||
if f.cplinu {
|
||||
f.deltbae[CPL] = r.read(2) as u8;
|
||||
}
|
||||
@@ -921,7 +1152,7 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
// skip field
|
||||
if r.bit() {
|
||||
if x.as_deref().is_none_or(|e| e.skipflde) && r.bit() {
|
||||
let l = r.read(9) as usize;
|
||||
r.skip(l * 8);
|
||||
}
|
||||
@@ -1014,6 +1245,16 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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] };
|
||||
|
||||
+174
-1
@@ -56,7 +56,7 @@ fn headers_and_gains() {
|
||||
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, 16 << 3, 0, 0]), Err(Error::Unsupported("bsid 16 (E-AC-3)".into())));
|
||||
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);
|
||||
@@ -96,3 +96,176 @@ fn garbage_never_panics() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MSB-first bit writer for synthetic E-AC-3 syncframes.
|
||||
#[derive(Default)]
|
||||
struct W {
|
||||
b: Vec<u8>,
|
||||
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<u8> {
|
||||
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()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_single_block_frames_reuse_exponents_across_syncframes() {
|
||||
// mono, one block per syncframe; the first sends exponents (all 10, bandwidth code 0), the
|
||||
// second reuses them. Zero SNR offsets allocate no bits: every coefficient is dither.
|
||||
let frame = |new_exps: bool| {
|
||||
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
|
||||
})
|
||||
};
|
||||
let (first, second) = (frame(true), frame(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<u8> = (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
-7
@@ -1,5 +1,5 @@
|
||||
//! AC-3 against ffmpeg: ffmpeg's AC-3 encoder writes 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
|
||||
//! 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.
|
||||
|
||||
@@ -8,6 +8,8 @@ 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,
|
||||
@@ -19,6 +21,7 @@ struct Spec {
|
||||
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,
|
||||
@@ -28,16 +31,26 @@ const SPECS: &[Spec] = &[
|
||||
// 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", input: "sine=frequency=1000:sample_rate=32000", rate: 32_000, channels: 1, enc: &["-b:a", "96k"], min_snr: 60.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,
|
||||
@@ -47,22 +60,86 @@ const SPECS: &[Spec] = &[
|
||||
// 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<PathBuf> {
|
||||
let out = filmcraft_testkit::fixtures_dir("ac3").join(format!("{}.ac3", s.name));
|
||||
let out = filmcraft_testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec));
|
||||
filmcraft_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", "ac3"])
|
||||
.args(["-c:a", s.codec])
|
||||
.args(s.enc)
|
||||
.args(["-f", "ac3"])
|
||||
.args(["-f", s.codec])
|
||||
.arg(tmp)
|
||||
.stdin(Stdio::null())
|
||||
.status();
|
||||
@@ -94,6 +171,7 @@ fn ffmpeg_encoded_streams_decode() {
|
||||
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);
|
||||
}
|
||||
@@ -136,12 +214,52 @@ fn ffmpeg_encoded_streams_decode() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = filmcraft_testkit::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 = filmcraft_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) = filmcraft_testkit::ffmpeg_or_skip("ac3 fixtures") else { return };
|
||||
for s in SPECS {
|
||||
let out = filmcraft_testkit::fixtures_dir("ac3").join(format!("{}.ac3", s.name));
|
||||
let out = filmcraft_testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec));
|
||||
filmcraft_testkit::fixtures::generate_and_report(&format!("ac3/{}", s.name), &[out], || make(&ff, s));
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ enum Inner {
|
||||
Opus { dec: Box<filmcraft_opus::Decoder>, order: Option<&'static [usize]> },
|
||||
/// Bootstrap decoders (MP3, ALAC, FLAC, HE-AAC…) via symphonia.
|
||||
Symphonia(Box<dyn Decoder>),
|
||||
/// Our AC-3 decoder (ATSC A/52).
|
||||
/// Our AC-3 and E-AC-3 decoder (ATSC A/52, Annex E).
|
||||
Ac3(Box<filmcraft_ac3::Decoder>),
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ impl PacketDecoder {
|
||||
self.inner = Inner::Aac { dec: Box::new(dec), asc: asc.to_vec(), up: None };
|
||||
Ok(())
|
||||
}
|
||||
/// AC-3 (ATSC A/52).
|
||||
/// AC-3 and E-AC-3 (ATSC A/52, Annex E).
|
||||
pub fn ac3() -> Result<Self> {
|
||||
Ok(Self { inner: Inner::Ac3(Box::new(filmcraft_ac3::Decoder::new())), channels: 0 })
|
||||
}
|
||||
@@ -161,7 +161,7 @@ impl PacketDecoder {
|
||||
match c {
|
||||
C::Aac(a) => Self::aac(&a.asc, if rate > 0 { rate } else { a.sample_rate }),
|
||||
C::Mp3 => Self::new(CODEC_TYPE_MP3, rate, None),
|
||||
C::Ac3 { .. } => Self::ac3(),
|
||||
C::Ac3 { .. } | C::Eac3 { .. } => Self::ac3(),
|
||||
C::Alac { cookie } => Self::new(CODEC_TYPE_ALAC, rate, Some(cookie.clone())),
|
||||
C::Flac(_) => Self::new(CODEC_TYPE_FLAC, rate, None),
|
||||
C::Opus(o) => Self::opus(filmcraft_opus::OpusHead::from_dops(&o.to_bytes()).map_err(|e| CodecError::Unsupported(format!("Opus: {e}")))?),
|
||||
@@ -193,12 +193,16 @@ impl PacketDecoder {
|
||||
Inner::Symphonia(d) => d,
|
||||
Inner::AacPending => return Err(CodecError::Decode("AAC: no configuration yet".into())),
|
||||
Inner::Ac3(d) => {
|
||||
// a packet may hold several syncframes (MP4 / Matroska)
|
||||
// a packet may hold several syncframes (MP4 / Matroska); E-AC-3 dependent
|
||||
// substreams and further programs decode to no channels and are skipped
|
||||
let mut out: Vec<Vec<f32>> = Vec::new();
|
||||
let mut p = 0;
|
||||
while p < data.len() {
|
||||
let f = d.decode(&data[p..]).map_err(|e| CodecError::Decode(e.to_string()))?;
|
||||
p += f.header.frame_bytes;
|
||||
if f.channels.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if out.is_empty() {
|
||||
out = f.channels;
|
||||
} else {
|
||||
@@ -279,21 +283,39 @@ pub enum FixedFrames<'a> {
|
||||
Aac(&'a [u8]),
|
||||
/// MPEG-1/2 audio, layer I, II or III.
|
||||
MpegAudio,
|
||||
/// AC-3 (and E-AC-3).
|
||||
/// AC-3 (and E-AC-3 in a stream of single syncframes, MPEG-TS).
|
||||
Ac3,
|
||||
/// E-AC-3 packets of whole syncframes (MP4, Matroska): an access unit may hold several
|
||||
/// syncframes and dependent substreams.
|
||||
Eac3,
|
||||
}
|
||||
|
||||
/// Samples each packet decodes to at the stream's output `rate`, for codecs where that is the same
|
||||
/// for every packet: AAC 1024 (2048 when [`PacketDecoder::aac`] upsamples an HE-AAC core); MPEG
|
||||
/// audio and AC-3 from the frame header of `first`, the stream's first packet. `None` when it is
|
||||
/// not known: an AAC configuration our decoder does not read, a header that does not parse, or a
|
||||
/// header whose rate is not `rate`.
|
||||
/// audio and AC-3 from the frame header of `first`, the stream's first packet; E-AC-3 from the
|
||||
/// syncframes of the program we decode in `first`. `None` when it is not known: an AAC
|
||||
/// configuration our decoder does not read, a header that does not parse, or a header whose rate
|
||||
/// is not `rate`.
|
||||
pub fn fixed_packet_samples(codec: FixedFrames, first: &[u8], rate: u32) -> Option<i64> {
|
||||
let mpegts = match codec {
|
||||
FixedFrames::Aac(asc) => {
|
||||
let core = filmcraft_aac::Decoder::new(asc).ok()?.sample_rate();
|
||||
return Some(if upsamples_core(core, rate) { 2048 } else { 1024 });
|
||||
}
|
||||
FixedFrames::Eac3 => {
|
||||
let mut samples = 0usize;
|
||||
let mut p = 0;
|
||||
while let Some(h) = first.get(p..).and_then(|b| filmcraft_ac3::parse_header(b).ok()) {
|
||||
if h.is_primary() {
|
||||
if h.sample_rate != rate {
|
||||
return None;
|
||||
}
|
||||
samples += h.samples();
|
||||
}
|
||||
p += h.frame_bytes;
|
||||
}
|
||||
return i64::try_from(samples).ok().filter(|&n| n > 0);
|
||||
}
|
||||
FixedFrames::MpegAudio => filmcraft_mpegts::Codec::MpegAudio,
|
||||
FixedFrames::Ac3 => filmcraft_mpegts::Codec::Ac3,
|
||||
};
|
||||
@@ -405,7 +427,7 @@ impl Upsample2x {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether AC-3 audio decodes (our ATSC A/52 decoder).
|
||||
/// Whether AC-3 and E-AC-3 audio decode (our ATSC A/52 decoder).
|
||||
pub const AC3_DECODER: bool = true;
|
||||
|
||||
/// An AudioSpecificConfig for AAC with these parameters (ISO/IEC 14496-3 §1.6.2.1, GA specific
|
||||
@@ -967,5 +989,14 @@ mod packet_time_tests {
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::MpegAudio, &[0xFF, 0xFB, 0x90, 0x44], 48_000), None);
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::MpegAudio, &[0xFF], 44_100), None);
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::Ac3, &[0x0B, 0x77], 48_000), None);
|
||||
// E-AC-3 access units: 8-byte syncframes (frmsiz 3) at 48 kHz, stereo. A six-block
|
||||
// independent frame with a dependent substream (7.1) is 1536 samples; two one-block
|
||||
// frames are 512
|
||||
let frame = |strmtyp: u8, numblkscod: u8| [0x0B, 0x77, strmtyp << 6, 3, (numblkscod << 4) | (2 << 1), 16 << 3, 0, 0];
|
||||
let au = |frames: &[[u8; 8]]| frames.concat();
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::Eac3, &au(&[frame(0, 3), frame(1, 3)]), 48_000), Some(1536));
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::Eac3, &au(&[frame(0, 0), frame(0, 0)]), 48_000), Some(512));
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::Eac3, &au(&[frame(0, 3)]), 44_100), None);
|
||||
assert_eq!(fixed_packet_samples(FixedFrames::Eac3, &[0x0B, 0x77], 48_000), None);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ pub struct MkvSource {
|
||||
///
|
||||
/// Block timestamps are quantised to `TimestampScale` (usually 1 ms), so they are only accurate to a
|
||||
/// tick: a 1024-sample AAC frame at 48 kHz lasts 21.33 ms. Packets that all decode to the same
|
||||
/// length (AAC, MPEG audio, AC-3) therefore run on from one another, resynchronising to the
|
||||
/// length (AAC, MPEG audio, AC-3, E-AC-3) therefore run on from one another, resynchronising to the
|
||||
/// timestamp only across gaps of more than half a packet (and more than two ticks); see
|
||||
/// [`crate::audio::contiguous_starts`]. Opus packets: [`opus_starts`]. Other codecs start at their
|
||||
/// timestamps.
|
||||
@@ -86,6 +86,7 @@ fn audio_starts(file: &MkvFile, bytes: &crate::Src, ti: usize, rate: i64) -> Vec
|
||||
Codec::Aac { asc } => fixed_packet_samples(FixedFrames::Aac(asc), &[], out_rate),
|
||||
Codec::Mp3 | Codec::Mp2 => fixed_packet_samples(FixedFrames::MpegAudio, &first(), out_rate),
|
||||
Codec::Ac3 => fixed_packet_samples(FixedFrames::Ac3, &first(), out_rate),
|
||||
Codec::Eac3 => fixed_packet_samples(FixedFrames::Eac3, &first(), out_rate),
|
||||
_ => None,
|
||||
};
|
||||
let Some(frame) = frame else {
|
||||
@@ -447,7 +448,7 @@ impl MkvSource {
|
||||
Codec::Opus { head } => PacketDecoder::opus(filmcraft_opus::OpusHead::parse(head).map_err(|e| CodecError::Unsupported(format!("Opus: {e}")))?),
|
||||
Codec::Mp3 => PacketDecoder::new(CODEC_TYPE_MP3, rate, None),
|
||||
Codec::Mp2 => PacketDecoder::mpeg_audio(2, rate),
|
||||
Codec::Ac3 => PacketDecoder::ac3(),
|
||||
Codec::Ac3 | Codec::Eac3 => PacketDecoder::ac3(),
|
||||
// symphonia wants the STREAMINFO block body: skip `fLaC` + the 4-byte block header.
|
||||
Codec::Flac { private } => PacketDecoder::new(CODEC_TYPE_FLAC, rate, private.get(8..42).map(<[u8]>::to_vec)),
|
||||
Codec::Vorbis { headers } if headers.len() == 3 => {
|
||||
|
||||
@@ -57,8 +57,9 @@ pub fn sniff(b: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
/// Audio packet start positions in source sample frames at `rate`: the running total of the sample
|
||||
/// durations, except that packets which all decode to the same length (AAC, MPEG audio, AC-3) run
|
||||
/// on from one another, resynchronising to the total only across gaps of more than half a packet.
|
||||
/// durations, except that packets which all decode to the same length (AAC, MPEG audio, AC-3,
|
||||
/// E-AC-3) run on from one another, resynchronising to the total only across gaps of more than half
|
||||
/// a packet.
|
||||
/// A remux from Matroska (OBS's) carries the millisecond rounding of its timestamps into the
|
||||
/// durations: 1008, 1008, 1056… for 1024-sample AAC frames at 48 kHz (see
|
||||
/// [`crate::audio::contiguous_starts`]).
|
||||
@@ -81,6 +82,7 @@ fn audio_starts(file: &Mp4File, bytes: &crate::Src, ti: usize, rate: u32) -> Vec
|
||||
Some(CodecConfig::Aac(a)) => fixed_packet_samples(FixedFrames::Aac(&a.asc), &[], rate),
|
||||
Some(CodecConfig::Mp3) => fixed_packet_samples(FixedFrames::MpegAudio, &first(), rate),
|
||||
Some(CodecConfig::Ac3 { .. }) => fixed_packet_samples(FixedFrames::Ac3, &first(), rate),
|
||||
Some(CodecConfig::Eac3 { .. }) => fixed_packet_samples(FixedFrames::Eac3, &first(), rate),
|
||||
_ => None,
|
||||
};
|
||||
let Some(frame) = frame else { return stamps };
|
||||
|
||||
@@ -120,7 +120,7 @@ fn vkind(c: &Codec) -> Option<VKind> {
|
||||
}
|
||||
|
||||
fn audio_supported(c: &Codec) -> bool {
|
||||
matches!(c, Codec::MpegAudio | Codec::AacAdts | Codec::AacLatm | Codec::LpcmBluray | Codec::LpcmDvd | Codec::Ac3)
|
||||
matches!(c, Codec::MpegAudio | Codec::AacAdts | Codec::AacLatm | Codec::LpcmBluray | Codec::LpcmDvd | Codec::Ac3 | Codec::Eac3)
|
||||
}
|
||||
|
||||
/// The coded rate unless the timestamps say otherwise (3:2 pulldown, field-rate coding). Never
|
||||
@@ -505,7 +505,7 @@ impl MpegSource {
|
||||
};
|
||||
let mut latm = None;
|
||||
let (codec, rate, channels, per_frame, bits) = match &st.codec {
|
||||
Codec::MpegAudio | Codec::AacAdts | Codec::Ac3 => {
|
||||
Codec::MpegAudio | Codec::AacAdts | Codec::Ac3 | Codec::Eac3 => {
|
||||
let Some(fi) = filmcraft_mpegts::frame_info(&st.codec, &first) else {
|
||||
return self.audio_unsupported(&codec_name, format!("{codec_name}: bad frame header"));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//! E-AC-3 (Dolby Digital Plus) audio in MP4, Matroska and MPEG-TS against ffmpeg: ffmpeg encodes
|
||||
//! and muxes the files and decodes them as the oracle. Zero-bit mantissas carry decoder-specific
|
||||
//! dither (A/52 §7.3.4), so the criterion is SNR against ffmpeg's decode, sample-aligned.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
|
||||
const STEREO: &str = "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000";
|
||||
const SURROUND: &str =
|
||||
"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";
|
||||
|
||||
/// Worst channel SNR (dB) of `ours` (planar) against ffmpeg's interleaved `want`, with ours
|
||||
/// shifted by `lag` samples, over the samples both cover past the first 1000.
|
||||
fn snr(ours: &[Vec<f32>], want: &[f32], ch: usize, lag: i64) -> f64 {
|
||||
let n = want.len() / ch;
|
||||
(0..ch)
|
||||
.map(|c| {
|
||||
let (mut sig, mut err) = (0f64, 0f64);
|
||||
for i in 1000..n - 1000 {
|
||||
let Some(&a) = ours[c].get((i as i64 + lag) as usize) else { continue };
|
||||
let (a, b) = (a as f64, want[i * ch + c] as f64);
|
||||
sig += b * b;
|
||||
err += (a - b) * (a - b);
|
||||
}
|
||||
10.0 * (sig / err.max(1e-30)).log10()
|
||||
})
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_in_mp4_matroska_and_transport_streams() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let cases: [(&str, &str, u32, &[&str]); 4] = [
|
||||
("eac3_51.mp4", SURROUND, 6, &["-b:a", "384k"]),
|
||||
("eac3_51.mkv", SURROUND, 6, &["-b:a", "384k"]),
|
||||
("eac3_stereo.mov", STEREO, 2, &["-b:a", "192k"]),
|
||||
("eac3_stereo.ts", STEREO, 2, &["-b:a", "192k"]),
|
||||
];
|
||||
for (name, input, channels, enc) in cases {
|
||||
let args: Vec<&str> = ["-f", "lavfi", "-i", input, "-t", "2", "-c:a", "eac3"].into_iter().chain(enc.iter().copied()).collect();
|
||||
let f = fixture(&ff, name, &args).unwrap_or_else(|| panic!("could not write {name}"));
|
||||
let src = filmcraft_codecs::open_bytes(name, bytes(&f)).unwrap();
|
||||
let a = src.info().audio().cloned().unwrap();
|
||||
assert_eq!((a.sample_rate, a.channels), (48_000, channels), "{name}: {}", a.codec);
|
||||
let want = ffmpeg_audio_f32(&ff, &f, &[]);
|
||||
let ch = channels as usize;
|
||||
let n = want.len() / ch;
|
||||
// MPEG-TS audio starts at its first PTS relative to the presentation start
|
||||
let start = match name.ends_with(".ts") {
|
||||
true => filmcraft_codecs::MpegSource::open(name, bytes(&f)).unwrap().audio_extent().map_or(0, |e| e.0),
|
||||
false => 0,
|
||||
};
|
||||
let ours = src.audio(start, n, 48_000).unwrap().channels;
|
||||
assert_eq!(ours.len(), ch, "{name}");
|
||||
// Matroska stores CodecDelay (the encoder's 256-sample priming) in ns and timestamps in
|
||||
// ms, and we place packets at the rounded value (240 samples): a shared Matroska timing
|
||||
// limit (AAC and AC-3 alike), not a decoding one. Elsewhere the samples line up exactly.
|
||||
let (worst, lag) = (-32..=32).map(|lag| (snr(&ours, &want, ch, lag), lag)).fold((f64::MIN, 0), |a, b| if b.0 > a.0 { b } else { a });
|
||||
let max_lag = if name.ends_with(".mkv") { 16 } else { 0 };
|
||||
assert!(lag.abs() <= max_lag, "{name}: decode is {lag} samples off ffmpeg's");
|
||||
// a window read on a fresh source (random access) gives the same samples
|
||||
let fresh = filmcraft_codecs::open_bytes(name, bytes(&f)).unwrap();
|
||||
let at = n / 3;
|
||||
let win = fresh.audio(start + at as i64, 4800, 48_000).unwrap().channels;
|
||||
let mut seek_err = 0f32;
|
||||
for c in 0..ch {
|
||||
for (k, v) in win[c].iter().enumerate() {
|
||||
seek_err = seek_err.max((v - ours[c][at + k]).abs());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{name}: {} ({} Hz, {ch} ch), worst channel SNR vs ffmpeg {worst:.1} dB (lag {lag}), random access max diff {seek_err:.1e}",
|
||||
a.codec, a.sample_rate
|
||||
);
|
||||
assert!(worst >= 50.0, "{name}: SNR {worst:.1} dB");
|
||||
assert!(seek_err <= 1e-6, "{name}: random-access audio differs by {seek_err}");
|
||||
}
|
||||
}
|
||||
+39
-7
@@ -235,6 +235,15 @@ pub fn frame_info(codec: &Codec, b: &[u8]) -> Option<FrameInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the syncframe at the start of `b` is E-AC-3 (bsid 11-16) other than independent
|
||||
/// substream 0: a dependent substream (strmtyp 1) or another program (A/52 §E2.3.1.1-2).
|
||||
fn eac3_joins_unit(b: &[u8]) -> bool {
|
||||
match b.get(2..6) {
|
||||
Some(&[b2, _, _, b5]) => (11..=16).contains(&(b5 >> 3)) && (b2 >> 6 == 1 || (b2 >> 3) & 7 != 0),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-bandwidth channels per AC-3 audio coding mode (A/52 Table 5.8).
|
||||
const AC3_CHANNELS: [u32; 8] = [2, 1, 2, 3, 3, 4, 4, 5];
|
||||
|
||||
@@ -572,14 +581,18 @@ impl Splitter {
|
||||
}
|
||||
match frame_len(sync, &self.buf[i..]) {
|
||||
Some(len) if self.next_frame == Some(at) || self.confirmed(sync, i, len) => {
|
||||
self.close(at);
|
||||
let (pos, offset) = self.locate(at).unwrap_or((u64::MAX, 0));
|
||||
let mut o = Open { start: at, pos, offset, pts: None, dts: None, key: true, disposable: false, vcl: false, picture: None };
|
||||
if let Some((pts, dts)) = self.timestamps_at(at) {
|
||||
o.pts = pts;
|
||||
o.dts = dts;
|
||||
// an E-AC-3 dependent substream or further program joins the access unit of
|
||||
// the independent substream 0 before it, as in MP4 and Matroska
|
||||
if !(sync == AudioSync::Ac3 && self.open.is_some() && eac3_joins_unit(&self.buf[i..])) {
|
||||
self.close(at);
|
||||
let (pos, offset) = self.locate(at).unwrap_or((u64::MAX, 0));
|
||||
let mut o = Open { start: at, pos, offset, pts: None, dts: None, key: true, disposable: false, vcl: false, picture: None };
|
||||
if let Some((pts, dts)) = self.timestamps_at(at) {
|
||||
o.pts = pts;
|
||||
o.dts = dts;
|
||||
}
|
||||
self.open = Some(o);
|
||||
}
|
||||
self.open = Some(o);
|
||||
self.next_frame = Some(at + len as u64);
|
||||
if at + len as u64 >= self.buf_start + self.buf.len() as u64 {
|
||||
self.buf.clear();
|
||||
@@ -654,4 +667,23 @@ mod tests {
|
||||
assert_eq!(ac3_frame_bytes(1, 21), Some(836));
|
||||
assert_eq!(ac3_frame_bytes(2, 0), Some(192));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_dependent_substreams_join_the_access_unit() {
|
||||
// 16-byte E-AC-3 syncframes (frmsiz 7), 48 kHz six blocks stereo: independent, dependent
|
||||
// (7.1 extension), independent, dependent
|
||||
let frame = |strmtyp: u8| {
|
||||
let mut f = vec![0x0B, 0x77, strmtyp << 6, 7, (3 << 4) | (2 << 1), 16 << 3];
|
||||
f.resize(16, 0);
|
||||
f
|
||||
};
|
||||
let es: Vec<u8> = [frame(0), frame(1), frame(0), frame(1)].concat();
|
||||
let mut s = Splitter::new(&Codec::Eac3);
|
||||
s.feed(0, &es);
|
||||
s.finish();
|
||||
assert_eq!(s.units.iter().map(|u| u.size).collect::<Vec<_>>(), vec![32, 32]);
|
||||
assert_eq!(frame_info(&Codec::Eac3, &es).map(|f| f.samples), Some(1536));
|
||||
// AC-3 frames are never joined
|
||||
assert!(!eac3_joins_unit(&[0x0B, 0x77, 0x40, 0, 0, 8 << 3]));
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed
|
||||
| PCM (WAV, BWF, AIFF, MOV, MXF, Blu-ray / DVD LPCM) | yes | yes, BWF timecode | WAV, AIFF, MOV, MXF | sample-exact | 95% | — |
|
||||
| AAC (LC, HE, LATM) | decode + encode | own decoder (LC, HE core), LATM | own LC encoder | loudness oracle vs ffmpeg | 90% | 3–5 h |
|
||||
| AC-3 | decode (licensed) | own decoder (ATSC A/52) | none | conformance | 80% | — |
|
||||
| **E-AC-3 (Dolby Digital Plus)** | decode | **refused** (bsid 11–16, #647) | none | — | 0% | 15–25 h |
|
||||
| E-AC-3 (Dolby Digital Plus) | decode | own decoder (ATSC A/52 Annex E, #647): independent substream, 1–6 blocks, spectral extension; **no** adaptive hybrid transform, enhanced coupling or 7.1 dependent substreams (plays the 5.1 core) | none | SNR vs ffmpeg 65–91 dB (raw, MP4, MOV, Matroska, TS) | 75% | 10–20 h |
|
||||
| MP3 / MP2 | decode + MP3 encode | decode (symphonia, MPL-2.0; own MP2) | **none** | — | 60% | 10–20 h (MP3 encoder) |
|
||||
| Opus | — (not a Premiere import format) | own decoder, RFC 8251 range-exact | none | conformance vectors | ahead | — |
|
||||
| FLAC, ALAC, Vorbis | partial | decode (symphonia); FLAC in MP4 silent (#603) | none | — | 70% | 3–6 h |
|
||||
|
||||
+9
-6
@@ -149,12 +149,15 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
## G10. Container and codec gaps on import
|
||||
|
||||
- **Missing:** E-AC-3 (#647; our AC-3 decoder refuses bsid 11–16), AVI (#598; listed as an
|
||||
extension but no demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603),
|
||||
camera RAW (R3D incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345,
|
||||
#383), JPEG 2000 / JPEG XS MXF, Cinema DNG; stills: PSD, EXR, DPX, Targa, HEIF / HEIC, Radiance
|
||||
HDR, AI / EPS; transparent video (#402); Sony start timecode in `rtmd` (#460); stereo 3D flag
|
||||
(#500); MKV multi-audio (#601).
|
||||
- **Have:** E-AC-3 (Dolby Digital Plus) in MP4, MOV, Matroska and MPEG-TS (#647): independent
|
||||
substream, 1–6 blocks per syncframe, spectral extension.
|
||||
- **Missing:** E-AC-3 adaptive hybrid transform, enhanced coupling and the extra channels of 7.1
|
||||
dependent substreams (played as the 5.1 core), AVI (#598; listed as an extension but no
|
||||
demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D
|
||||
incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345, #383), JPEG 2000 /
|
||||
JPEG XS MXF, Cinema DNG; stills: PSD, EXR, DPX, Targa, HEIF / HEIC, Radiance HDR, AI / EPS;
|
||||
transparent video (#402); Sony start timecode in `rtmd` (#460); stereo 3D flag (#500); MKV
|
||||
multi-audio (#601).
|
||||
- **Estimate:** 120–200 h (camera RAW alone 60–100 h, and some SDKs have licences we cannot use:
|
||||
clean-room from public specs only where specs exist). Docs: [codec-parity.md](codec-parity.md),
|
||||
[file-format-parity.md](file-format-parity.md).
|
||||
|
||||
Reference in new issue
Block a user