163 lines
6.6 KiB
Rust
163 lines
6.6 KiB
Rust
//! RFC 6716 / RFC 8251 conformance vectors.
|
|
//!
|
|
//! The official vectors (opus_testvectors-rfc8251) are test data downloaded to
|
|
//! `target/fixtures/opus/vectors/` (never committed). The test is skipped when they are absent.
|
|
//! For each vector we check (1) the final range-coder state after every packet against the value
|
|
//! stored in the `.bit` file (bit-exact parsing), and (2) the decoded PCM against the reference
|
|
//! `.dec` output using our implementation of the opus_compare-style quality metric
|
|
//! (see `common::opus_quality`), for stereo (`.dec`) and mono (`m.dec`) output at 48 kHz.
|
|
|
|
mod common;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use filmcraft_opus::StreamDecoder;
|
|
|
|
fn vectors_dir() -> Option<PathBuf> {
|
|
let d = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/opus/vectors");
|
|
d.join("testvector01.bit").exists().then_some(d)
|
|
}
|
|
|
|
fn read_bit(path: &PathBuf) -> Vec<(Vec<u8>, u32)> {
|
|
let data = std::fs::read(path).unwrap();
|
|
let mut out = Vec::new();
|
|
let mut p = 0;
|
|
while p + 8 <= data.len() {
|
|
let len = u32::from_be_bytes(data[p..p + 4].try_into().unwrap()) as usize;
|
|
let rng = u32::from_be_bytes(data[p + 4..p + 8].try_into().unwrap());
|
|
p += 8;
|
|
if p + len > data.len() {
|
|
break;
|
|
}
|
|
out.push((data[p..p + len].to_vec(), rng));
|
|
p += len;
|
|
}
|
|
out
|
|
}
|
|
|
|
fn read_pcm(path: &PathBuf) -> Vec<f32> {
|
|
std::fs::read(path).unwrap().chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0).collect()
|
|
}
|
|
|
|
/// Decodes a vector; returns (interleaved output, index of first range mismatch).
|
|
fn decode_vector(packets: &[(Vec<u8>, u32)], rate: u32, channels: usize, no_inv: bool) -> (Vec<f32>, Option<usize>) {
|
|
let mut dec = StreamDecoder::new(rate, channels).unwrap();
|
|
if no_inv {
|
|
dec.set_phase_inversion_disabled(true);
|
|
}
|
|
let mut out = Vec::new();
|
|
let mut mismatch = None;
|
|
for (i, (pkt, rng)) in packets.iter().enumerate() {
|
|
let r = if pkt.is_empty() { dec.decode_interleaved(None, &mut out) } else { dec.decode_interleaved(Some(pkt), &mut out) };
|
|
if r.is_err() {
|
|
mismatch.get_or_insert(i);
|
|
continue;
|
|
}
|
|
if !pkt.is_empty() && dec.final_range() != *rng {
|
|
mismatch.get_or_insert(i);
|
|
}
|
|
}
|
|
(out, mismatch)
|
|
}
|
|
|
|
#[test]
|
|
fn rfc8251_test_vectors() {
|
|
let Some(dir) = vectors_dir() else {
|
|
eprintln!("opus test vectors not found; skipping");
|
|
return;
|
|
};
|
|
let mut failures = Vec::new();
|
|
for v in 1..=12 {
|
|
let name = format!("testvector{v:02}");
|
|
let packets = read_bit(&dir.join(format!("{name}.bit")));
|
|
for (channels, suffix) in [(2usize, ""), (2usize, "m"), (1usize, "m")] {
|
|
let (out, mismatch) = decode_vector(&packets, 48000, channels, suffix == "m");
|
|
let mut reference = read_pcm(&dir.join(format!("{name}{suffix}.dec")));
|
|
if channels == 1 {
|
|
// Mono output is compared against the downmix of the phase-inversion-free reference.
|
|
reference = reference.chunks_exact(2).map(|c| 0.5 * (c[0] + c[1])).collect();
|
|
}
|
|
let q = common::opus_quality(&reference, &out, channels, 48000);
|
|
let snr = common::snr_db(&reference, &out);
|
|
let pass = mismatch.is_none() && q >= 0.0;
|
|
eprintln!(
|
|
"{name} {}: range {} quality {q:6.1} snr {snr:6.1} dB len {} / ref {} -> {}",
|
|
match (channels, suffix) {
|
|
(2, "") => "stereo ",
|
|
(2, _) => "stereo-m",
|
|
_ => "mono ",
|
|
},
|
|
mismatch.map_or("ok".to_string(), |i| format!("MISMATCH@{i}")),
|
|
out.len(),
|
|
reference.len(),
|
|
if pass { "PASS" } else { "FAIL" }
|
|
);
|
|
if !pass {
|
|
failures.push(format!("{name}{suffix} ({channels} ch)"));
|
|
}
|
|
}
|
|
}
|
|
assert!(failures.is_empty(), "failing vectors: {failures:?}");
|
|
}
|
|
|
|
/// Sanity check of the quality metric calibration: additive white noise at 48 dB SNR sits at the
|
|
/// pass threshold (quality ~0), at 70 dB it is clearly above it.
|
|
#[test]
|
|
fn quality_metric_calibration() {
|
|
let Some(dir) = vectors_dir() else {
|
|
return;
|
|
};
|
|
let reference = read_pcm(&dir.join("testvector01.dec"));
|
|
let reference = &reference[..reference.len().min(48000 * 2 * 20)];
|
|
let p: f64 = reference.iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / reference.len() as f64;
|
|
for (snr, lo, hi) in [(48.0f64, -15.0, 15.0), (70.0, 60.0, 101.0)] {
|
|
let amp = (p / 10f64.powf(snr / 10.0) * 3.0).sqrt();
|
|
let mut seed = 1u32;
|
|
let noisy: Vec<f32> = reference
|
|
.iter()
|
|
.map(|v| {
|
|
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
|
v + (((seed >> 8) as f64 / (1u64 << 24) as f64 * 2.0 - 1.0) * amp) as f32
|
|
})
|
|
.collect();
|
|
let q = common::opus_quality(reference, &noisy, 2, 48000);
|
|
eprintln!("white noise at {snr} dB SNR: quality {q:.1}");
|
|
assert!(q > lo && q < hi, "{snr} dB -> {q}");
|
|
}
|
|
}
|
|
|
|
/// Diagnostic (`VEC=n cargo test ... -- --ignored`): per-second SNR/quality with packet modes.
|
|
#[test]
|
|
#[ignore]
|
|
fn vector_diag() {
|
|
let Some(dir) = vectors_dir() else {
|
|
return;
|
|
};
|
|
let v: usize = std::env::var("VEC").ok().and_then(|s| s.parse().ok()).unwrap_or(2);
|
|
let name = format!("testvector{v:02}");
|
|
let packets = read_bit(&dir.join(format!("{name}.bit")));
|
|
let mut dec = StreamDecoder::new(48000, 2).unwrap();
|
|
let mut out = Vec::new();
|
|
let mut lines = Vec::new();
|
|
for (pkt, _) in &packets {
|
|
let start = out.len() / 2;
|
|
if pkt.is_empty() {
|
|
dec.decode_interleaved(None, &mut out).unwrap();
|
|
lines.push((start, "LOST".to_string()));
|
|
} else {
|
|
let p = filmcraft_opus::Packet::parse(pkt).unwrap();
|
|
dec.decode_interleaved(Some(pkt), &mut out).unwrap();
|
|
lines.push((start, format!("{:?} {:?} st{} n{}", p.toc.mode, p.toc.bandwidth, p.toc.stereo as u8, p.frames.len())));
|
|
}
|
|
}
|
|
let reference = read_pcm(&dir.join(format!("{name}.dec")));
|
|
let sec = 48000 * 2;
|
|
for s in 0..reference.len() / sec {
|
|
let r = &reference[s * sec..(s + 1) * sec];
|
|
let o = &out[s * sec..(s + 1) * sec];
|
|
let mut kinds: Vec<&String> = lines.iter().filter(|(st, _)| *st >= s * 48000 && *st < (s + 1) * 48000).map(|(_, c)| c).collect();
|
|
kinds.dedup();
|
|
eprintln!("{s:3}s snr {:6.1} q {:6.1} {:?}", common::snr_db(r, o), common::opus_quality(r, o, 2, 48000), kinds);
|
|
}
|
|
}
|