makepad/libs/audio_encode/tests/alignment.rs
Admin 016a171a35 libs/audio_*: MP3, Vorbis and Ogg of our own, plus lyric alignment and audio imaging
Another dependency the app should not be asking the platform for:

  - audio_decode — MP3 (layer 3, LSF tables, synthesis) and Ogg Vorbis
    (codebooks, floor, residue, MDCT) decoders, with tag reading. Both are
    checked against oracle fixtures rather than against our own expectations.
  - audio_encode — an Ogg Vorbis encoder: MDCT, psychoacoustics, floor and
    Huffman coding, setup tables, plus `oggenc` and `audiobench` binaries.
  - audio_picture — waveform and spectrogram rendering, and compositing.
  - audio_lyrics — word-level lyric alignment (DTW plus a DP snap) and the
    baked schema behind karaoke timing.
  - audio_sidechannels — the side-channel plumbing between them.

libs/voice grows a CUDA backend and an alignment path beside its CPU decoder,
with a `whisper_parity` binary to keep the two honest.
2026-08-23 01:34:35 +02:00

81 lines
3.1 KiB
Rust

//! Alignment canary: the whole-chain round trip must land at shift ZERO.
//!
//! This is the test that caught the single-page granule bug (the decoder
//! read end-truncation as encoder delay and the whole stream came back 376
//! samples early). It cross-correlates the decoded signal against the
//! input over a wide window and asserts the best alignment is exactly 0.
use makepad_audio_decode::vorbis;
use makepad_audio_encode::{encode_vorbis, EncodeOptions};
fn tone_mix(rate: u32, secs: f64) -> Vec<f32> {
let frames = (rate as f64 * secs) as usize;
(0..frames)
.map(|f| {
let t = f as f64 / rate as f64;
let bass = (2.0 * std::f64::consts::PI * 110.0 * t).sin() * 0.30;
let mid = (2.0 * std::f64::consts::PI * 880.0 * t).sin() * 0.20;
let high = (2.0 * std::f64::consts::PI * 6100.0 * t).sin() * 0.05;
(bass + mid + high) as f32
})
.collect()
}
#[test]
fn error_profile_by_quality() {
let rate = 44_100u32;
let pcm = tone_mix(rate, 2.0);
for q in [0.1f32, 0.5, 0.9] {
let ogg =
encode_vorbis(rate, 1, &pcm, &EncodeOptions { quality: q, ..Default::default() })
.unwrap();
let dec = vorbis::decode_all(&ogg).unwrap();
let got = &dec.pcm_interleaved_f32;
assert_eq!(got.len(), pcm.len());
// SNR over windows: find the worst second.
let win = rate as usize / 10;
let mut worst = f64::INFINITY;
let mut worst_at = 0usize;
for (i, (r, g)) in pcm.chunks(win).zip(got.chunks(win)).enumerate() {
let s: f64 = r.iter().map(|&v| v as f64 * v as f64).sum();
let n: f64 =
r.iter().zip(g).map(|(&a, &b)| (a as f64 - b as f64).powi(2)).sum();
if n > 0.0 && s > 1e-9 {
let snr = 10.0 * (s / n).log10();
if snr < worst {
worst = snr;
worst_at = i;
}
}
}
let s: f64 = pcm.iter().map(|&v| v as f64 * v as f64).sum();
let n: f64 =
pcm.iter().zip(got).map(|(&a, &b)| (a as f64 - b as f64).powi(2)).sum();
// Is the error a constant time shift? Search small offsets.
let mut best = (0isize, f64::NEG_INFINITY);
for off in -1200isize..=1200 {
let mut s2 = 0f64;
let mut n2 = 0f64;
for i in 2000..pcm.len() - 2000 {
let g = got[(i as isize + off) as usize] as f64;
let r = pcm[i] as f64;
s2 += r * r;
n2 += (r - g) * (r - g);
}
let snr = 10.0 * (s2 / n2.max(1e-12)).log10();
if snr > best.1 {
best = (off, snr);
}
}
println!(
"q={q}: {:.0} kbps, snr {:.1} dB, worst window {:.1} dB at {}ms, best-shift {} -> {:.1} dB",
ogg.len() as f64 * 8.0 / 2.0 / 1000.0,
10.0 * (s / n).log10(),
worst,
worst_at * 100,
best.0,
best.1
);
assert_eq!(best.0, 0, "decoded stream is time-shifted by {} samples", best.0);
}
}