use std::f32::consts::PI;
use timestretch::{StreamProcessor, StreamProfile, StretchParams};
const SR: u32 = 44_100;
const CHUNK: usize = 256;
fn params(ratio: f64, profile: StreamProfile) -> StretchParams {
StretchParams::new(ratio)
.with_sample_rate(SR)
.with_channels(1)
.with_stream_profile(profile)
}
fn sine(freq: f32, len: usize, amp: f32) -> Vec<f32> {
(0..len)
.map(|i| amp * (2.0 * PI * freq * i as f32 / SR as f32).sin())
.collect()
}
fn sine_max_slew(freq: f32, amp: f32) -> f32 {
amp * 2.0 * PI * freq / SR as f32
}
const CLICK_SLEW_MULTIPLE: f32 = 6.0;
fn max_adjacent_diff(samples: &[f32]) -> (usize, f32) {
let mut worst = (0usize, 0.0f32);
for (i, w) in samples.windows(2).enumerate() {
let d = (w[1] - w[0]).abs();
if d > worst.1 {
worst = (i, d);
}
}
worst
}
fn rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
(samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
}
fn stream_range(
processor: &mut StreamProcessor,
input: &[f32],
range: std::ops::Range<usize>,
output: &mut Vec<f32>,
) {
for chunk in input[range].chunks(CHUNK) {
processor.process_into(chunk, output).expect("process");
}
}
#[test]
fn loop_wrap_seams_are_click_free() {
let freq = 210.0f32; let amp = 0.5f32;
let period = SR as usize / 210;
let input = sine(freq, SR as usize * 4, amp);
for &(ratio, profile) in &[
(1.05f64, StreamProfile::Live),
(0.94, StreamProfile::Live),
(1.05, StreamProfile::Quality),
] {
let mut processor = StreamProcessor::new(params(ratio, profile));
let loop_start = 10 * period;
let loop_len = (SR as usize / period) * period;
let loop_end = loop_start + loop_len;
let mut output: Vec<f32> = Vec::with_capacity(input.len() * 8);
stream_range(&mut processor, &input, 0..loop_end, &mut output);
for _ in 0..4 {
processor.notify_source_jump(loop_start);
stream_range(&mut processor, &input, loop_start..loop_end, &mut output);
}
let bound = sine_max_slew(freq, amp) * CLICK_SLEW_MULTIPLE;
let skip = 8192;
let (idx, worst) = max_adjacent_diff(&output[skip..]);
assert!(
worst <= bound,
"ratio {ratio} {profile}: loop-wrap click at output {} (|delta|={:.4} > {:.4})",
skip + idx,
worst,
bound
);
}
}
#[test]
fn cue_drumming_resumes_converged() {
let freq = 210.0f32;
let amp = 0.5f32;
let input = sine(freq, SR as usize * 6, amp);
let mut processor = StreamProcessor::new(params(1.06, StreamProfile::Live));
let preroll_frames = processor.warm_start_preroll_frames();
let cue = SR as usize * 2;
let mut warmup_out: Vec<f32> = Vec::with_capacity(input.len() * 4);
stream_range(&mut processor, &input, 0..SR as usize, &mut warmup_out);
let steady_rms = rms(&warmup_out[warmup_out.len().saturating_sub(4096)..]);
assert!(steady_rms > 0.1, "steady reference not established");
let bound = sine_max_slew(freq, amp) * CLICK_SLEW_MULTIPLE;
let burst = SR as usize / 10;
for hit in 0..20 {
processor
.warm_start_seek(cue, &input[cue - preroll_frames..cue])
.expect("cue jump");
let mut segment: Vec<f32> = Vec::with_capacity(burst * 4);
stream_range(&mut processor, &input, cue..cue + burst, &mut segment);
assert!(
segment.len() > burst / 2,
"hit {hit}: warm start gapped — only {} output samples for {} input frames",
segment.len(),
burst
);
let post_fade = &segment[512..2048.min(segment.len())];
let level = rms(post_fade);
assert!(
level > steady_rms * 0.7,
"hit {hit}: post-seek level {:.3} below steady {:.3} — warm-up transient",
level,
steady_rms
);
let (idx, worst) = max_adjacent_diff(&segment[512..]);
assert!(
worst <= bound,
"hit {hit}: click inside post-seek segment at {} (|delta|={:.4} > {:.4})",
512 + idx,
worst,
bound
);
}
}
#[test]
fn warm_start_preserves_control_state() {
let input = sine(210.0, SR as usize * 4, 0.5);
let mut processor = StreamProcessor::new(params(1.0, StreamProfile::Live));
let preroll_frames = processor.warm_start_preroll_frames();
let mut output = Vec::with_capacity(input.len() * 4);
stream_range(&mut processor, &input, 0..SR as usize, &mut output);
processor.set_stretch_ratio(1.08).expect("ratio");
processor.set_pitch_scale(1.05).expect("pitch");
processor
.process_into(&input[SR as usize..SR as usize + CHUNK], &mut output)
.expect("process");
let current_before = processor.current_stretch_ratio();
let target_before = processor.target_stretch_ratio();
let pitch_before = processor.pitch_scale();
assert!(
(current_before - target_before).abs() > 0.001,
"test premise: glide still in flight"
);
let target = SR as usize * 2;
processor
.warm_start_seek(target, &input[target - preroll_frames..target])
.expect("warm start");
assert_eq!(processor.current_stretch_ratio(), current_before);
assert_eq!(processor.target_stretch_ratio(), target_before);
assert_eq!(processor.pitch_scale(), pitch_before);
let in_flight = processor.capacities().0; assert_eq!(processor.source_position() + in_flight, target);
}
#[test]
fn warm_start_short_preroll_degrades_gracefully() {
let input = sine(210.0, SR as usize * 2, 0.5);
let mut processor = StreamProcessor::new(params(1.05, StreamProfile::Live));
processor
.warm_start_seek(300, &input[..300])
.expect("short preroll");
let mut output = Vec::with_capacity(input.len() * 4);
stream_range(&mut processor, &input, 300..SR as usize, &mut output);
assert!(!output.is_empty(), "short-preroll seek must still stream");
processor
.warm_start_seek(SR as usize, &[])
.expect("empty preroll");
assert_eq!(processor.source_position(), SR as usize);
}
#[test]
fn warm_start_unity_resumes_passthrough() {
let input = sine(210.0, SR as usize * 3, 0.5);
let mut processor = StreamProcessor::new(params(1.0, StreamProfile::Live));
let preroll_frames = processor.warm_start_preroll_frames();
let mut output = Vec::with_capacity(input.len() * 2);
stream_range(&mut processor, &input, 0..SR as usize, &mut output);
let target = SR as usize * 2;
processor
.warm_start_seek(target, &input[target - preroll_frames..target])
.expect("unity warm start");
assert_eq!(processor.source_position(), target);
let before = output.len();
stream_range(&mut processor, &input, target..target + 2048, &mut output);
assert_eq!(output.len() - before, 2048);
assert_eq!(&output[before + 512..], &input[target + 512..target + 2048]);
}