M9.15: AC-3 decoder from ATSC A/52 (all modes, coupling, rematrixing, block switching), wired into TS/PS/MP4/MKV

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 04:11:27 -04:00
parent 2078cc0e22
commit 78c263a238
11 changed files with 1425 additions and 4 deletions

9
Cargo.lock generated
View File

@ -1684,6 +1684,14 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-ac3"
version = "0.1.0"
dependencies = [
"filmcraft-testkit",
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-audio-dsp"
version = "0.1.0"
@ -1759,6 +1767,7 @@ name = "filmcraft-codecs"
version = "0.1.0"
dependencies = [
"filmcraft-aac",
"filmcraft-ac3",
"filmcraft-av1",
"filmcraft-bitstream",
"filmcraft-color",

View File

@ -21,6 +21,7 @@ filmcraft-matroska = { path = "crates/matroska" }
filmcraft-mxf = { path = "crates/mxf" }
filmcraft-mpegts = { path = "crates/mpegts" }
filmcraft-mpeg2v = { path = "crates/mpeg2v" }
filmcraft-ac3 = { path = "crates/ac3" }
filmcraft-ogg = { path = "crates/ogg" }
filmcraft-h264 = { path = "crates/h264" }
filmcraft-hevc = { path = "crates/hevc" }

16
crates/ac3/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "filmcraft-ac3"
description = "Clean-room AC-3 (Dolby Digital, ATSC A/52) audio decoder"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
thiserror = { workspace = true }
[dev-dependencies]
filmcraft-testkit = { workspace = true }

1054
crates/ac3/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

69
crates/ac3/src/tables.rs Normal file
View File

@ -0,0 +1,69 @@
// Generated from the tables of ATSC A/52:2012 (Tables 7.12-7.16, 7.33).
pub(crate) const MASKTAB: [u8; 256] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32,
32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 40,
40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
43, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46,
46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49,
49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 0, 0, 0,
];
pub(crate) const LATAB: [u16; 256] = [
64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 52, 51, 50, 49, 48, 47, 47, 46, 45, 44, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 33,
33, 32, 32, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22, 21, 21, 21, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16,
15, 15, 15, 14, 14, 14, 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6,
6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
pub(crate) const BAPTAB: [u8; 64] = [
0, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13,
13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15,
];
pub(crate) const BNDTAB: [u8; 50] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 34, 37, 40, 43, 46, 49, 55, 61, 67, 73, 79,
85, 97, 109, 121, 133, 157, 181, 205, 229,
];
pub(crate) const BNDSZ: [u8; 50] = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 24, 24, 24, 24,
24,
];
pub(crate) const HTH: [[u16; 50]; 3] = [
[
1232, 1232, 1088, 1024, 992, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 880, 880, 864, 864, 848, 848, 832, 832, 816, 800, 784,
768, 752, 752, 752, 752, 768, 784, 832, 912, 992, 1056, 1120, 1168, 1184, 1120, 1088, 1088, 1312, 2048, 2112, 2112,
],
[
1264, 1264, 1120, 1040, 992, 976, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 896, 880, 880, 864, 864, 848, 848, 832, 832, 800,
784, 768, 752, 752, 752, 752, 768, 800, 848, 912, 992, 1056, 1104, 1184, 1168, 1120, 1088, 1152, 1584, 2112, 2112,
],
[
1408, 1408, 1200, 1104, 1056, 1008, 992, 976, 960, 944, 944, 944, 928, 928, 928, 928, 928, 928, 928, 928, 912, 912, 912, 912, 896, 896, 896, 880, 864,
848, 832, 816, 800, 784, 768, 752, 752, 752, 768, 784, 816, 848, 960, 1040, 1136, 1184, 1120, 1088, 1104, 1248,
],
];
/// Table 7.33 as printed (5 decimals), for checking the computed window.
#[cfg(test)]
#[allow(clippy::approx_constant)]
pub(crate) const WINDOW_TABLE: [f32; 256] = [
0.00014, 0.00024, 0.00037, 0.00051, 0.00067, 0.00086, 0.00107, 0.0013, 0.00157, 0.00187, 0.0022, 0.00256, 0.00297, 0.00341, 0.0039, 0.00443, 0.00501,
0.00564, 0.00632, 0.00706, 0.00785, 0.00871, 0.00962, 0.01061, 0.01166, 0.01279, 0.01399, 0.01526, 0.01662, 0.01806, 0.01959, 0.02121, 0.02292, 0.02472,
0.02662, 0.02863, 0.03073, 0.03294, 0.03527, 0.0377, 0.04025, 0.04292, 0.04571, 0.04862, 0.05165, 0.05481, 0.0581, 0.06153, 0.06508, 0.06878, 0.07261,
0.07658, 0.08069, 0.08495, 0.08935, 0.09389, 0.09859, 0.10343, 0.10842, 0.11356, 0.11885, 0.12429, 0.12988, 0.13563, 0.14152, 0.14757, 0.15376, 0.16011,
0.16661, 0.17325, 0.18005, 0.18699, 0.19407, 0.2013, 0.20867, 0.21618, 0.22382, 0.23161, 0.23952, 0.24757, 0.25574, 0.26404, 0.27246, 0.281, 0.28965,
0.29841, 0.30729, 0.31626, 0.32533, 0.3345, 0.34376, 0.35311, 0.36253, 0.37204, 0.38161, 0.39126, 0.40096, 0.41072, 0.42054, 0.4304, 0.4403, 0.45023,
0.4602, 0.47019, 0.4802, 0.49022, 0.50025, 0.51028, 0.52031, 0.53033, 0.54033, 0.55031, 0.56026, 0.57019, 0.58007, 0.58991, 0.5997, 0.60944, 0.61912,
0.62873, 0.63827, 0.64774, 0.65713, 0.66643, 0.67564, 0.68476, 0.69377, 0.70269, 0.7115, 0.72019, 0.72877, 0.73723, 0.74557, 0.75378, 0.76186, 0.76981,
0.77762, 0.7853, 0.79283, 0.80022, 0.80747, 0.81457, 0.82151, 0.82831, 0.83496, 0.84145, 0.84779, 0.85398, 0.86001, 0.86588, 0.8716, 0.87716, 0.88257,
0.88782, 0.89291, 0.89785, 0.90264, 0.90728, 0.91176, 0.9161, 0.92028, 0.92432, 0.92822, 0.93197, 0.93558, 0.93906, 0.9424, 0.9456, 0.94867, 0.95162,
0.95444, 0.95713, 0.95971, 0.96217, 0.96451, 0.96674, 0.96887, 0.97089, 0.97281, 0.97463, 0.97635, 0.97799, 0.97953, 0.98099, 0.98236, 0.98366, 0.98488,
0.98602, 0.9871, 0.98811, 0.98905, 0.98994, 0.99076, 0.99153, 0.99225, 0.99291, 0.99353, 0.99411, 0.99464, 0.99513, 0.99558, 0.996, 0.99639, 0.99674,
0.99706, 0.99736, 0.99763, 0.99788, 0.99811, 0.99831, 0.9985, 0.99867, 0.99882, 0.99895, 0.99908, 0.99919, 0.99929, 0.99938, 0.99946, 0.99953, 0.99959,
0.99965, 0.99969, 0.99974, 0.99978, 0.99981, 0.99984, 0.99986, 0.99988, 0.9999, 0.99992, 0.99993, 0.99994, 0.99995, 0.99996, 0.99997, 0.99998, 0.99998,
0.99998, 0.99999, 0.99999, 0.99999, 0.99999, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
];

