#![cfg(test)]
use super::decoder::MlowDecoder;
const SR: f64 = 16000.0;
fn decode_capture() -> Vec<f32> {
let frames: Vec<String> =
serde_json::from_str(include_str!("testdata/inbound_capture_frames.json"))
.expect("inbound_capture_frames.json");
let mut dec = MlowDecoder::new();
let mut out = Vec::new();
for hex_frame in &frames {
let frame = hex::decode(hex_frame).unwrap();
out.extend_from_slice(&dec.decode(&frame));
}
out
}
fn load_ref() -> Vec<f32> {
let raw = include_bytes!("testdata/ref_usesmpl_expected.raw");
raw.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0)
.collect()
}
fn rms(x: &[f32]) -> f64 {
if x.is_empty() {
return 0.0;
}
let ss: f64 = x.iter().map(|&v| (v as f64) * (v as f64)).sum();
(ss / x.len() as f64).sqrt()
}
fn corr_at_lag(refp: &[f32], out: &[f32], lag: usize) -> f64 {
let n = refp.len().saturating_sub(lag).min(out.len());
if n < 16 {
return 0.0;
}
let r = &refp[lag..lag + n];
let o = &out[..n];
let mr: f64 = r.iter().map(|&v| v as f64).sum::<f64>() / n as f64;
let mo: f64 = o.iter().map(|&v| v as f64).sum::<f64>() / n as f64;
let (mut sxy, mut sxx, mut syy) = (0f64, 0f64, 0f64);
for i in 0..n {
let dr = r[i] as f64 - mr;
let dz = o[i] as f64 - mo;
sxy += dr * dz;
sxx += dr * dr;
syy += dz * dz;
}
if sxx < 1e-12 || syy < 1e-12 {
return 0.0;
}
sxy / (sxx * syy).sqrt()
}
fn best_delay_corr(refp: &[f32], out: &[f32], max_lag: usize) -> (usize, f64) {
let mut best = (0usize, f64::NEG_INFINITY);
for lag in 0..=max_lag {
let c = corr_at_lag(refp, out, lag);
if c > best.1 {
best = (lag, c);
}
}
best
}
fn near_zero_fraction(x: &[f32]) -> f64 {
const W: usize = 960;
let (mut zeros, mut total) = (0usize, 0usize);
for chunk in x.chunks(W) {
if rms(chunk) < 0.003 {
continue;
}
for &v in chunk {
if v.abs() < 1e-4 {
zeros += 1;
}
total += 1;
}
}
if total == 0 {
return 0.0;
}
zeros as f64 / total as f64
}
fn goertzel(x: &[f32], freq_hz: f64) -> f64 {
let w = 2.0 * std::f64::consts::PI * freq_hz / SR;
let c = 2.0 * w.cos();
let (mut s1, mut s2) = (0f64, 0f64);
for &v in x {
let s0 = v as f64 + c * s1 - s2;
s2 = s1;
s1 = s0;
}
s1 * s1 + s2 * s2 - c * s1 * s2
}
fn band_energy(x: &[f32]) -> (f64, f64, f64) {
let (mut lo, mut mid, mut hi) = (0f64, 0f64, 0f64);
let mut f = 100.0;
while f < 7900.0 {
let p = goertzel(x, f);
if f < 700.0 {
lo += p;
} else if f < 4000.0 {
mid += p;
} else {
hi += p;
}
f += 100.0;
}
(lo, mid, hi)
}
fn report(out: &[f32], refp: &[f32]) {
let n = out.len().min(refp.len());
let (lag, c) = best_delay_corr(refp, out, 128);
let (lo, mid, hi) = band_energy(&out[..n]);
let (rlo, rmid, rhi) = band_energy(&refp[..n]);
eprintln!(
"METRICS len={} rms={:.5} ref_rms={:.5} best_lag={} corr={:.4} gap={:.4} \
band(lo/mid/hi)={:.3e}/{:.3e}/{:.3e} ref_band={:.3e}/{:.3e}/{:.3e} \
hi_ratio={:.3} mid_ratio={:.3} lo_ratio={:.3}",
out.len(),
rms(out),
rms(refp),
lag,
c,
near_zero_fraction(out),
lo,
mid,
hi,
rlo,
rmid,
rhi,
hi / rhi.max(1e-30),
mid / rmid.max(1e-30),
lo / rlo.max(1e-30),
);
}
#[test]
#[ignore = "diagnostic: prints metrics, used while iterating on the decoder"]
fn decode_metrics_report() {
let out = decode_capture();
let refp = load_ref();
report(&out, &refp);
}
#[test]
fn decode_matches_ref_usesmpl() {
let out = decode_capture();
let refp = load_ref();
assert_eq!(
out.len(),
refp.len(),
"decode length must match the reference"
);
let (lo, mid, hi) = band_energy(&out);
let (rlo, _rmid, rhi) = band_energy(&refp);
let our_rms = rms(&out);
let ref_rms = rms(&refp);
let gap = near_zero_fraction(&out);
let (_lag, c) = best_delay_corr(&refp, &out, 128);
let hi_ratio = hi / rhi.max(1e-30);
let lo_ratio = lo / rlo.max(1e-30);
assert!(
(0.35..3.0).contains(&hi_ratio),
"hi-band ratio {hi_ratio:.3} out of [0.35, 3.0): out={hi:.3e} ref={rhi:.3e}, mid={mid:.3e}"
);
assert!(
lo_ratio < 2.5,
"low-band ratio {lo_ratio:.3} too boomy: out={lo:.3e} ref={rlo:.3e}"
);
assert!(
gap < 0.32,
"inter-pulse gap fraction {gap:.4} >= 0.32, noise fill missing"
);
assert!(
(ref_rms * 0.4..ref_rms * 2.2).contains(&our_rms),
"rms {our_rms:.4} vs ref {ref_rms:.4} out of range"
);
assert!(
c > 0.55,
"delay-aligned corr {c:.4} <= 0.55, excitation shape still wrong"
);
}