use timestretch::core::window::WindowType;
use timestretch::stretch::{PhaseLockingMode, PhaseVocoder};
const SAMPLE_RATE: u32 = 44_100;
const FFT_SIZE: usize = 2048;
const HOP: usize = 256;
const SUB_BASS_HZ: f32 = 100.0;
fn make_pv(ratio: f64) -> PhaseVocoder {
PhaseVocoder::with_options(
FFT_SIZE,
HOP,
ratio,
SAMPLE_RATE,
SUB_BASS_HZ,
WindowType::Hann,
PhaseLockingMode::Identity,
)
}
fn sine(num_samples: usize, freq: f64) -> Vec<f32> {
(0..num_samples)
.map(|i| (2.0 * std::f64::consts::PI * freq * i as f64 / SAMPLE_RATE as f64).sin() as f32)
.collect()
}
fn tone_mix(num_samples: usize) -> Vec<f32> {
let low = sine(num_samples, 220.0);
let high = sine(num_samples, 3_300.0);
let mut seed = 0x2545_f491_4f6c_dd1du64;
(0..num_samples)
.map(|i| {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let noise = ((seed >> 33) as f64 / (1u64 << 31) as f64 - 1.0) as f32;
0.5 * low[i] + 0.3 * high[i] + 0.05 * noise
})
.collect()
}
fn best_aligned_ser(reference: &[f32], out: &[f32], max_lag: i64) -> (f64, f64, i64) {
let mut best = (f64::NEG_INFINITY, f64::INFINITY, 0i64);
for lag in -max_lag..=max_lag {
let mut signal = 0.0f64;
let mut error = 0.0f64;
let mut peak = 0.0f64;
let mut count = 0usize;
for (i, &r) in reference.iter().enumerate() {
let j = i as i64 + lag;
if j < 0 || j as usize >= out.len() {
continue;
}
let e = (r as f64 - out[j as usize] as f64).abs();
signal += (r as f64) * (r as f64);
error += e * e;
peak = peak.max(e);
count += 1;
}
if count < reference.len() / 2 {
continue;
}
let ser = 10.0 * (signal / error.max(1e-30)).log10();
if ser > best.0 {
best = (ser, peak, lag);
}
}
(best.0, best.1, best.2)
}
fn tone_purity_db(segment: &[f32], freq: f64) -> f64 {
let n = segment.len();
let windowed: Vec<f64> = segment
.iter()
.enumerate()
.map(|(i, &s)| {
let w = 0.5 - 0.5 * (2.0 * std::f64::consts::PI * i as f64 / n as f64).cos();
s as f64 * w
})
.collect();
let total: f64 = windowed.iter().map(|&s| s * s).sum();
let bin_hz = SAMPLE_RATE as f64 / n as f64;
let mut tone = 0.0f64;
for k in -2i64..=2 {
let f = freq + k as f64 * bin_hz;
let w = 2.0 * std::f64::consts::PI * f / SAMPLE_RATE as f64;
let coeff = 2.0 * w.cos();
let (mut s1, mut s2) = (0.0f64, 0.0f64);
for &x in &windowed {
let s0 = x + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
tone += (s1 * s1 + s2 * s2 - coeff * s1 * s2) * 2.0 / n as f64;
}
let rest = (total - tone).max(1e-30);
10.0 * (tone / rest).log10()
}
#[test]
fn pv_null_at_ratio_one() {
let input = tone_mix(5 * SAMPLE_RATE as usize);
let mut pv = make_pv(1.0);
let output = pv.process(&input).expect("pv process");
assert_eq!(
output.len(),
input.len(),
"ratio-1.0 output length must equal input length"
);
let margin = 4 * FFT_SIZE;
let reference = &input[margin..input.len() - margin];
let window = &output[margin - FFT_SIZE..input.len() - margin + FFT_SIZE];
let (ser, peak_residual, lag) = best_aligned_ser(reference, window, 2 * FFT_SIZE as i64);
assert!(
ser > 100.0,
"ratio-1.0 null: SER {ser:.1} dB (peak residual {peak_residual:.2e}, lag {lag}) — \
expected > 100 dB (measured 139 dB fixed, 59 dB with unwrapped blends)"
);
}
#[test]
fn pv_long_render_purity_stable() {
let seconds = 30;
let freq = 5_000.0;
let input = sine(seconds * SAMPLE_RATE as usize, freq);
let mut pv = make_pv(1.5);
let output = pv.process(&input).expect("pv process");
let sr = SAMPLE_RATE as usize;
let early = tone_purity_db(&output[5 * sr..6 * sr], freq);
let late_start = output.len() - 10 * sr;
let late = tone_purity_db(&output[late_start..late_start + sr], freq);
assert!(
early > 45.0 && late > 45.0,
"tone purity {early:.1} dB early / {late:.1} dB late — expected > 45 dB (measured \
~57 dB fixed, ~25 dB with unwrapped blends)"
);
assert!(
late > early - 3.0,
"tone purity decayed over the render: {early:.1} dB at 5 s vs {late:.1} dB near the \
end — phase accumulator precision is degrading"
);
}
#[test]
fn pv_preserves_dc_component() {
let n = 3 * SAMPLE_RATE as usize;
let input: Vec<f32> = sine(n, 440.0).iter().map(|&s| 0.3 * s + 0.25).collect();
let mut pv = make_pv(1.25);
let output = pv.process(&input).expect("pv process");
let margin = 4 * FFT_SIZE;
let mid = &output[margin..output.len() - margin];
let mean = mid.iter().map(|&s| s as f64).sum::<f64>() / mid.len() as f64;
assert!(
(mean - 0.25).abs() < 0.02,
"DC offset not preserved: input +0.25, output mean {mean:+.3}"
);
}