98
crates/ac3/src/tests.rs Normal file
View File

@ -0,0 +1,98 @@
use super::*;
#[test]
fn computed_window_matches_table_7_33() {
let w = window();
for (n, (&a, &b)) in w.iter().zip(tables::WINDOW_TABLE.iter()).enumerate() {
assert!((a - b).abs() <= 6e-6, "w[{n}] = {a}, table {b}");
}
// princen-bradley: w[n]² + w[255-n]²... (the second half mirrors): w[n]² + w[511-n]² = 1
for n in 0..256 {
let m = w[255 - n];
assert!((w[n] * w[n] + m * m - 1.0).abs() < 1e-5);
}
}
#[test]
fn tables_are_consistent() {
// masktab maps every bin of a band to that band
for b in 0..50 {
for bin in tables::BNDTAB[b] as usize..(tables::BNDTAB[b] + tables::BNDSZ[b]) as usize {
if bin < 253 {
assert_eq!(tables::MASKTAB[bin] as usize, b, "bin {bin}");
}
}
}
assert_eq!(tables::BNDTAB[49] as usize + tables::BNDSZ[49] as usize, 253);
// latab is non-increasing from 64 to 0
assert_eq!(tables::LATAB[0], 64);
assert!(tables::LATAB.windows(2).all(|w| w[0] >= w[1]));
assert!(tables::BAPTAB.windows(2).all(|w| w[0] <= w[1]));
assert_eq!((tables::BAPTAB[0], tables::BAPTAB[63]), (0, 15));
}
#[test]
fn inverse_fft_matches_the_definition() {
for n in [64usize, 128] {
let re0: Vec<f32> = (0..n).map(|i| ((i * 7 + 3) % 11) as f32 - 5.0).collect();
let im0: Vec<f32> = (0..n).map(|i| ((i * 5 + 1) % 13) as f32 - 6.0).collect();
let (mut re, mut im) = (re0.clone(), im0.clone());
ifft(&mut re, &mut im);
for t in 0..n {
let (mut sr, mut si) = (0f64, 0f64);
for k in 0..n {
let a = 2.0 * std::f64::consts::PI * (k * t) as f64 / n as f64;
sr += re0[k] as f64 * a.cos() - im0[k] as f64 * a.sin();
si += re0[k] as f64 * a.sin() + im0[k] as f64 * a.cos();
}
assert!((sr - re[t] as f64).abs() < 1e-3 && (si - im[t] as f64).abs() < 1e-3, "n {n} t {t}");
}
}
}
#[test]
fn headers_and_gains() {
// 48 kHz, 448 kb/s, 3/2 + LFE: bsid 8, bsmod 0, acmod 7 (cmixlev, surmixlev), lfeon
let b = [0x0B, 0x77, 0, 0, 30, 8 << 3, 0b1110_0001, 0b0100_0000];
let h = parse_header(&b).unwrap();
assert_eq!((h.sample_rate, h.bitrate_kbps, h.frame_bytes, h.acmod, h.lfeon, h.channels()), (48_000, 448, 1792, 7, true, 6));
assert_eq!(parse_header(&[0x0B, 0x77, 0, 0, 0, 16 << 3, 0, 0]), Err(Error::Unsupported("bsid 16 (E-AC-3)".into())));
assert_eq!(dynrng_gain(0), 1.0);
assert!((dynrng_gain(0b1110_0000) - 0.5).abs() < 1e-7);
assert!((dynrng_gain(0b0111_1111) - 16.0 * 63.0 / 64.0).abs() < 1e-5);
}
#[test]
fn channel_order() {
let ch = |n: usize| (0..n).map(|i| vec![i as f32]).collect::<Vec<_>>();
let ids = |v: Vec<Vec<f32>>| v.iter().map(|c| c[0] as usize).collect::<Vec<_>>();
// coded L C R Ls Rs LFE → L R C LFE Ls Rs
assert_eq!(ids(wav_order(ch(6), 7, true)), vec![0, 2, 1, 5, 3, 4]);
assert_eq!(ids(wav_order(ch(3), 2, true)), vec![0, 1, 2]);
assert_eq!(ids(wav_order(ch(1), 1, false)), vec![0]);
}
#[test]
fn garbage_never_panics() {
let mut d = Decoder::new();
let mut x = 0xACu32;
for len in [0usize, 5, 8, 100, 2000] {
for _ in 0..200 {
let mut f: Vec<u8> = (0..len)
.map(|_| {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
x as u8
})
.collect();
if len >= 6 {
f[0] = 0x0B;
f[1] = 0x77;
f[4] &= 0x3F;
f[5] = (f[5] & 7) | (8 << 3);
}
let _ = d.decode(&f);
}
}
}

