#[allow(dead_code)]
#[path = "ab/mod.rs"]
mod ab;
use ab::{render_with_rate_schedule, Arm};
use timestretch::StreamProfile;
const SAMPLE_RATE: u32 = 44_100;
const CALLBACK_FRAMES: usize = 256;
const REFERENCE_HZ: f64 = 440.0;
const ESTIMATE_WINDOW: usize = 4_410;
const ESTIMATE_HOP: usize = 1_102;
const WARMUP_SECS: f64 = 0.5;
const P95_CENTS_LIMIT: f64 = 15.0;
const MAX_CENTS_LIMIT: f64 = 22.0;
fn sine(freq: f64, len: usize, amp: f32) -> Vec<f32> {
(0..len)
.map(|i| {
amp * (2.0 * std::f64::consts::PI * freq * i as f64 / SAMPLE_RATE as f64).sin() as f32
})
.collect()
}
fn zero_crossing_freq(window: &[f32]) -> Option<f64> {
let (mut first, mut last, mut count) = (None, None, 0usize);
for i in 1..window.len() {
let (a, b) = (window[i - 1] as f64, window[i] as f64);
if a <= 0.0 && b > 0.0 {
let t = (i - 1) as f64 + a / (a - b);
if first.is_none() {
first = Some(t);
}
last = Some(t);
count += 1;
}
}
match (first, last) {
(Some(f), Some(l)) if count >= 3 && l > f => {
Some((count - 1) as f64 * SAMPLE_RATE as f64 / (l - f))
}
_ => None,
}
}
fn cents_deviation_track(output: &[f32]) -> (f64, f64) {
let start = (WARMUP_SECS * SAMPLE_RATE as f64) as usize;
let mut deviations: Vec<f64> = Vec::new();
let mut pos = start;
while pos + ESTIMATE_WINDOW <= output.len() {
if let Some(freq) = zero_crossing_freq(&output[pos..pos + ESTIMATE_WINDOW]) {
deviations.push((1_200.0 * (freq / REFERENCE_HZ).log2()).abs());
}
pos += ESTIMATE_HOP;
}
assert!(
deviations.len() > 50,
"not enough frequency estimates: {}",
deviations.len()
);
deviations.sort_by(|a, b| a.total_cmp(b));
let p95 = deviations[((deviations.len() - 1) as f64 * 0.95).round() as usize];
let max = *deviations.last().unwrap();
(p95, max)
}
fn torture_ride(t: f64) -> f64 {
1.0 + 0.08 * (2.0 * std::f64::consts::PI * t / 2.0).sin()
}
#[test]
fn keylock_cents_wobble_under_torture_ride() {
let input = sine(REFERENCE_HZ, SAMPLE_RATE as usize * 10, 0.7);
let new = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&torture_ride,
);
let old = render_with_rate_schedule(
Arm::OldVarispeed(StreamProfile::Live),
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&torture_ride,
);
let (new_p95, new_max) = cents_deviation_track(&new.output);
let (old_p95, old_max) = cents_deviation_track(&old.output);
println!(
"keylock cents wobble: new p95={new_p95:.2} max={new_max:.2} | old Live p95={old_p95:.2} max={old_max:.2}"
);
assert!(
new_p95 <= P95_CENTS_LIMIT,
"keylock p95 wobble {new_p95:.2} cents > {P95_CENTS_LIMIT}"
);
assert!(
new_max <= MAX_CENTS_LIMIT,
"keylock max wobble {new_max:.2} cents > {MAX_CENTS_LIMIT}"
);
assert!(
new_p95 <= old_p95 * 1.1 + 0.5,
"new keylock p95 {new_p95:.2} worse than old {old_p95:.2}"
);
assert_eq!(new.underrun_frames, 0);
}
#[test]
fn keylock_cents_wobble_sola_band_ride() {
let input = sine(REFERENCE_HZ, SAMPLE_RATE as usize * 10, 0.7);
let render = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|t| 1.0 + 0.04 * (2.0 * std::f64::consts::PI * t / 2.0).sin(),
);
let (p95, max) = cents_deviation_track(&render.output);
println!("keylock SOLA-band ±4% ride: p95={p95:.2} max={max:.2} cents");
assert!(
p95 <= 6.0,
"SOLA-band ride p95 {p95:.2} cents exceeds the time-domain budget"
);
assert!(max <= 10.0, "SOLA-band ride max {max:.2} cents");
}
#[test]
fn keylock_steady_ratio_is_subcent() {
let input = sine(REFERENCE_HZ, SAMPLE_RATE as usize * 6, 0.7);
let render = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| 1.06,
);
let (p95, max) = cents_deviation_track(&render.output);
println!("keylock steady 1.06: p95={p95:.3} max={max:.3} cents");
assert!(
p95 <= 1.0,
"steady-ratio p95 {p95:.3} cents should be ~exact"
);
assert!(
max <= 2.0,
"steady-ratio max {max:.3} cents should be ~exact"
);
}
fn seam_metrics(rate: f64) -> (f64, f64) {
let seam_hz = 150.0;
let input = sine(seam_hz, SAMPLE_RATE as usize * 8, 0.6);
let render = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let out = &render.output;
let start = (WARMUP_SECS * SAMPLE_RATE as f64) as usize;
let win = 2_205;
let mut env: Vec<f64> = Vec::new();
let mut pos = start;
while pos + win <= out.len() {
let rms = (out[pos..pos + win]
.iter()
.map(|&s| (s as f64) * (s as f64))
.sum::<f64>()
/ win as f64)
.sqrt();
env.push(rms);
pos += win / 2;
}
let mean = env.iter().sum::<f64>() / env.len() as f64;
let input_rms = 0.6 / std::f64::consts::SQRT_2;
let level_db = 20.0 * (mean / input_rms).log10();
let max_dev = env
.iter()
.map(|&e| (e - mean).abs() / mean)
.fold(0.0f64, f64::max);
(level_db, max_dev)
}
#[test]
fn crossover_seam_resums_cleanly_at_unity() {
let (level_db, max_dev) = seam_metrics(1.0);
println!(
"seam @ unity: level {level_db:+.2} dB, max envelope deviation {:.1}%",
max_dev * 100.0
);
assert!(
level_db.abs() <= 1.0,
"unity seam level {level_db:+.2} dB — delay mismatch / comb filtering"
);
assert!(
max_dev <= 0.1,
"unity seam envelope deviation {:.1}% — delay mismatch",
max_dev * 100.0
);
}
#[test]
fn crossover_seam_detuning_at_dj_rates_stays_in_known_envelope() {
let (level_db, max_dev) = seam_metrics(1.06);
println!(
"seam @ 1.06: level {level_db:+.2} dB, max envelope deviation {:.1}% \
(measured 2026-07 with the corrected LR8: -3.0 dB / 39% — the power \
sum of two detuned copies; worst case: pure tone exactly at the seam)",
max_dev * 100.0
);
assert!(
level_db >= -4.5,
"seam detuning level {level_db:+.2} dB regressed past the known envelope"
);
assert!(
max_dev <= 0.55,
"seam beating {:.1}% regressed past the known envelope",
max_dev * 100.0
);
}
#[test]
#[ignore = "renders listening material; run explicitly"]
fn falsification_render_low_band_listening_pairs() {
let len = SAMPLE_RATE as usize * 12;
let mut input = vec![0.0f32; len];
for (i, s) in input.iter_mut().enumerate() {
let t = i as f64 / SAMPLE_RATE as f64;
*s += 0.45 * (2.0 * std::f64::consts::PI * 55.0 * t).sin() as f32;
*s += 0.15 * (2.0 * std::f64::consts::PI * 220.0 * t).sin() as f32;
*s += 0.1 * (2.0 * std::f64::consts::PI * 880.0 * t).sin() as f32;
}
let beat = SAMPLE_RATE as usize / 2;
for start in (0..len).step_by(beat) {
for k in 0..2_000.min(len - start) {
let t = k as f64 / SAMPLE_RATE as f64;
let env = (-t * 18.0).exp();
input[start + k] += (0.8 * env * (2.0 * std::f64::consts::PI * 60.0 * t).sin()) as f32;
}
}
let out_dir = std::path::Path::new("target/keylock_falsification");
std::fs::create_dir_all(out_dir).expect("create output dir");
write_wav(&out_dir.join("source.wav"), &input);
for rate in [1.08, 0.92, 1.2, 0.8] {
let new = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let old = render_with_rate_schedule(
Arm::OldVarispeed(StreamProfile::Live),
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let tag = format!("{:+.0}pct", (rate - 1.0) * 100.0);
write_wav(&out_dir.join(format!("new_keylock_{tag}.wav")), &new.output);
write_wav(&out_dir.join(format!("old_keylock_{tag}.wav")), &old.output);
println!(
"rendered {tag}: new {} frames, old {} frames",
new.output.len(),
old.output.len()
);
}
println!("listening pairs in {}", out_dir.display());
}
fn write_wav(path: &std::path::Path, samples: &[f32]) {
let spec = hound::WavSpec {
channels: 1,
sample_rate: SAMPLE_RATE,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer = hound::WavWriter::create(path, spec).expect("create wav");
for &s in samples {
writer
.write_sample((s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16)
.expect("write sample");
}
writer.finalize().expect("finalize wav");
}