Two root causes, both found by building an oracle rather than guessing. 1. Amplitude ~75x low: the IMDCT applied a 2/n normalisation the encoder's forward transform had already carried. Because 2/n varies with block size it produced DIFFERENT errors on 256- vs 2048-sample blocks — exactly the reported symptom. Removing it gives fit scale 1.0000. 2. Leading trim, the real remaining defect. The first audio packet produces NO output (its window only primes the overlap-add) but we emitted from the first block's centre, injecting half a priming window of garbage and shifting everything early. And Vorbis carries encoder delay in the GRANULE POSITION, which varies per file — afinfo confirms 128 / 1103 / 960 frames on three samples — while our Ogg reader kept only last_granule and discarded per-page granules, making it unrecoverable. Added per-page granule tracking: the first page reporting a granule pins priming as centre - granule, and valid audio starts at priming + blocksize_0/2. That reproduces afinfo's numbers exactly on all three. A premise in the brief was also wrong and worth recording: our output length was already correct. afinfo reports valid frames matching OUR output — it is afconvert that trims a further 128. The reference WAV was short, not us. mono 47 files mean corr 1.00000 (min 1.00000) 47/47 exact stereo 107 files mean corr 0.826 68/107 exact Decode cost 5.13 ms/file average; 11.5 MB compressed expands to 143.3 MB of f32 PCM, which is why the sample bank's LRU cap matters. Honest remaining defect: ~39 stereo files decode wrongly and it is NOT alignment — a full lag sweep peaks at 0.40-0.89 with fit scales 0.40-1.87, so specific blocks have wrong amplitude. Mono being 47/47 rules out floor, residue 0/1, MDCT, windowing and priming; coupling matches the spec's square-polar mapping including reverse order; floor 0 is rejected rather than mis-decoded; and both channels are identical in the failing files, so it is not a swap. The failing set is transient-heavy impact/footstep sounds, so the lead is residue type 2 partition counting on short blocks. reference_decode.rs is no longer #[ignore]d: 3 real tests asserting mono correlation > 0.999 and length == granule, plus a 3000-mutation fuzz that must never panic, all skipping cleanly without fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.7 KiB
Rust
90 lines
3.7 KiB
Rust
//! Diagnostic: compare our Vorbis decode against a reference WAV.
|
|
//! Usage: vorbis_probe <file.ogg> <reference.wav>
|
|
use makepad_game_audio as audio;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let ogg = std::fs::read(&args[1]).expect("read ogg");
|
|
let refwav = std::fs::read(&args[2]).expect("read ref");
|
|
let got = audio::decode(&ogg).expect("decode ogg");
|
|
let want = audio::wav::decode(&refwav).expect("decode ref");
|
|
println!(
|
|
"got {}ch {}Hz {} frames | ref {}ch {}Hz {} frames",
|
|
got.channels,
|
|
got.sample_rate,
|
|
got.frames(),
|
|
want.channels,
|
|
want.sample_rate,
|
|
want.frames()
|
|
);
|
|
|
|
let g = &got.samples;
|
|
let w = &want.samples;
|
|
let rms = |v: &[f32]| (v.iter().map(|x| (*x as f64).powi(2)).sum::<f64>() / v.len() as f64).sqrt();
|
|
println!("rms got={:.6e} ref={:.6e} ratio(ref/got)={:.3}", rms(g), rms(w), rms(w) / rms(g));
|
|
println!(
|
|
"peak got={:.6e} ref={:.6e}",
|
|
g.iter().fold(0f32, |a, b| a.max(b.abs())),
|
|
w.iter().fold(0f32, |a, b| a.max(b.abs()))
|
|
);
|
|
|
|
// Best correlation over a lag sweep, plus the scale that best fits there.
|
|
let n = g.len().min(w.len());
|
|
let mut best = (0i64, 0f64, 0f64);
|
|
for lag in -2000i64..2000 {
|
|
let (mut num, mut dg, mut dw) = (0f64, 0f64, 0f64);
|
|
for i in 0..n {
|
|
let j = i as i64 + lag;
|
|
if j < 0 || j as usize >= n {
|
|
continue;
|
|
}
|
|
let (a, b) = (g[i] as f64, w[j as usize] as f64);
|
|
num += a * b;
|
|
dg += a * a;
|
|
dw += b * b;
|
|
}
|
|
if dg > 0.0 && dw > 0.0 {
|
|
let c = num / (dg.sqrt() * dw.sqrt());
|
|
if c.abs() > best.1.abs() {
|
|
best = (lag, c, num / dg.max(1e-30));
|
|
}
|
|
}
|
|
}
|
|
println!("best corr {:.4} at lag {} (fit scale ref=got*{:.4})", best.1, best.0, best.2);
|
|
|
|
for &l in &[-1088i64,-1024,-901,-773,-640,-576,-512,-256,-128,0,128] {
|
|
let (mut num, mut dg, mut dw) = (0f64,0f64,0f64);
|
|
for i in 0..n { let j=i as i64+l; if j<0||j as usize>=n {continue;}
|
|
let (a,b)=(g[i] as f64, w[j as usize] as f64); num+=a*b; dg+=a*a; dw+=b*b; }
|
|
if dg>0.0&&dw>0.0 { println!(" lag {:>6}: corr {:.4}", l, num/(dg.sqrt()*dw.sqrt())); }
|
|
}
|
|
let on = |v: &[f32], thr: f32| v.iter().position(|s| s.abs() > thr);
|
|
let (pg, pw) = (g.iter().fold(0f32,|a,b| a.max(b.abs())), w.iter().fold(0f32,|a,b| a.max(b.abs())));
|
|
println!("onset(1% of peak) got={:?} ref={:?}", on(g, pg*0.01), on(w, pw*0.01));
|
|
println!("first 6 got: {:?}", &g[..6.min(g.len())]);
|
|
println!("first 6 ref: {:?}", &w[..6.min(w.len())]);
|
|
if g.len() > 780 { println!("got[773..779]: {:?}", &g[773..779]); }
|
|
println!("tail 4 got: {:?}", &g[g.len().saturating_sub(4)..]);
|
|
println!("tail 4 ref: {:?}", &w[w.len().saturating_sub(4)..]);
|
|
|
|
// Per-window scale, to expose block-size dependence.
|
|
let win = 512;
|
|
let mut ratios = Vec::new();
|
|
for k in 0..(n / win) {
|
|
let (a, b) = (&g[k * win..(k + 1) * win], &w[k * win..(k + 1) * win]);
|
|
let (ra, rb) = (rms(a), rms(b));
|
|
if ra > 1e-9 && rb > 1e-9 {
|
|
ratios.push((k, rb / ra));
|
|
}
|
|
}
|
|
let show: Vec<String> = ratios.iter().take(14).map(|(k, r)| format!("{k}:{r:.1}")).collect();
|
|
println!("per-{win} ratios(ref/got): {}", show.join(" "));
|
|
if !ratios.is_empty() {
|
|
let mut v: Vec<f64> = ratios.iter().map(|(_, r)| *r).collect();
|
|
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
println!("ratio median={:.2} min={:.2} max={:.2}", v[v.len() / 2], v[0], v[v.len() - 1]);
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn onset(v: &[f32], thr: f32) -> Option<usize> { v.iter().position(|s| s.abs() > thr) }
|