147
crates/ac3/tests/oracle.rs Normal file
View File

@ -0,0 +1,147 @@
//! 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
//! decoders (zero-bit mantissas are filled with decoder-specific dither, §7.3.4; float vs fixed
//! arithmetic), so the criterion is SNR against ffmpeg's decode.
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
struct Spec {
name: &'static str,
input: &'static str,
rate: u32,
channels: u32,
enc: &'static [&'static str],
/// Minimum SNR (dB) against ffmpeg.
min_snr: f64,
}
const SPECS: &[Spec] = &[
Spec {
name: "ac3_stereo_192k",
input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000",
rate: 48_000,
channels: 2,
enc: &["-b:a", "192k"],
min_snr: 60.0,
},
// low bitrate stereo: coupling and rematrixing
Spec {
name: "ac3_stereo_96k_coupling",
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 },
// 5.1 with LFE
Spec {
name: "ac3_51_448k",
input: "aevalsrc=exprs=0.3*sin(2*PI*300*t)|0.3*sin(2*PI*400*t)|0.3*sin(2*PI*500*t)|0.3*sin(2*PI*60*t)|0.2*sin(2*PI*700*t)|0.2*sin(2*PI*800*t):s=48000:c=5.1",
rate: 48_000,
channels: 6,
enc: &["-b:a", "448k"],
min_snr: 55.0,
},
// noise (wideband, many zero-bit mantissas): the SNR is limited by dither
Spec {
name: "ac3_noise_stereo",
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,
},
];
fn make(ff: &Path, s: &Spec) -> Option<PathBuf> {
let out = filmcraft_testkit::fixtures_dir("ac3").join(format!("{}.ac3", s.name));
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(s.enc)
.args(["-f", "ac3"])
.arg(tmp)
.stdin(Stdio::null())
.status();
matches!(st, Ok(x) if x.success())
})
}
fn frames(data: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut p = 0;
while let Ok(h) = filmcraft_ac3::parse_header(&data[p..]) {
if p + h.frame_bytes > data.len() {
break;
}
out.push(&data[p..p + h.frame_bytes]);
p += h.frame_bytes;
}
out
}
#[test]
fn ffmpeg_encoded_streams_decode() {
let ff = filmcraft_testkit::require_ffmpeg!();
for s in SPECS {
let f = make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name));
let data = std::fs::read(&f).unwrap();
let mut dec = filmcraft_ac3::Decoder::new();
let mut ours: Vec<Vec<f32>> = vec![Vec::new(); s.channels as usize];
for fr in frames(&data) {
let d = dec.decode(fr).unwrap();
assert_eq!((d.header.sample_rate, d.channels.len() as u32), (s.rate, s.channels), "{}", s.name);
for (o, c) in ours.iter_mut().zip(d.channels) {
o.extend(c);
}
}
let o = Command::new(&ff).args(["-v", "error", "-i"]).arg(&f).args(["-f", "f32le", "-"]).output().unwrap();
let want: Vec<f32> = o.stdout.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
let ch = s.channels as usize;
let n = want.len() / ch;
assert_eq!(n, ours[0].len(), "{}: sample count", s.name);
let mut worst = f64::INFINITY;
for c in 0..ch {
let (mut sig, mut err) = (0f64, 0f64);
for i in 0..n {
let (a, b) = (ours[c][i] as f64, want[i * ch + c] as f64);
sig += b * b;
err += (a - b) * (a - b);
}
let snr = 10.0 * (sig / err.max(1e-30)).log10();
worst = worst.min(snr);
}
// the same stream decoded with another dither sequence: how far dither alone moves the
// output (the difference to ffmpeg must be of that order)
let mut dec2 = filmcraft_ac3::Decoder::new();
dec2.set_dither_seed(0xDEAD_BEEF);
let other: Vec<f32> = frames(&data).iter().flat_map(|fr| dec2.decode(fr).unwrap().channels.swap_remove(0)).collect();
let (mut sig, mut err) = (0f64, 0f64);
for i in 0..n {
sig += (ours[0][i] as f64).powi(2);
err += (ours[0][i] as f64 - other[i] as f64).powi(2);
}
let dither_snr = 10.0 * (sig / err.max(1e-30)).log10();
println!(
"{}: {} Hz, {} ch, {} frames, worst channel SNR vs ffmpeg {worst:.1} dB (two dither seeds: {dither_snr:.1} dB)",
s.name,
s.rate,
ch,
frames(&data).len()
);
assert!(worst >= s.min_snr, "{}: SNR {worst:.1} dB < {}", s.name, s.min_snr);
}
}
#[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));
filmcraft_testkit::fixtures::generate_and_report(&format!("ac3/{}", s.name), &[out], || make(&ff, s));
}
}

