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>
78 lines
2.9 KiB
Rust
78 lines
2.9 KiB
Rust
//! Correlate block geometry against the true start offset, so the lapping
|
|
//! rule is derived from data instead of guessed.
|
|
//! Usage: vorbis_align <dir> <tmp> [limit]
|
|
use makepad_game_audio as audio;
|
|
use std::process::Command;
|
|
|
|
fn best_lag(g: &[f32], w: &[f32], ch: usize) -> (i64, f64) {
|
|
let n = g.len().min(w.len());
|
|
let mut best = (0i64, 0f64);
|
|
// Lags are whole frames; step by channel count so stereo isn't tested
|
|
// at half-frame offsets that can never be right.
|
|
let step = ch as i64;
|
|
let mut lag = -8192i64;
|
|
while lag <= 8192 {
|
|
let (mut num, mut dg, mut dw) = (0f64, 0f64, 0f64);
|
|
let mut i = 0usize;
|
|
while i < n {
|
|
let j = i as i64 + lag;
|
|
if j >= 0 && (j as usize) < n {
|
|
let (a, b) = (g[i] as f64, w[j as usize] as f64);
|
|
num += a * b;
|
|
dg += a * a;
|
|
dw += b * b;
|
|
}
|
|
i += 16; // sparse sample: enough to find the peak, 16x faster
|
|
}
|
|
if dg > 0.0 && dw > 0.0 {
|
|
let c = num / (dg.sqrt() * dw.sqrt());
|
|
if c > best.1 {
|
|
best = (lag, c);
|
|
}
|
|
}
|
|
lag += step;
|
|
}
|
|
best
|
|
}
|
|
|
|
fn main() {
|
|
let a: Vec<String> = std::env::args().collect();
|
|
let (dir, tmp) = (&a[1], &a[2]);
|
|
let limit: usize = a.get(3).and_then(|s| s.parse().ok()).unwrap_or(20);
|
|
let mut files = walk(dir);
|
|
files.sort();
|
|
println!("{:>4} {:>5} {:>5} {:>8} {:>8} {:>7} {:>7} file", "ch", "n0", "n1", "granule", "reflen", "lagfrm", "corr");
|
|
let mut done = 0;
|
|
for f in files.iter() {
|
|
if done >= limit { break }
|
|
let refwav = format!("{tmp}/align_ref.wav");
|
|
let _ = std::fs::remove_file(&refwav);
|
|
if !matches!(Command::new("afconvert").args(["-f","WAVE","-d","LEF32",f,&refwav]).status(), Ok(s) if s.success()) { continue }
|
|
let (Ok(o), Ok(wv)) = (std::fs::read(f), std::fs::read(&refwav)) else { continue };
|
|
let Ok(got) = audio::decode(&o) else { continue };
|
|
let Ok(want) = audio::wav::decode(&wv) else { continue };
|
|
done += 1;
|
|
let (lag, c) = best_lag(&got.samples, &want.samples, got.channels);
|
|
let bs = audio::debug_block_sizes(&o).unwrap_or_default();
|
|
let name = f.rsplit('/').next().unwrap_or(f);
|
|
println!("{:>4} {:>5} {:>5} {:>8} {:>8} {:>7} {:>7.4} {}",
|
|
got.channels,
|
|
bs.first().copied().unwrap_or(0),
|
|
bs.get(1).copied().unwrap_or(0),
|
|
got.frames(),
|
|
want.frames(),
|
|
-lag / got.channels as i64,
|
|
c, name);
|
|
}
|
|
}
|
|
|
|
fn walk(dir: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
let Ok(rd) = std::fs::read_dir(dir) else { return out };
|
|
for e in rd.flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() { out.extend(walk(&p.to_string_lossy())) }
|
|
else if p.extension().map(|x| x == "ogg").unwrap_or(false) { out.push(p.to_string_lossy().into_owned()) }
|
|
}
|
|
out
|
|
}
|