use timestretch::engine::{Engine, EngineConfig, EngineProfile};
const RATE: f64 = 0.874;
fn power_at(scan: &[f32], freq: f64, sample_rate: f64) -> f64 {
let w = 2.0 * std::f64::consts::PI * freq / sample_rate;
let coeff = 2.0 * w.cos();
let (mut s1, mut s2) = (0.0f64, 0.0);
for &x in scan {
let s0 = f64::from(x) + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
let n = scan.len() as f64;
(s1 * s1 + s2 * s2 - coeff * s1 * s2) / (n * n / 4.0)
}
fn sub_state_windows(sample_rate: u32) -> (usize, usize) {
let handles = Engine::build(EngineConfig {
sample_rate,
channels: 1,
profile: EngineProfile::Keylock,
initial_tempo_rate: RATE,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source, _controller) =
(handles.processor, handles.source, handles.controller);
let sr = sample_rate as usize;
let mut n_gen = 0usize;
let (mut p_sub, mut p_bass) = (0.0f64, 0.0f64);
let mut next_source = |n: usize| -> Vec<f32> {
(0..n)
.map(|_| {
p_sub += 2.0 * std::f64::consts::PI * 55.0 / f64::from(sample_rate);
p_bass += 2.0 * std::f64::consts::PI * 85.0 / f64::from(sample_rate);
let gate = (n_gen % (sr / 2)) < sr / 4;
n_gen += 1;
(0.35 * p_sub.sin() + if gate { 0.45 * p_bass.sin() } else { 0.0 }) as f32
})
.collect()
};
let total_frames = 12 * sr;
let mut out = Vec::with_capacity(total_frames);
let mut chunk = vec![0.0f32; 1024];
while out.len() < total_frames {
while source.occupied_frames() < 8192 {
let feed = next_source(2048);
assert_eq!(source.push(&feed), feed.len(), "source ring saturated");
}
processor.process(&mut chunk);
out.extend_from_slice(&chunk);
}
let scan = &out[4 * sr..];
let win = sr / 4;
let (mut corrected, mut detuned) = (0usize, 0usize);
for w in scan.chunks(win) {
if w.len() < win {
break;
}
if power_at(w, 55.0, f64::from(sample_rate))
> power_at(w, 55.0 * RATE, f64::from(sample_rate))
{
corrected += 1;
} else {
detuned += 1;
}
}
(corrected, detuned)
}
#[test]
fn sub_band_stays_corrected_at_96k() {
for sample_rate in [44_100u32, 96_000] {
let (corrected, detuned) = sub_state_windows(sample_rate);
assert!(
detuned == 0 && corrected > 0,
"{sample_rate} Hz build: sub flapped out of correction \
({corrected} corrected / {detuned} detuned windows)"
);
}
}
#[test]
fn low_mids_stay_pure_at_96k() {
for sample_rate in [44_100u32, 96_000] {
let handles = Engine::build(EngineConfig {
sample_rate,
channels: 1,
profile: EngineProfile::Keylock,
initial_tempo_rate: 1.08,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source, _controller) =
(handles.processor, handles.source, handles.controller);
let sr = sample_rate as usize;
let mut phase = 0.0f64;
let mut next_source = |n: usize| -> Vec<f32> {
(0..n)
.map(|_| {
phase += 2.0 * std::f64::consts::PI * 150.0 / f64::from(sample_rate);
0.6 * phase.sin() as f32
})
.collect()
};
let total_frames = 10 * sr;
let mut out = Vec::with_capacity(total_frames);
let mut chunk = vec![0.0f32; 1024];
while out.len() < total_frames {
while source.occupied_frames() < 8192 {
let feed = next_source(2048);
assert_eq!(source.push(&feed), feed.len(), "source ring saturated");
}
processor.process(&mut chunk);
out.extend_from_slice(&chunk);
}
let scan = &out[4 * sr..];
let energy: f64 = scan.iter().map(|&x| f64::from(x) * f64::from(x)).sum();
let at_150 = power_at(scan, 150.0, f64::from(sample_rate));
let purity = at_150 / (energy / scan.len() as f64 * 2.0).max(1e-12);
assert!(
purity > 0.9,
"{sample_rate} Hz build: 150 Hz corrected at purity {purity:.3} (want > 0.9)"
);
}
}