#![cfg(test)]
use super::decoder::MlowDecoder;
use super::encode::MlowEncoder;
fn gen_tone(freq_hz: f64, n: usize, amp: f32) -> Vec<f32> {
(0..n)
.map(|i| {
let t = i as f64 / 16000.0;
(amp as f64 * (2.0 * std::f64::consts::PI * freq_hz * t).cos()) as f32
})
.collect()
}
fn gen_voiced_harmonic(f0_hz: f64, n: usize, amp: f32) -> Vec<f32> {
const N_HARM: usize = 6;
(0..n)
.map(|i| {
let mut s = 0f64;
for k in 1..=N_HARM {
let t = i as f64 / 16000.0;
s += (1.0 / k as f64) * (2.0 * std::f64::consts::PI * k as f64 * f0_hz * t).cos();
}
(amp as f64 * s / 2.0) as f32
})
.collect()
}
fn gen_white_noise(n: usize, amp: f32) -> Vec<f32> {
let mut state = 0x12345678u32;
(0..n)
.map(|_| {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
let v = (state as i32 as f64) / (i32::MAX as f64);
(amp as f64 * v) as f32
})
.collect()
}
fn gen_silence(n: usize) -> Vec<f32> {
vec![0.0f32; n]
}
fn gen_chirp(f_lo: f64, f_hi: f64, n: usize, amp: f32) -> Vec<f32> {
let mut phase = 0.0f64;
(0..n)
.map(|i| {
let f = f_lo + (f_hi - f_lo) * (i as f64 / n as f64);
phase += 2.0 * std::f64::consts::PI * f / 16000.0;
(amp as f64 * phase.cos()) as f32
})
.collect()
}
pub(crate) fn synth_mic_pcm() -> Vec<i16> {
const SR: f64 = 16000.0;
const FRAME: usize = 960;
const SEG: usize = 10;
struct Lcg(u32);
impl Lcg {
fn next_unit(&mut self) -> f64 {
self.0 = self.0.wrapping_mul(1664525).wrapping_add(1013904223);
(self.0 as i32 as f64) / (i32::MAX as f64)
}
}
fn voiced(out: &mut Vec<f32>, f0: f64, amp: f64, noise: f64, lcg: &mut Lcg, t0: usize) {
for i in 0..SEG * FRAME {
let t = (t0 + i) as f64 / SR;
let mut s = 0f64;
for k in 1..=5 {
let fk = k as f64 * f0;
let g = (1.0 / (k as f64).powf(1.3))
* (0.6 + 0.5 * (-((fk - 700.0) / 600.0).powi(2)).exp());
s += g * (2.0 * std::f64::consts::PI * fk * t).cos();
}
let n = lcg.next_unit();
out.push(((amp * s / 2.0) + noise * n) as f32);
}
}
fn noise_block(out: &mut Vec<f32>, amp: f64, lcg: &mut Lcg) {
for _ in 0..SEG * FRAME {
out.push((amp * lcg.next_unit()) as f32);
}
}
fn silence_block(out: &mut Vec<f32>) {
out.resize(out.len() + SEG * FRAME, 0.0);
}
let mut out: Vec<f32> = Vec::with_capacity(11 * SEG * FRAME);
let mut lcg = Lcg(0x1234_5678);
{
let t0 = out.len();
voiced(&mut out, 150.0, 0.30, 0.03, &mut lcg, t0);
}
{
let t0 = out.len();
voiced(&mut out, 130.0, 0.30, 0.03, &mut lcg, t0);
}
noise_block(&mut out, 0.12, &mut lcg);
{
let t0 = out.len();
voiced(&mut out, 150.0, 0.22, 0.04, &mut lcg, t0);
}
{
let t0 = out.len();
voiced(&mut out, 130.0, 0.30, 0.03, &mut lcg, t0);
}
{
let t0 = out.len();
voiced(&mut out, 150.0, 0.30, 0.03, &mut lcg, t0);
}
silence_block(&mut out);
noise_block(&mut out, 0.06, &mut lcg);
{
let t0 = out.len();
voiced(&mut out, 130.0, 0.30, 0.03, &mut lcg, t0);
}
noise_block(&mut out, 0.02, &mut lcg);
{
let t0 = out.len();
voiced(&mut out, 150.0, 0.30, 0.03, &mut lcg, t0);
}
out.iter()
.map(|&v| (v * 32767.0).clamp(-32768.0, 32767.0) as i16)
.collect()
}
fn synth_mic_bytes() -> Vec<u8> {
let pcm = synth_mic_pcm();
let mut bytes = Vec::with_capacity(pcm.len() * 2);
for &s in &pcm {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes
}
#[test]
fn synth_mic_raw_matches_generator() {
let committed = include_bytes!("testdata/synth_mic.raw");
let generated = synth_mic_bytes();
assert_eq!(
generated.len(),
committed.len(),
"synth_mic.raw length drifted: generator {} bytes vs committed {} bytes",
generated.len(),
committed.len()
);
assert!(
generated == committed,
"synth_mic_pcm() no longer matches committed testdata/synth_mic.raw byte-for-byte"
);
}
#[test]
fn regen_synth_mic_raw() {
if std::env::var("MLOW_GEN_SYNTH").as_deref() != Ok("1") {
return;
}
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/voip/mlow/testdata/synth_mic.raw"
);
std::fs::write(path, synth_mic_bytes()).expect("write synth_mic.raw");
eprintln!("wrote {path}");
}
fn corr(a: &[f32], b: &[f32]) -> f64 {
let (mut sxy, mut sxx, mut syy) = (0f64, 0f64, 0f64);
for (&x, &y) in a.iter().zip(b.iter()) {
let (x, y) = (x as f64, y as f64);
sxy += x * y;
sxx += x * x;
syy += y * y;
}
if sxx < 1e-12 || syy < 1e-12 {
return 0.0;
}
sxy / (sxx * syy).sqrt()
}
fn rms(x: &[f32]) -> f64 {
let ss: f64 = x.iter().map(|&v| (v as f64) * (v as f64)).sum();
(ss / x.len() as f64).sqrt()
}
fn goertzel(x: &[f32], freq_hz: f64, sr: 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 normalized_autocorr(x: &[f32], lag: usize) -> f64 {
if lag >= x.len() {
return 0.0;
}
let (mut num, mut e1, mut e2) = (0f64, 0f64, 0f64);
for i in lag..x.len() {
let (a, b) = (x[i] as f64, x[i - lag] as f64);
num += a * b;
e1 += a * a;
e2 += b * b;
}
if e1 < 1e-12 || e2 < 1e-12 {
return 0.0;
}
num / (e1 * e2).sqrt()
}
fn peak_autocorr(x: &[f32], lag_lo: usize, lag_hi: usize) -> f64 {
(lag_lo..=lag_hi)
.map(|lag| normalized_autocorr(x, lag).abs())
.fold(0f64, f64::max)
}
fn round_trip(pcm: &[f32]) -> Vec<f32> {
const FRAME: usize = 960;
let mut enc = MlowEncoder::new();
let mut dec = MlowDecoder::new();
let mut out = Vec::with_capacity(pcm.len());
let mut i = 0;
while i + FRAME <= pcm.len() {
let frame = enc.encode(&pcm[i..i + FRAME]).expect("encode");
let decoded = dec.decode(&frame);
out.extend_from_slice(&decoded);
i += FRAME;
}
out
}
fn envelope(x: &[f32], win: usize) -> Vec<f32> {
(0..x.len() / win)
.map(|i| {
let s: f32 = x[i * win..(i + 1) * win].iter().map(|&v| v * v).sum();
(s / win as f32).sqrt()
})
.collect()
}
#[test]
fn speech_energy_contour_tracks_input() {
let raw = include_bytes!("testdata/synth_mic.raw");
let pcm: Vec<f32> = raw
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect();
let out = round_trip(&pcm);
const DELAY: usize = 48;
const WIN: usize = 320;
let ea = envelope(&pcm, WIN);
let eb = envelope(&out[DELAY.min(out.len())..], WIN);
let m = ea.len().min(eb.len());
let ec = corr(&ea[..m], &eb[..m]);
assert!(
ec > 0.45,
"real-speech envelope correlation {ec:.3} too low (outbound mute regression)"
);
let (ri, ro) = (rms(&pcm), rms(&out));
assert!(
ro > 0.3 * ri && ro < 3.0 * ri,
"decoded RMS {ro:.4} not within [0.3x,3x] of input RMS {ri:.4}"
);
}
#[test]
fn tone_spectral_concentration() {
const FREQ: f64 = 550.0;
const N_FRAMES: usize = 5;
const FRAME: usize = 960;
let pcm = gen_tone(FREQ, FRAME * N_FRAMES, 0.4);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let signal_power = goertzel(steady, FREQ, 16000.0);
let controls = [
goertzel(steady, FREQ / 2.0, 16000.0),
goertzel(steady, FREQ * 1.7, 16000.0),
goertzel(steady, FREQ * 2.0 + 50.0, 16000.0),
goertzel(steady, 1500.0, 16000.0),
];
let control_power = controls.iter().cloned().fold(0f64, f64::max);
let ratio = if control_power > 0.0 {
signal_power / control_power
} else {
f64::INFINITY
};
assert!(
ratio > 3.0,
"pure tone {FREQ} Hz: spectral concentration ratio {ratio:.1}x < 3x (constant noise would fail this)"
);
}
#[test]
fn silence_stays_silent() {
const N_FRAMES: usize = 4;
const FRAME: usize = 960;
let pcm = gen_silence(FRAME * N_FRAMES);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let r = rms(steady);
assert!(
r < 0.01,
"silence: decoded RMS {r:.6} is non-negligible (encoder should not inject energy into silence)"
);
}
#[test]
fn voiced_harmonic_has_pitch_periodicity() {
const N_FRAMES: usize = 6;
const FRAME: usize = 960;
let sr = 16000.0f64;
for f0 in [150.0f64, 130.0, 120.0] {
let pcm = gen_voiced_harmonic(f0, FRAME * N_FRAMES, 0.3);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let lag = (sr / f0).round() as usize;
let autocorr_at_lag = normalized_autocorr(steady, lag).abs();
let input_steady = &pcm[FRAME..];
let input_autocorr = normalized_autocorr(input_steady, lag).abs();
assert!(
input_autocorr > 0.5,
"f0={f0}Hz: input autocorr at lag {lag} is {input_autocorr:.3}; test signal is not voiced enough"
);
assert!(
autocorr_at_lag > 0.4,
"f0={f0}Hz (lag={lag}): decoded autocorr at pitch lag = {autocorr_at_lag:.3} < 0.4; \
the encoder is stripping pitch periodicity"
);
}
}
#[test]
fn noise_does_not_produce_pitch_periodicity() {
const N_FRAMES: usize = 4;
const FRAME: usize = 960;
let pcm = gen_white_noise(FRAME * N_FRAMES, 0.3);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let peak = peak_autocorr(steady, 80, 143);
assert!(
peak < 0.8,
"noise: peak autocorr in voiced range = {peak:.3} >= 0.8; encoder is spuriously applying LTP"
);
let r = rms(steady);
assert!(
r > 0.001,
"noise: decoded RMS {r:.6} is too low; encoder is discarding signal energy"
);
}
#[test]
fn chirp_tracks_frequency() {
const N_FRAMES: usize = 8;
const FRAME: usize = 960;
let pcm = gen_chirp(300.0, 800.0, FRAME * N_FRAMES, 0.3);
let out = round_trip(&pcm);
let find_dom_freq = |seg: &[f32]| -> f64 {
let (mut best_power, mut best_f) = (0f64, 0f64);
let mut f = 100.0f64;
while f < 1500.0 {
let p = goertzel(seg, f, 16000.0);
if p > best_power {
best_power = p;
best_f = f;
}
f += 5.0;
}
best_f
};
let early_start = FRAME;
let early_end = 3 * FRAME;
let late_start = (N_FRAMES - 3) * FRAME;
let late_end = N_FRAMES * FRAME;
assert!(
out.len() >= late_end,
"decoded output too short ({} < {late_end}); the chirp decode regressed",
out.len()
);
let dom_early = find_dom_freq(&out[early_start..early_end]);
let dom_late = find_dom_freq(&out[late_start..late_end]);
assert!(
dom_late > dom_early,
"chirp: dominant frequency did not rise (early={dom_early:.0}Hz late={dom_late:.0}Hz); \
encoder is not tracking the time-varying spectral envelope"
);
}
#[test]
fn energy_tracks_input_level() {
const N_FRAMES: usize = 4;
const FRAME: usize = 960;
let mut prev_rms = 0.0f64;
for & in &[0.05f32, 0.15, 0.35] {
let pcm = gen_white_noise(FRAME * N_FRAMES, amp);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let r = rms(steady);
assert!(
r > prev_rms,
"energy not monotone: amp={amp} decoded RMS={r:.4} not above prev {prev_rms:.4}"
);
prev_rms = r;
}
let silent_pcm = gen_silence(FRAME * N_FRAMES);
let silent_out = round_trip(&silent_pcm);
let silent_steady = &silent_out[FRAME..];
assert!(
rms(silent_steady) < prev_rms / 5.0,
"energy not quashed for silence: silent decoded RMS {:.4} is too close to loud {prev_rms:.4}",
rms(silent_steady)
);
}
#[test]
fn voiced_fundamental_survives_in_spectrum() {
const N_FRAMES: usize = 6;
const FRAME: usize = 960;
for f0 in [150.0f64, 130.0, 120.0] {
let pcm = gen_voiced_harmonic(f0, FRAME * N_FRAMES, 0.3);
let out = round_trip(&pcm);
let steady = &out[FRAME..];
let e_f0 = goertzel(steady, f0, 16000.0);
let controls = [
goertzel(steady, f0 * 0.5, 16000.0),
goertzel(steady, f0 * 1.5, 16000.0),
goertzel(steady, f0 * 2.5, 16000.0),
];
let control_max = controls.iter().cloned().fold(0f64, f64::max);
let ratio = if control_max > 0.0 {
e_f0 / control_max
} else {
f64::INFINITY
};
assert!(
ratio > 5.0,
"f0={f0}Hz: fundamental/inter-harmonic ratio {ratio:.1}x < 5x in spectrum; \
voiced signal is not reconstructing the fundamental (unvoiced noise smears it)"
);
}
}
#[test]
fn decoder_tracks_energy_envelope() {
let recs: serde_json::Value =
serde_json::from_str(include_str!("testdata/e2e_vectors.json")).expect("e2e_vectors");
let arr = recs.as_array().unwrap();
let mut dec = MlowDecoder::new();
let mut active_pairs: Vec<(f64, f64)> = Vec::new();
for rec in arr {
let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap();
let want: Vec<f32> = rec["pcm"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap() as f32)
.collect();
let got = dec.decode(&frame);
let ref_r = rms(&want);
let rust_r = rms(&got);
if ref_r > 0.005 {
active_pairs.push((ref_r, rust_r));
}
}
assert!(
!active_pairs.is_empty(),
"no active frames found in e2e_vectors.json; capture may be DTX-only"
);
let mut failures = 0;
for (i, &(ref_r, rust_r)) in active_pairs.iter().enumerate() {
if rust_r < ref_r / 10.0 {
eprintln!(
" active frame {i}: ref RMS={ref_r:.4} Rust RMS={rust_r:.6} (Rust {:.1}x quieter)",
ref_r / rust_r.max(1e-9)
);
failures += 1;
} else if rust_r > ref_r * 10.0 {
eprintln!(
" active frame {i}: ref RMS={ref_r:.4} Rust RMS={rust_r:.6} (Rust {:.1}x louder)",
rust_r / ref_r.max(1e-9)
);
failures += 1;
}
}
assert!(
failures == 0,
"{failures}/{} active frames: Rust decoder RMS is >10x off the reference (either direction)",
active_pairs.len()
);
}
#[test]
fn decoder_silence_frames_produce_zero() {
let recs: serde_json::Value =
serde_json::from_str(include_str!("testdata/e2e_vectors.json")).expect("e2e_vectors");
let arr = recs.as_array().unwrap();
let mut dec = MlowDecoder::new();
for (i, rec) in arr.iter().enumerate() {
let frame = hex::decode(rec["frame"].as_str().unwrap()).unwrap();
let want: Vec<f32> = rec["pcm"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap() as f32)
.collect();
let ref_r = rms(&want);
let got = dec.decode(&frame);
let rust_r = rms(&got);
if ref_r < 0.001 {
assert!(
rust_r < 0.001,
"frame {i}: reference is silence (RMS={ref_r:.6}) but Rust produced RMS={rust_r:.6}"
);
}
}
}
#[test]
fn voiced_harmonic_low_amplitude_tracks_input() {
const N_FRAMES: usize = 8;
const FRAME: usize = 960;
let pcm = gen_voiced_harmonic(130.0, FRAME * N_FRAMES, 0.05);
let out = round_trip(&pcm);
let steady_in = &pcm[FRAME..];
let mut best = 0f64;
for d in 0..128 {
if FRAME + d >= out.len() {
break;
}
let n = steady_in.len().min(out.len() - FRAME - d);
best = best.max(corr(&steady_in[..n], &out[FRAME + d..FRAME + d + n]).abs());
}
assert!(
best > 0.7,
"low-amplitude harmonic: best phase-aligned |corr|={best:.3} < 0.7; the voiced path is not \
reconstructing the signal"
);
}
#[test]
fn voiced_harmonic_multiple_frames_no_panic() {
const N_FRAMES: usize = 6;
const FRAME: usize = 960;
let pcm = gen_voiced_harmonic(130.0, FRAME * N_FRAMES, 0.3);
let mut enc = MlowEncoder::new();
let mut dec = MlowDecoder::new();
let mut total_decoded = 0usize;
for i in 0..N_FRAMES {
let frame = enc
.encode(&pcm[i * FRAME..(i + 1) * FRAME])
.expect("encode");
assert_eq!(frame[0], 0x50, "expected active config=0 TOC");
let decoded = dec.decode(&frame);
total_decoded += decoded.len();
}
assert_eq!(
total_decoded,
FRAME * N_FRAMES,
"expected {FRAME}*{N_FRAMES} decoded samples"
);
}
#[test]
fn captured_inbound_mlow_decodes_clean() {
let frames: Vec<String> =
serde_json::from_str(include_str!("testdata/inbound_capture_frames.json"))
.expect("inbound_capture_frames.json");
assert!(!frames.is_empty(), "capture is empty");
let mut dec = MlowDecoder::new();
let mut all: Vec<f32> = Vec::new();
let mut active_frames = 0usize;
for hex_frame in &frames {
let frame = hex::decode(hex_frame).unwrap();
let pcm = dec.decode(&frame);
assert!(!pcm.is_empty(), "every mlow frame decodes to non-empty PCM");
if rms(&pcm) > 0.005 {
active_frames += 1;
}
all.extend_from_slice(&pcm);
}
let overall_rms = rms(&all);
let peak = all.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
let clip = all.iter().filter(|&&x| x.abs() >= 0.999).count();
let clip_pct = 100.0 * clip as f64 / all.len() as f64;
assert!(
(0.01..0.20).contains(&overall_rms),
"inbound RMS {overall_rms:.4} outside voice range (mis-routed decode would be louder)"
);
assert!(
peak < 0.95,
"inbound peak {peak:.4} near full-scale; clipping noise, not clean voice"
);
assert!(
clip_pct < 0.5,
"inbound {clip_pct:.2}% samples clipped (mis-routed decode clips heavily)"
);
assert!(
active_frames > frames.len() / 10,
"only {active_frames}/{} active frames; stream decoded to silence",
frames.len()
);
}
#[test]
fn inbound_capture_frames_cover_config1_and_config2_tocs() {
let frames: Vec<String> =
serde_json::from_str(include_str!("testdata/inbound_capture_frames.json"))
.expect("inbound_capture_frames.json");
let toc = |hex_frame: &str| -> Option<u8> {
hex::decode(hex_frame).ok().and_then(|b| b.first().copied())
};
let has = |want: u8| frames.iter().any(|f| toc(f) == Some(want));
assert!(
has(0x10),
"fixture lost config-1 (0x10) frames: config-1 decode branch no longer covered"
);
assert!(
has(0x12),
"fixture lost config-2 (0x12) frames: config-2 decode branch no longer covered"
);
assert!(
has(0x50),
"fixture lost active config-0 (0x50) frames: capture is not a normal call stream"
);
}