View File

@ -19,6 +19,7 @@ filmcraft-matroska = { workspace = true }
filmcraft-mxf = { workspace = true }
filmcraft-mpegts = { workspace = true }
filmcraft-mpeg2v = { workspace = true }
filmcraft-ac3 = { workspace = true }
filmcraft-ogg = { workspace = true }
filmcraft-bitstream = { workspace = true }
filmcraft-h264 = { workspace = true }

View File

@ -24,6 +24,8 @@ 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).
Ac3(Box<filmcraft_ac3::Decoder>),
}
/// Opus always decodes at 48 kHz (RFC 7845 §5.1: the input rate in the header is informational).
@ -122,7 +124,7 @@ impl PacketDecoder {
}
/// AC-3 (ATSC A/52).
pub fn ac3() -> Result<Self> {
Err(CodecError::Unsupported("AC-3 audio".into()))
Ok(Self { inner: Inner::Ac3(Box::new(filmcraft_ac3::Decoder::new())), channels: 0 })
}
/// Whether this decodes Opus (which needs [`OPUS_PRE_ROLL`] of pre-roll after a seek).
pub fn is_opus(&self) -> bool {
@ -133,6 +135,7 @@ impl PacketDecoder {
match c {
C::Aac(a) => Self::aac(&a.asc, if a.sample_rate > 0 { a.sample_rate } else { rate }),
C::Mp3 => Self::new(CODEC_TYPE_MP3, rate, None),
C::Ac3 { .. } => 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}")))?),
@ -161,6 +164,24 @@ 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)
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 out.is_empty() {
out = f.channels;
} else {
for (o, c) in out.iter_mut().zip(f.channels) {
o.extend(c);
}
}
}
self.channels = out.len();
return Ok(out);
}
};
let pkt = Packet::new_from_slice(0, ts, 0, data);
let buf = dec.decode(&pkt).map_err(|e| CodecError::Decode(e.to_string()))?;
@ -188,13 +209,14 @@ impl PacketDecoder {
}
Inner::Opus { dec, .. } => dec.reset(),
Inner::Symphonia(d) => d.reset(),
Inner::Ac3(d) => d.reset(),
Inner::AacPending => {}
}
}
}
/// Whether AC-3 audio decodes (our ATSC A/52 decoder).
pub const AC3_DECODER: bool = false;
pub const AC3_DECODER: bool = true;
/// An AudioSpecificConfig for AAC with these parameters (ISO/IEC 14496-3 §1.6.2.1, GA specific
/// config with no extension flags).

