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:
parent
2078cc0e22
commit
78c263a238
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||
];
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue