use std::f32::consts::PI;
use timestretch::{stretch, EdmPreset, StreamProcessor, StretchParams, WindowType};
fn sine_wave(freq: f32, sample_rate: u32, num_samples: usize) -> Vec<f32> {
(0..num_samples)
.map(|i| (2.0 * PI * freq * i as f32 / sample_rate as f32).sin())
.collect()
}
fn rms(signal: &[f32]) -> f32 {
if signal.is_empty() {
return 0.0;
}
(signal.iter().map(|x| x * x).sum::<f32>() / signal.len() as f32).sqrt()
}
fn spectral_energy_at_freq(signal: &[f32], sample_rate: u32, target_freq: f32) -> f32 {
let n = signal.len();
if n == 0 {
return 0.0;
}
let two_pi = 2.0 * PI;
let mut real = 0.0f64;
let mut imag = 0.0f64;
for (i, &s) in signal.iter().enumerate() {
let angle = two_pi * target_freq * i as f32 / sample_rate as f32;
real += s as f64 * angle.cos() as f64;
imag += s as f64 * angle.sin() as f64;
}
((real * real + imag * imag) / n as f64).sqrt() as f32
}
fn stream_stretch(input: &[f32], params: StretchParams, chunk_size: usize) -> Vec<f32> {
let mut processor = StreamProcessor::new(params);
let mut output = Vec::new();
for chunk in input.chunks(chunk_size) {
if let Ok(out) = processor.process(chunk) {
output.extend_from_slice(&out);
}
}
if let Ok(remaining) = processor.flush() {
output.extend_from_slice(&remaining);
}
output
}
#[test]
fn test_streaming_basic_mono() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = stream_stretch(&input, params, 4096);
assert!(!output.is_empty(), "Streaming should produce output");
let max = output.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
assert!(max > 0.01, "Streaming output should not be silent");
}
#[test]
fn test_streaming_stereo() {
let sample_rate = 44100;
let num_frames = sample_rate as usize;
let mut input = Vec::with_capacity(num_frames * 2);
for i in 0..num_frames {
let t = i as f32 / sample_rate as f32;
input.push((2.0 * PI * 440.0 * t).sin());
input.push((2.0 * PI * 880.0 * t).sin());
}
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(2);
let output = stream_stretch(&input, params, 4096);
assert!(!output.is_empty());
assert_eq!(output.len() % 2, 0, "Stereo output must have even length");
}
#[test]
fn test_streaming_ratio_change() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 4);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1);
let mut processor = StreamProcessor::new(params);
let chunk_size = 4096;
let mut output = Vec::new();
for (chunk_idx, chunk) in input.chunks(chunk_size).enumerate() {
if chunk_idx == input.len() / chunk_size / 2 {
processor
.set_stretch_ratio(1.5)
.expect("valid stretch ratio");
}
if let Ok(out) = processor.process(chunk) {
output.extend_from_slice(&out);
}
}
if let Ok(remaining) = processor.flush() {
output.extend_from_slice(&remaining);
}
assert!(!output.is_empty());
}
#[test]
fn test_streaming_flush() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let mut processor = StreamProcessor::new(params);
let mut output_before_flush = Vec::new();
for chunk in input.chunks(2048) {
if let Ok(out) = processor.process(chunk) {
output_before_flush.extend_from_slice(&out);
}
}
let flushed = processor.flush().unwrap();
let total = output_before_flush.len() + flushed.len();
assert!(total > 0, "Total output should be non-empty after flush");
}
#[test]
fn test_streaming_reset() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let output1 = stream_stretch(&input, params.clone(), 4096);
let output2 = stream_stretch(&input, params, 4096);
let rms1 = rms(&output1);
let rms2 = rms(&output2);
assert!(
(rms1 - rms2).abs() < rms1 * 0.2,
"Reset should produce consistent output: rms1={}, rms2={}",
rms1,
rms2
);
}
#[test]
fn test_streaming_vs_batch_length() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
for &ratio in &[0.75, 1.0, 1.25, 1.5, 2.0] {
let params = StretchParams::new(ratio)
.with_sample_rate(sample_rate)
.with_channels(1);
let batch_output = stretch(&input, ¶ms).unwrap();
let stream_output = stream_stretch(&input, params, 4096);
let batch_ratio = batch_output.len() as f64 / input.len() as f64;
let stream_ratio = stream_output.len() as f64 / input.len() as f64;
assert!(
(batch_ratio - stream_ratio).abs() < 0.5,
"Ratio {}: batch_ratio={:.3}, stream_ratio={:.3}",
ratio,
batch_ratio,
stream_ratio
);
}
}
#[test]
fn test_streaming_vs_batch_rms() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let batch_output = stretch(&input, ¶ms).unwrap();
let stream_output = stream_stretch(&input, params, 4096);
let batch_rms = rms(&batch_output);
let stream_rms = rms(&stream_output);
assert!(
(batch_rms - stream_rms).abs() < batch_rms * 0.4,
"Streaming RMS={} should be close to batch RMS={}",
stream_rms,
batch_rms
);
}
#[test]
fn test_streaming_vs_batch_frequency() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let batch_output = stretch(&input, ¶ms).unwrap();
let stream_output = stream_stretch(&input, params, 4096);
let batch_energy = spectral_energy_at_freq(&batch_output, sample_rate, 440.0);
let stream_energy = spectral_energy_at_freq(&stream_output, sample_rate, 440.0);
assert!(
batch_energy > 0.1 && stream_energy > 0.1,
"440 Hz should be preserved: batch={}, stream={}",
batch_energy,
stream_energy
);
}
#[test]
fn test_streaming_chunk_size_consistency() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let chunk_sizes = [512, 4096, 16384];
let mut rms_values = Vec::new();
for &chunk_size in &chunk_sizes {
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = stream_stretch(&input, params, chunk_size);
rms_values.push(rms(&output));
}
let avg_rms = rms_values.iter().sum::<f32>() / rms_values.len() as f32;
for (i, &r) in rms_values.iter().enumerate() {
assert!(
(r - avg_rms).abs() < avg_rms * 0.3,
"Chunk size {} gave RMS {} (avg={})",
chunk_sizes[i],
r,
avg_rms
);
}
}
#[test]
fn test_streaming_small_chunks() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = stream_stretch(&input, params, 256);
assert!(
!output.is_empty(),
"Small chunks should still produce output"
);
}
#[test]
fn test_streaming_stereo_vs_batch() {
let sample_rate = 44100;
let num_frames = sample_rate as usize;
let mut input = Vec::with_capacity(num_frames * 2);
for i in 0..num_frames {
let t = i as f32 / sample_rate as f32;
input.push((2.0 * PI * 440.0 * t).sin());
input.push((2.0 * PI * 880.0 * t).sin());
}
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(2);
let batch_output = stretch(&input, ¶ms).unwrap();
let stream_output = stream_stretch(&input, params, 4096);
assert_eq!(batch_output.len() % 2, 0);
assert_eq!(stream_output.len() % 2, 0);
let batch_rms = rms(&batch_output);
let stream_rms = rms(&stream_output);
assert!(
(batch_rms - stream_rms).abs() < batch_rms * 0.4,
"Stereo streaming RMS={} should be close to batch RMS={}",
stream_rms,
batch_rms
);
}
#[test]
fn test_streaming_with_edm_preset() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.02)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::DjBeatmatch);
let output = stream_stretch(&input, params, 4096);
assert!(!output.is_empty());
let max = output.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
assert!(max > 0.01, "DJ beatmatch streaming should not be silent");
}
#[test]
fn test_streaming_compression() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
for &ratio in &[0.5, 0.75] {
let params = StretchParams::new(ratio)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = stream_stretch(&input, params, 4096);
assert!(
!output.is_empty(),
"Compression ratio {} should produce output",
ratio
);
let actual_ratio = output.len() as f64 / input.len() as f64;
assert!(
(actual_ratio - ratio).abs() < 0.5,
"Compression ratio {}: expected ~{}, got {}",
ratio,
ratio,
actual_ratio
);
}
}
#[test]
fn test_streaming_empty_flush() {
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let mut processor = StreamProcessor::new(params);
let flushed = processor.flush().unwrap();
assert!(
flushed.is_empty(),
"Flush without input should give empty output"
);
let flushed2 = processor.flush().unwrap();
assert!(flushed2.is_empty(), "Double flush should give empty output");
}
#[test]
fn test_streaming_single_sample_chunks() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, 4410);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = stream_stretch(&input, params, 1);
let _ = output;
}
#[test]
fn test_streaming_large_fft_size() {
let sample_rate = 44100;
let input = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_fft_size(8192);
let output = stream_stretch(&input, params, 4096);
assert!(
!output.is_empty(),
"Large FFT streaming should produce output"
);
}
#[test]
fn test_streaming_latency_reporting() {
let sample_rate = 44100;
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1);
let processor = StreamProcessor::new(params);
let latency = processor.latency_samples();
assert!(
latency < sample_rate as usize * 5,
"Latency {} samples seems too high",
latency
);
}
#[test]
fn test_streaming_from_tempo_dj_workflow() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let chunk_size = 4096;
let mut processor = StreamProcessor::from_tempo(126.0, 128.0, sample_rate, 1);
let expected_ratio = 126.0 / 128.0;
assert!(
(processor.current_stretch_ratio() - expected_ratio).abs() < 1e-6,
"Initial ratio should be {}, got {}",
expected_ratio,
processor.current_stretch_ratio()
);
assert_eq!(processor.source_bpm(), Some(126.0));
assert_eq!(processor.params().preset, Some(EdmPreset::DjBeatmatch));
let mut total_output = Vec::new();
for chunk in signal.chunks(chunk_size) {
total_output.extend_from_slice(&processor.process(chunk).unwrap());
}
total_output.extend_from_slice(&processor.flush().unwrap());
assert!(!total_output.is_empty(), "from_tempo should produce output");
let output_ratio = total_output.len() as f64 / signal.len() as f64;
assert!(
(output_ratio - expected_ratio).abs() < 0.3,
"126→128 BPM: output ratio {} too far from expected {}",
output_ratio,
expected_ratio
);
}
#[test]
fn test_streaming_from_tempo_stereo() {
let sample_rate = 44100u32;
let num_frames = sample_rate as usize;
let mut signal = vec![0.0f32; num_frames * 2];
for i in 0..num_frames {
let t = i as f32 / sample_rate as f32;
signal[i * 2] = (2.0 * std::f32::consts::PI * 440.0 * t).sin();
signal[i * 2 + 1] = (2.0 * std::f32::consts::PI * 880.0 * t).sin();
}
let mut processor = StreamProcessor::from_tempo(120.0, 125.0, sample_rate, 2);
let mut total_output = Vec::new();
for chunk in signal.chunks(8192) {
total_output.extend_from_slice(&processor.process(chunk).unwrap());
}
total_output.extend_from_slice(&processor.flush().unwrap());
assert!(!total_output.is_empty());
assert_eq!(total_output.len() % 2, 0, "Stereo output must be even");
}
#[test]
fn test_streaming_set_tempo_mid_stream() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 4);
let chunk_size = 4096;
let mut processor = StreamProcessor::from_tempo(126.0, 128.0, sample_rate, 1);
let mut total_output = Vec::new();
let chunks: Vec<&[f32]> = signal.chunks(chunk_size).collect();
let mid = chunks.len() / 2;
for chunk in &chunks[..mid] {
total_output.extend_from_slice(&processor.process(chunk).unwrap());
}
assert!(processor.set_tempo(130.0));
for chunk in &chunks[mid..] {
total_output.extend_from_slice(&processor.process(chunk).unwrap());
}
total_output.extend_from_slice(&processor.flush().unwrap());
assert!(
!total_output.is_empty(),
"Should produce output across tempo change"
);
let target_ratio = 126.0 / 130.0;
assert!(
(processor.current_stretch_ratio() - target_ratio).abs() < 0.05,
"After set_tempo(130), ratio should be ~{}, got {}",
target_ratio,
processor.current_stretch_ratio()
);
}
#[test]
fn test_streaming_set_tempo_without_source_returns_false() {
let params = StretchParams::new(1.0)
.with_sample_rate(44100)
.with_channels(1);
let mut processor = StreamProcessor::new(params);
assert!(!processor.set_tempo(128.0));
}
#[test]
fn test_streaming_from_tempo_slowdown() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let mut processor = StreamProcessor::from_tempo(130.0, 120.0, sample_rate, 1);
let mut total_output = Vec::new();
for chunk in signal.chunks(4096) {
total_output.extend_from_slice(&processor.process(chunk).unwrap());
}
total_output.extend_from_slice(&processor.flush().unwrap());
assert!(!total_output.is_empty());
let output_ratio = total_output.len() as f64 / signal.len() as f64;
assert!(
output_ratio > 1.0,
"130→120 BPM should stretch, got ratio {}",
output_ratio
);
}
#[test]
fn test_streaming_blackman_harris_produces_output() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_window_type(WindowType::BlackmanHarris);
let output = stream_stretch(&signal, params, 4096);
assert!(!output.is_empty(), "BH streaming should produce output");
let output_rms = rms(&output);
assert!(
output_rms > 0.1,
"BH streaming output should have energy, got RMS={}",
output_rms
);
}
#[test]
fn test_streaming_kaiser_produces_output() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_window_type(WindowType::Kaiser(800));
let output = stream_stretch(&signal, params, 4096);
assert!(!output.is_empty(), "Kaiser streaming should produce output");
let output_rms = rms(&output);
assert!(
output_rms > 0.1,
"Kaiser streaming output should have energy, got RMS={}",
output_rms
);
}
#[test]
fn test_streaming_different_windows_preserve_frequency() {
let sample_rate = 44100u32;
let freq = 440.0;
let signal = sine_wave(freq, sample_rate, sample_rate as usize * 2);
let windows = [
WindowType::Hann,
WindowType::BlackmanHarris,
WindowType::Kaiser(800),
];
for &win in &windows {
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_window_type(win);
let output = stream_stretch(&signal, params, 4096);
if output.len() < 4096 {
continue; }
let skip = 4096;
let end = output.len().saturating_sub(4096);
if end <= skip {
continue;
}
let trimmed = &output[skip..end];
let energy_440 = spectral_energy_at_freq(trimmed, sample_rate, freq);
let energy_880 = spectral_energy_at_freq(trimmed, sample_rate, freq * 2.0);
assert!(
energy_440 > energy_880 * 2.0,
"Window {:?}: 440 Hz energy ({:.4}) should dominate over 880 Hz ({:.4})",
win,
energy_440,
energy_880
);
}
}
#[test]
fn test_streaming_window_ratio_change() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 4);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_window_type(WindowType::BlackmanHarris);
let mut processor = StreamProcessor::new(params);
let mut output = Vec::new();
for chunk in signal[..signal.len() / 2].chunks(4096) {
if let Ok(out) = processor.process(chunk) {
output.extend_from_slice(&out);
}
}
processor
.set_stretch_ratio(1.05)
.expect("valid stretch ratio");
for chunk in signal[signal.len() / 2..].chunks(4096) {
if let Ok(out) = processor.process(chunk) {
output.extend_from_slice(&out);
}
}
if let Ok(remaining) = processor.flush() {
output.extend_from_slice(&remaining);
}
assert!(
!output.is_empty(),
"BH window with ratio change should produce output"
);
let mut max_diff = 0.0f32;
for i in 1..output.len() {
max_diff = max_diff.max((output[i] - output[i - 1]).abs());
}
assert!(
max_diff < 1.0,
"BH window ratio change should not produce clicks: max_diff={}",
max_diff
);
}
#[test]
fn test_streaming_ambient_preset_uses_blackman_harris() {
let sample_rate = 44100u32;
let signal = sine_wave(440.0, sample_rate, sample_rate as usize * 2);
let params = StretchParams::new(2.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::Ambient);
assert_eq!(params.window_type, WindowType::BlackmanHarris);
let output = stream_stretch(&signal, params, 4096);
assert!(
!output.is_empty(),
"Ambient preset streaming should produce output"
);
let ratio = output.len() as f64 / signal.len() as f64;
assert!(
(ratio - 2.0).abs() < 0.5,
"Ambient preset stream ratio {} too far from 2.0",
ratio
);
}
#[test]
fn test_streaming_normalize_with_window() {
let sample_rate = 44100u32;
let num_samples = sample_rate as usize * 2;
let signal: Vec<f32> = (0..num_samples)
.map(|i| 0.7 * (2.0 * PI * 440.0 * i as f32 / sample_rate as f32).sin())
.collect();
let input_rms = rms(&signal);
let windows = [WindowType::Hann, WindowType::BlackmanHarris];
for &win in &windows {
let params = StretchParams::new(1.5)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_window_type(win)
.with_normalize(true);
let output = stretch(&signal, ¶ms).unwrap();
let output_rms = rms(&output);
assert!(
(output_rms - input_rms).abs() < input_rms * 0.1,
"Window {:?} with normalize: RMS mismatch input={:.4} output={:.4}",
win,
input_rms,
output_rms
);
}
}
#[test]
fn test_streaming_pitch_sweep_no_zipper() {
for extra in [0usize, 512, 1536, 2560] {
run_pitch_sweep_no_zipper(extra);
}
}
fn run_pitch_sweep_no_zipper(extra_samples: usize) {
let sample_rate = 44100u32;
let freq = 1000.0f32;
let num_samples = sample_rate as usize * 2 + extra_samples;
let input = sine_wave(freq, sample_rate, num_samples);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_fft_size(1024)
.with_hop_size(256);
let mut processor = StreamProcessor::new(params);
let chunk = 256usize;
let n_chunks = num_samples / chunk;
let mut output: Vec<f32> = Vec::with_capacity(num_samples * 3);
for (ci, block) in input.chunks(chunk).enumerate() {
let scale = 1.0 + 0.12 * (ci as f64 / (n_chunks - 1) as f64);
processor.set_pitch_scale(scale).unwrap();
processor.process_into(block, &mut output).unwrap();
}
processor.flush_into(&mut output).unwrap();
let max_slew = 2.0 * PI * freq * 1.12 / sample_rate as f32 * 2.5;
let skip = 8192; for (i, w) in output[skip..].windows(2).enumerate() {
let d = (w[1] - w[0]).abs();
assert!(
d <= max_slew,
"extra={}: zipper/click at output sample {}: |delta| = {:.4} > {:.4}",
extra_samples,
skip + i,
d,
max_slew
);
}
let tail_start = output.len().saturating_sub(sample_rate as usize / 2);
let tail = &output[tail_start..output.len().saturating_sub(2048)];
let e_shifted = spectral_energy_at_freq(tail, sample_rate, freq * 1.12);
let e_original = spectral_energy_at_freq(tail, sample_rate, freq);
assert!(
e_shifted > e_original * 2.0,
"extra={}: expected pitch-shifted tone to dominate at stream tail: shifted={:.4} original={:.4}",
extra_samples,
e_shifted,
e_original
);
}
#[test]
fn test_streaming_mono_transient_phase_resets() {
let sample_rate = 44100u32;
let num_samples = sample_rate as usize * 2;
let mut input: Vec<f32> = (0..num_samples)
.map(|i| 0.2 * (2.0 * PI * 220.0 * i as f32 / sample_rate as f32).sin())
.collect();
let click_period = sample_rate as usize / 2; for start in (click_period / 2..num_samples).step_by(click_period) {
for s in input.iter_mut().skip(start).take(24) {
*s += 1.5;
}
}
let params = StretchParams::new(1.25)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_fft_size(1024)
.with_hop_size(256);
let mut processor = StreamProcessor::new(params);
let mut output: Vec<f32> = Vec::with_capacity(num_samples * 3);
for chunk in input.chunks(256) {
processor.process_into(chunk, &mut output).unwrap();
}
processor.flush_into(&mut output).unwrap();
let stats = processor.transient_reset_stats();
assert!(
stats.events_detected_total > 0,
"mono stream should schedule transient phase resets, got {:?}",
stats
);
assert!(!output.is_empty());
}