View File

@ -335,6 +335,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(),
// 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 => {

View File

@ -350,7 +350,8 @@ fn avchd_ac3_audio() {
let a = src.info().audio.clone().unwrap();
if filmcraft_codecs::audio::AC3_DECODER {
assert_eq!((a.codec.as_str(), a.channels), ("AC-3", 2));
let e = check_audio(&ff, &f, 1e-3);
// AC-3 zero-bit mantissas carry decoder-specific dither (A/52 §7.3.4)
let e = check_audio(&ff, &f, 5e-3);
println!("m2ts_h264_ac3: AC-3 max error {e:.2e}");
} else {
assert_eq!(a.codec, "AC-3 (unsupported)");
@ -396,7 +397,9 @@ fn program_streams() {
// AC-3 in private stream 1
let f = make(&ff, "ps_mpeg2_ac3.vob");
let src = open(&f);
assert!(src.info().audio.as_ref().unwrap().codec.starts_with("AC-3"));
assert_eq!(src.info().audio.as_ref().unwrap().codec, "AC-3");
let e = check_audio(&ff, &f, 5e-3);
println!("ps_mpeg2_ac3: AC-3 max error {e:.2e}");
// MPEG-1 system stream
let f = make(&ff, "mpeg1_system.mpg");
let src = open(&f);