use timestretch::{AudioBuffer, EdmPreset, StreamProcessor, StretchParams, WindowType};
#[allow(dead_code)]
fn sine_mono(freq: f32, sample_rate: u32, num_samples: usize) -> AudioBuffer {
let data: Vec<f32> = (0..num_samples)
.map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate as f32).sin())
.collect();
AudioBuffer::from_mono(data, sample_rate)
}
fn assert_finite(buf: &AudioBuffer, label: &str) {
for (i, &s) in buf.data.iter().enumerate() {
assert!(
s.is_finite(),
"{}: non-finite at sample {}: {}",
label,
i,
s
);
}
}
#[test]
fn silence_as_gap_between_stretched_segments() {
let tone = AudioBuffer::tone(440.0, 44100, 0.5, 0.8);
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&tone, ¶ms).unwrap();
let gap = AudioBuffer::silence(44100, 0.2);
let combined = AudioBuffer::concatenate(&[&stretched, &gap, &stretched]);
assert_finite(&combined, "silence gap");
let expected_secs = stretched.duration_secs() * 2.0 + 0.2;
assert!(
(combined.duration_secs() - expected_secs).abs() < 0.01,
"Duration mismatch: expected ~{:.2}s, got {:.2}s",
expected_secs,
combined.duration_secs()
);
}
#[test]
fn silence_mix_with_tone() {
let silence = AudioBuffer::silence(44100, 1.0);
let tone = AudioBuffer::tone(440.0, 44100, 1.0, 0.5);
let mixed = silence.mix(&tone);
for (a, b) in mixed.data.iter().zip(tone.data.iter()) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn tone_stretch_preserves_energy() {
let tone = AudioBuffer::tone(440.0, 44100, 2.0, 0.7);
let input_rms = tone.rms();
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1)
.with_normalize(true);
let stretched = timestretch::stretch_buffer(&tone, ¶ms).unwrap();
assert!(
(stretched.rms() - input_rms).abs() < input_rms * 0.1,
"RMS mismatch: input={:.4}, stretched={:.4}",
input_rms,
stretched.rms()
);
}
#[test]
fn tone_at_different_frequencies_stretch() {
for &freq in &[60.0f32, 440.0, 8000.0] {
let tone = AudioBuffer::tone(freq as f64, 44100, 1.0, 0.5);
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&tone, ¶ms).unwrap();
assert!(
!stretched.is_empty(),
"Tone at {}Hz produced no output",
freq
);
assert!(
stretched.rms() > 0.01,
"Tone at {}Hz produced silence after stretch",
freq
);
assert_finite(&stretched, &format!("tone_{}Hz", freq));
}
}
#[test]
fn tone_pitch_shift_octave_up() {
let tone = AudioBuffer::tone(440.0, 44100, 1.0, 0.8);
let params = StretchParams::new(1.0)
.with_sample_rate(44100)
.with_channels(1);
let shifted = timestretch::pitch_shift_buffer(&tone, ¶ms, 2.0).unwrap();
assert_eq!(shifted.num_frames(), tone.num_frames());
assert_finite(&shifted, "pitch_shift_octave_up");
}
#[test]
fn pan_then_stretch_stereo() {
let mono = AudioBuffer::tone(440.0, 44100, 1.0, 0.7);
let stereo = mono.pan(0.3); assert!(stereo.is_stereo());
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(2)
.with_preset(EdmPreset::HouseLoop);
let stretched = timestretch::stretch_buffer(&stereo, ¶ms).unwrap();
assert!(stretched.is_stereo());
assert!(!stretched.is_empty());
assert_finite(&stretched, "pan_then_stretch");
}
#[test]
fn stereo_field_from_two_panned_tones() {
let tone_l = AudioBuffer::tone(440.0, 44100, 1.0, 0.6).pan(-0.7);
let tone_r = AudioBuffer::tone(880.0, 44100, 1.0, 0.4).pan(0.7);
let stereo = tone_l.mix(&tone_r);
let params = StretchParams::new(1.25)
.with_sample_rate(44100)
.with_channels(2);
let stretched = timestretch::stretch_buffer(&stereo, ¶ms).unwrap();
assert!(stretched.is_stereo());
assert_finite(&stretched, "panned_stereo_stretch");
}
#[test]
fn pan_sweep_automation() {
let tone = AudioBuffer::tone(440.0, 44100, 0.5, 0.6);
let left = tone.pan(-1.0);
let center = tone.pan(0.0);
let right = tone.pan(1.0);
let sweep = AudioBuffer::concatenate(&[&left, ¢er, &right]);
assert_eq!(sweep.num_frames(), tone.num_frames() * 3);
assert!(sweep.is_stereo());
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(2);
let stretched = timestretch::stretch_buffer(&sweep, ¶ms).unwrap();
assert_finite(&stretched, "pan_sweep");
assert!(stretched.is_stereo());
}
#[test]
fn sidechain_duck_effect() {
let pad = AudioBuffer::tone(220.0, 44100, 2.0, 0.7);
let breakpoints: Vec<(f64, f32)> = (0..8)
.flat_map(|i| {
let t = i as f64 * 0.25;
vec![(t, 1.0), (t + 0.02, 0.2), (t + 0.15, 1.0)]
})
.collect();
let ducked = pad.with_gain_envelope(&breakpoints);
assert_eq!(ducked.num_frames(), pad.num_frames());
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&ducked, ¶ms).unwrap();
assert!(!stretched.is_empty());
assert_finite(&stretched, "sidechain_duck");
}
#[test]
fn gain_envelope_then_stretch_preserves_shape() {
let tone = AudioBuffer::tone(440.0, 44100, 1.0, 0.8);
let enveloped = tone.with_gain_envelope(&[(0.0, 0.0), (0.5, 1.0), (1.0, 1.0)]);
let first_quarter = enveloped.slice(0, 11025);
let last_quarter = enveloped.slice(33075, 11025);
assert!(
first_quarter.rms() < last_quarter.rms(),
"Envelope shape not preserved: first_rms={:.4} >= last_rms={:.4}",
first_quarter.rms(),
last_quarter.rms()
);
let params = StretchParams::new(2.0)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&enveloped, ¶ms).unwrap();
assert_finite(&stretched, "envelope_stretch");
}
#[test]
fn volume_automation_stereo() {
let mono = AudioBuffer::tone(440.0, 44100, 1.0, 0.6);
let stereo = mono.pan(0.0);
let automated = stereo.with_gain_envelope(&[(0.0, 0.5), (0.5, 1.0), (1.0, 0.0)]);
let params = StretchParams::new(0.75)
.with_sample_rate(44100)
.with_channels(2);
let stretched = timestretch::stretch_buffer(&automated, ¶ms).unwrap();
assert!(stretched.is_stereo());
assert_finite(&stretched, "volume_auto_stereo");
}
#[test]
fn remove_dc_before_stretch() {
let data: Vec<f32> = (0..44100)
.map(|i| 0.5 + 0.3 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
.collect();
let mean_before: f64 = data.iter().map(|&s| s as f64).sum::<f64>() / data.len() as f64;
assert!(mean_before.abs() > 0.4, "Should have DC offset");
let buf = AudioBuffer::from_mono(data, 44100);
let centered = buf.remove_dc();
let mean_after: f64 =
centered.data.iter().map(|&s| s as f64).sum::<f64>() / centered.data.len() as f64;
assert!(
mean_after.abs() < 0.01,
"DC should be removed: {}",
mean_after
);
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(¢ered, ¶ms).unwrap();
assert_finite(&stretched, "dc_removed_stretch");
assert!(stretched.rms() > 0.01);
}
#[test]
fn remove_dc_stereo_then_stretch() {
let l: Vec<f32> = (0..44100)
.map(|i| 0.3 + 0.5 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
.collect();
let r: Vec<f32> = (0..44100)
.map(|i| -0.2 + 0.5 * (2.0 * std::f32::consts::PI * 880.0 * i as f32 / 44100.0).sin())
.collect();
let buf = AudioBuffer::from_channels(&[l, r], 44100);
let centered = buf.remove_dc();
let params = StretchParams::new(1.25)
.with_sample_rate(44100)
.with_channels(2);
let stretched = timestretch::stretch_buffer(¢ered, ¶ms).unwrap();
assert!(stretched.is_stereo());
assert_finite(&stretched, "dc_stereo_stretch");
}
#[test]
fn window_then_stretch_for_granular_synthesis() {
let grain = AudioBuffer::tone(440.0, 44100, 0.05, 0.8); let windowed = grain.apply_window(WindowType::Hann);
assert!(windowed.data[0].abs() < 0.01);
assert!(windowed.data[windowed.data.len() - 1].abs() < 0.01);
let params = StretchParams::new(2.0)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&windowed, ¶ms).unwrap();
assert_finite(&stretched, "windowed_grain_stretch");
}
#[test]
fn window_types_all_work_with_stretch() {
let tone = AudioBuffer::tone(440.0, 44100, 0.5, 0.6);
for wt in &[
WindowType::Hann,
WindowType::BlackmanHarris,
WindowType::Kaiser(12),
] {
let windowed = tone.apply_window(*wt);
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1);
let stretched = timestretch::stretch_buffer(&windowed, ¶ms).unwrap();
assert!(!stretched.is_empty(), "{:?} produced empty output", wt);
assert_finite(&stretched, &format!("{:?}", wt));
}
}
#[test]
fn reverse_riser_effect() {
let tone = AudioBuffer::tone(220.0, 44100, 1.0, 0.7);
let reversed = tone.reverse();
let faded = reversed.fade_in(22050);
let params = StretchParams::new(2.0)
.with_sample_rate(44100)
.with_channels(1)
.with_preset(EdmPreset::Ambient);
let stretched = timestretch::stretch_buffer(&faded, ¶ms).unwrap();
assert_finite(&stretched, "reverse_riser");
assert!(stretched.duration_secs() > 1.5);
}
#[test]
fn tape_stop_effect() {
let tone = AudioBuffer::tone(440.0, 44100, 1.0, 0.8);
let decelerated =
tone.with_gain_envelope(&[(0.0, 1.0), (0.3, 0.9), (0.6, 0.6), (0.8, 0.3), (1.0, 0.0)]);
let params = StretchParams::new(2.0)
.with_sample_rate(44100)
.with_channels(1)
.with_preset(EdmPreset::Halftime);
let stretched = timestretch::stretch_buffer(&decelerated, ¶ms).unwrap();
assert_finite(&stretched, "tape_stop");
}
#[test]
fn dj_transition_with_pan_and_crossfade() {
let track_a = AudioBuffer::tone(440.0, 44100, 2.0, 0.6).pan(-0.3);
let track_b = AudioBuffer::tone(660.0, 44100, 2.0, 0.6).pan(0.3);
let params = StretchParams::new(128.0 / 126.0)
.with_sample_rate(44100)
.with_channels(2)
.with_preset(EdmPreset::DjBeatmatch);
let a_stretched = timestretch::stretch_buffer(&track_a, ¶ms).unwrap();
let b_stretched = timestretch::stretch_buffer(&track_b, ¶ms).unwrap();
let transition = a_stretched.crossfade_into(&b_stretched, 22050);
assert!(transition.is_stereo());
assert_finite(&transition, "dj_transition");
}
#[test]
fn layered_synth_pad_production() {
let osc1 = AudioBuffer::tone(440.0, 44100, 2.0, 0.3);
let osc2 = AudioBuffer::tone(440.5, 44100, 2.0, 0.3); let osc3 = AudioBuffer::tone(439.5, 44100, 2.0, 0.3);
let pad = osc1.mix(&osc2).mix(&osc3);
let enveloped = pad.with_gain_envelope(&[(0.0, 0.0), (0.5, 1.0), (1.5, 1.0), (2.0, 0.0)]);
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1)
.with_preset(EdmPreset::Ambient);
let stretched = timestretch::stretch_buffer(&enveloped, ¶ms).unwrap();
assert_finite(&stretched, "synth_pad");
assert!(stretched.duration_secs() > 2.5);
}
#[test]
fn granular_freeze_effect() {
let source = AudioBuffer::tone(440.0, 44100, 1.0, 0.7);
let grain = source.slice(22050, 2205); let windowed = grain.apply_window(WindowType::Hann);
let repeated = windowed.repeat(20);
let params = StretchParams::new(3.0)
.with_sample_rate(44100)
.with_channels(1)
.with_preset(EdmPreset::Ambient);
let stretched = timestretch::stretch_buffer(&repeated, ¶ms).unwrap();
assert_finite(&stretched, "granular_freeze");
assert!(stretched.duration_secs() > 2.0);
}
#[test]
fn dc_removal_in_processing_chain() {
let data: Vec<f32> = (0..88200)
.map(|i| 0.3 + 0.5 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
.collect();
let buf = AudioBuffer::from_mono(data, 44100);
let clean = buf.remove_dc();
let params = StretchParams::new(1.5)
.with_sample_rate(44100)
.with_channels(1)
.with_normalize(true);
let stretched = timestretch::stretch_buffer(&clean, ¶ms).unwrap();
let normalized = stretched.normalize(0.9);
assert_finite(&normalized, "dc_chain");
assert!(normalized.peak() <= 0.9 + 0.01);
}
#[test]
fn streaming_with_tone_factory() {
let tone = AudioBuffer::tone(440.0, 44100, 2.0, 0.6);
let mut proc = StreamProcessor::from_tempo(126.0, 128.0, 44100, 1);
let mut output = Vec::new();
for chunk in tone.data.chunks(4096) {
if let Ok(out) = proc.process(chunk) {
output.extend_from_slice(&out);
}
}
if let Ok(out) = proc.flush() {
output.extend_from_slice(&out);
}
assert!(
!output.is_empty(),
"Streaming with tone factory should produce output"
);
let target = proc.target_bpm().unwrap();
assert!((target - 128.0).abs() < 0.1);
}
#[test]
fn streaming_target_ratio_tracks_changes() {
let mut proc = StreamProcessor::from_tempo(126.0, 128.0, 44100, 1);
let initial_target = proc.target_stretch_ratio();
assert!((initial_target - 126.0 / 128.0).abs() < 1e-6);
proc.set_tempo(130.0);
let new_target = proc.target_stretch_ratio();
assert!(
(new_target - 126.0 / 130.0).abs() < 1e-6,
"Target ratio should update immediately: expected {}, got {}",
126.0 / 130.0,
new_target
);
assert!(
(proc.current_stretch_ratio() - initial_target).abs() < 0.1,
"Current ratio should lag behind target"
);
}