filmcraft/crates/flac/tests/ffmpeg_oracle.rs

204 lines
8.8 KiB
Rust

//! ffmpeg as an external decoder oracle (skipped when absent): every stream must decode without an
//! error to exactly the samples that went in, with the STREAMINFO MD5 matching ffmpeg's hash of the
//! decoded audio.
use std::path::{Path, PathBuf};
use std::process::Command;
use filmcraft_flac::{Encoder, EncoderConfig, Error, STREAMINFO_OFFSET};
fn ffmpeg() -> Option<PathBuf> {
filmcraft_testkit::ffmpeg_or_skip("flac oracle")
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
}
}
/// Test signals at `bps`, full scale: music-like (two sines and noise), a sweep, white noise,
/// a full-scale square, silence and a lone click.
fn signal(kind: &str, n: usize, bps: u8, seed: u64) -> Vec<i32> {
let max = ((1i64 << (bps - 1)) - 1) as f64;
let mut r = Rng(seed | 1);
(0..n)
.map(|i| {
let t = i as f64 / 48_000.0;
let v = match kind {
"music" => 0.5 * (2.0 * std::f64::consts::PI * 220.0 * t).sin() + 0.3 * (2.0 * std::f64::consts::PI * 1375.0 * t).sin() + 0.05 * r.next(),
"sweep" => (2.0 * std::f64::consts::PI * (40.0 + 4000.0 * t) * t).sin() * 0.9,
"noise" => r.next(),
"square" => {
if (i / 37) % 2 == 0 {
1.0
} else {
-1.0
}
}
"click" => f64::from(u8::from(i == n / 2)),
_ => 0.0,
};
((v * max).round() as i64).clamp(-(max as i64) - 1, max as i64) as i32
})
.collect()
}
fn encode(planar: &[Vec<i32>], cfg: EncoderConfig, chunk: usize) -> Result<Vec<u8>, Error> {
let mut enc = Encoder::new(cfg)?;
let mut file = enc.header();
let n = planar.first().map_or(0, Vec::len);
let mut at = 0;
while at < n {
let end = (at + chunk).min(n);
let parts: Vec<&[i32]> = planar.iter().map(|c| &c[at..end]).collect();
file.extend(enc.encode(&parts)?);
at = end;
}
file.extend(enc.finish());
let o = STREAMINFO_OFFSET as usize;
file[o..o + 34].copy_from_slice(&enc.streaminfo());
Ok(file)
}
fn run(ffmpeg: &Path, args: &[&str], input: &Path) -> (Vec<u8>, String) {
let out = Command::new(ffmpeg).args(["-v", "error", "-i"]).arg(input).args(args).output().unwrap();
assert!(out.status.success(), "ffmpeg failed: {}", String::from_utf8_lossy(&out.stderr));
(out.stdout, String::from_utf8_lossy(&out.stderr).into_owned())
}
/// Encode, decode with ffmpeg, compare every sample and the MD5; returns the file size.
fn round_trip(name: &str, planar: &[Vec<i32>], cfg: EncoderConfig, chunk: usize) -> Option<usize> {
let ffmpeg = ffmpeg()?;
let bps = cfg.bits_per_sample;
let rate = cfg.sample_rate;
let file = encode(planar, cfg, chunk).unwrap();
let path = filmcraft_testkit::fixtures_dir("flac").join(format!("{name}.flac"));
std::fs::write(&path, &file).unwrap();
// samples: s32 holds each one shifted to the top bits
let (raw, err) = run(&ffmpeg, &["-f", "s32le", "-c:a", "pcm_s32le", "-"], &path);
assert!(err.is_empty(), "{name}: decoder errors: {err}");
let decoded: Vec<i32> = raw.as_chunks::<4>().0.iter().map(|b| i32::from_le_bytes(*b) >> (32 - bps)).collect();
let n = planar[0].len();
assert_eq!(decoded.len(), n * planar.len(), "{name}: sample count");
for (i, frame) in decoded.chunks_exact(planar.len()).enumerate() {
for (c, &v) in frame.iter().enumerate() {
assert_eq!(v, planar[c][i], "{name}: channel {c} sample {i}");
}
}
// STREAMINFO: total samples, rate, and the MD5 against ffmpeg's hash of the decoded audio. FLAC
// hashes samples unscaled; ffmpeg rescales other sizes (20-bit becomes 24-bit), so only 8, 16,
// 24 and 32 bit compare
let si = &file[STREAMINFO_OFFSET as usize..STREAMINFO_OFFSET as usize + 34];
let pcm = match bps {
8 => Some("s8"),
16 => Some("s16le"),
24 => Some("s24le"),
32 => Some("s32le"),
_ => None,
};
if let Some(pcm) = pcm {
let (hash, _) = run(&ffmpeg, &["-c:a", &format!("pcm_{pcm}"), "-f", "hash", "-hash", "md5", "-"], &path);
let hash = String::from_utf8_lossy(&hash);
let ours: String = si[18..34].iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(hash.trim().strip_prefix("MD5=").unwrap_or(""), ours, "{name}: MD5");
}
let total = (u64::from(si[13] & 0x0F) << 32) | u64::from(u32::from_be_bytes([si[14], si[15], si[16], si[17]]));
assert_eq!(total, n as u64, "{name}: total samples");
let si_rate = (u32::from(si[10]) << 12) | (u32::from(si[11]) << 4) | (u32::from(si[12]) >> 4);
assert_eq!(si_rate, rate);
Some(file.len())
}
#[test]
fn every_signal_round_trips_exactly() {
for (kind, seed) in [("music", 1), ("sweep", 2), ("noise", 3), ("square", 4), ("silence", 5), ("click", 6)] {
for bps in [16u8, 24, 32] {
let planar = vec![signal(kind, 20_000, bps, seed), signal(kind, 20_000, bps, seed + 100)];
if round_trip(&format!("{kind}-{bps}"), &planar, EncoderConfig::new(48_000, 2, bps), 4800).is_none() {
return;
}
}
}
}
#[test]
fn channel_layouts_rates_and_sizes() {
let cases: [(&str, u8, u8, u32, usize); 8] = [
("mono-8bit", 1, 8, 8_000, 3_001),
("mono-24-44k1", 1, 24, 44_100, 9_999),
("stereo-20bit", 2, 20, 96_000, 10_000),
("5.1-24", 6, 24, 48_000, 12_345),
("mono-32", 1, 32, 48_000, 7_777),
("5.1-32", 6, 32, 96_000, 12_345),
("odd-rate", 2, 16, 37_800, 5_000),
("tiny", 2, 16, 48_000, 1),
];
for (name, ch, bps, rate, n) in cases {
let planar: Vec<Vec<i32>> = (0..ch).map(|c| signal(if c == 3 { "silence" } else { "music" }, n, bps, u64::from(c) + 7)).collect();
if round_trip(name, &planar, EncoderConfig::new(rate, ch, bps), 1000).is_none() {
return;
}
}
}
#[test]
fn levels_and_block_sizes_trade_time_for_size() {
let planar = vec![signal("music", 48_000, 16, 11), signal("sweep", 48_000, 16, 12)];
let mut sizes = Vec::new();
for level in 0..=8 {
let cfg = EncoderConfig { level, ..EncoderConfig::new(48_000, 2, 16) };
let Some(size) = round_trip(&format!("level{level}"), &planar, cfg, 48_000) else { return };
sizes.push(size);
}
// the effortful levels are never larger than the fastest, and all beat PCM
assert!(sizes[8] <= sizes[0] && sizes[5] <= sizes[0], "{sizes:?}");
assert!(sizes.iter().all(|&s| s < 48_000 * 4), "{sizes:?}");
for bs in [16u16, 192, 1152, 4608, 65_535] {
let cfg = EncoderConfig { block_size: bs, ..EncoderConfig::new(48_000, 2, 16) };
round_trip(&format!("block{bs}"), &planar, cfg, 777);
}
}
#[test]
fn bad_input_is_an_error_not_a_panic() {
assert!(Encoder::new(EncoderConfig::new(48_000, 0, 16)).is_err());
assert!(Encoder::new(EncoderConfig::new(48_000, 9, 16)).is_err());
assert!(Encoder::new(EncoderConfig::new(48_000, 2, 33)).is_err());
assert!(Encoder::new(EncoderConfig::new(48_000, 2, 7)).is_err());
assert!(Encoder::new(EncoderConfig::new(0, 2, 16)).is_err());
assert!(Encoder::new(EncoderConfig { block_size: 8, ..EncoderConfig::new(48_000, 2, 16) }).is_err());
let mut enc = Encoder::new(EncoderConfig::new(48_000, 2, 16)).unwrap();
assert_eq!(enc.encode(&[&[0, 1]]), Err(Error::Channels { expected: 2, got: 1 }));
assert_eq!(enc.encode(&[&[0, 1], &[0]]), Err(Error::UnequalChannels));
assert_eq!(enc.encode(&[&[40_000], &[0]]), Err(Error::SampleRange(40_000)));
assert_eq!(enc.encode(&[&[-32_769], &[0]]), Err(Error::SampleRange(-32_769)));
// nothing encoded: no frames, and a header that still says "unknown"
assert!(enc.finish().is_empty());
assert_eq!(&enc.header()[..4], b"fLaC");
}
/// 32-bit stereo at the extremes of i32: left minus right would need 33 bits, so the encoder must
/// not code a side channel (it overflowed `i32` and wrote garbage); every level decodes exactly.
#[test]
fn full_scale_32_bit_stereo_round_trips() {
let n = 9_000;
let left: Vec<i32> = (0..n).map(|i| if i % 3 == 0 { i32::MAX } else { i32::MIN }).collect();
let right: Vec<i32> = left.iter().map(|&v| if v == i32::MAX { i32::MIN } else { i32::MAX }).collect();
let planar = vec![left, right];
for level in [0u8, 5, 8] {
let cfg = EncoderConfig { level, ..EncoderConfig::new(48_000, 2, 32) };
if round_trip(&format!("extremes-32-level{level}"), &planar, cfg, 4096).is_none() {
return;
}
}
let mut enc = Encoder::new(EncoderConfig::new(48_000, 2, 32)).unwrap();
assert!(enc.encode(&[&[i32::MIN, i32::MAX], &[0, -1]]).is_ok());
}