use timestretch::{AudioBuffer, EdmPreset, StretchParams};
const SAMPLE_RATE: u32 = 44100;
fn make_sine(freq: f32, duration_secs: f32, sample_rate: u32) -> Vec<f32> {
let n = (duration_secs * sample_rate as f32) as usize;
(0..n)
.map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate as f32).sin())
.collect()
}
fn make_stereo_sine(freq_l: f32, freq_r: f32, duration_secs: f32, sample_rate: u32) -> Vec<f32> {
let n = (duration_secs * sample_rate as f32) as usize;
let mut data = Vec::with_capacity(n * 2);
for i in 0..n {
let t = i as f32 / sample_rate as f32;
data.push((2.0 * std::f32::consts::PI * freq_l * t).sin());
data.push((2.0 * std::f32::consts::PI * freq_r * t).sin());
}
data
}
#[test]
fn test_resample_then_stretch() {
let samples = make_sine(440.0, 1.0, 48000);
let buf = AudioBuffer::from_mono(samples, 48000);
let resampled = buf.resample(SAMPLE_RATE);
let params = StretchParams::new(1.5)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE);
let output = timestretch::stretch_buffer(&resampled, ¶ms).unwrap();
let ratio = output.num_frames() as f64 / resampled.num_frames() as f64;
assert!(
(ratio - 1.5).abs() < 0.15,
"Stretch ratio should be ~1.5, got {}",
ratio
);
assert!(output.data.iter().all(|s| s.is_finite()));
}
#[test]
fn test_resample_stereo_preserves_channels() {
let data = make_stereo_sine(440.0, 880.0, 0.5, 48000);
let buf = AudioBuffer::from_stereo(data, 48000);
let resampled = buf.resample(SAMPLE_RATE);
assert!(resampled.is_stereo());
assert_eq!(resampled.channel_count(), 2);
assert!((resampled.duration_secs() - 0.5).abs() < 0.02);
}
#[test]
fn test_resample_roundtrip_preserves_frequency() {
let samples = make_sine(440.0, 0.5, SAMPLE_RATE);
let buf = AudioBuffer::from_mono(samples, SAMPLE_RATE);
let up = buf.resample(48000);
let back = up.resample(SAMPLE_RATE);
assert_eq!(back.num_frames(), buf.num_frames());
let skip = 100;
let end = buf.num_frames() - skip;
let mut max_err: f32 = 0.0;
for i in skip..end {
let err = (buf.data[i] - back.data[i]).abs();
if err > max_err {
max_err = err;
}
}
assert!(
max_err < 0.1,
"Resample round-trip max error too high: {}",
max_err
);
}
#[test]
fn test_crossfade_two_stretched_tracks() {
let track_a = make_sine(440.0, 1.0, SAMPLE_RATE);
let track_b = make_sine(660.0, 1.0, SAMPLE_RATE);
let buf_a = AudioBuffer::from_mono(track_a, SAMPLE_RATE);
let buf_b = AudioBuffer::from_mono(track_b, SAMPLE_RATE);
let params_a = StretchParams::new(1.02)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE)
.with_preset(EdmPreset::DjBeatmatch);
let params_b = StretchParams::new(0.98)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE)
.with_preset(EdmPreset::DjBeatmatch);
let stretched_a = timestretch::stretch_buffer(&buf_a, ¶ms_a).unwrap();
let stretched_b = timestretch::stretch_buffer(&buf_b, ¶ms_b).unwrap();
let mixed = stretched_a.crossfade_into(&stretched_b, 2000);
assert!(!mixed.is_empty());
let expected_len = stretched_a.num_frames() + stretched_b.num_frames() - 2000;
assert_eq!(mixed.num_frames(), expected_len);
assert!(mixed.data.iter().all(|s| s.is_finite()));
assert!(
mixed.data.iter().all(|s| s.abs() <= 2.0),
"Crossfade output should not clip excessively"
);
}
#[test]
fn test_crossfade_stereo_dj_workflow() {
let data_a = make_stereo_sine(440.0, 880.0, 0.5, SAMPLE_RATE);
let data_b = make_stereo_sine(330.0, 660.0, 0.5, SAMPLE_RATE);
let buf_a = AudioBuffer::from_stereo(data_a, SAMPLE_RATE);
let buf_b = AudioBuffer::from_stereo(data_b, SAMPLE_RATE);
let mixed = buf_a.crossfade_into(&buf_b, 1000);
let expected_frames = (SAMPLE_RATE as usize / 2) * 2 - 1000;
assert_eq!(mixed.num_frames(), expected_frames);
assert!(mixed.is_stereo());
assert!(mixed.data.iter().all(|s| s.is_finite()));
}
#[test]
fn test_reverse_preserves_length() {
let buf = AudioBuffer::from_mono(make_sine(440.0, 0.5, SAMPLE_RATE), SAMPLE_RATE);
let rev = buf.reverse();
assert_eq!(rev.num_frames(), buf.num_frames());
assert_eq!(rev.sample_rate, buf.sample_rate);
}
#[test]
fn test_reverse_stereo_preserves_channel_pairing() {
let data = make_stereo_sine(440.0, 880.0, 0.1, SAMPLE_RATE);
let buf = AudioBuffer::from_stereo(data, SAMPLE_RATE);
let rev = buf.reverse();
let last_frame = buf.num_frames() - 1;
assert_eq!(rev.data[0], buf.data[last_frame * 2]); assert_eq!(rev.data[1], buf.data[last_frame * 2 + 1]); }
#[test]
fn test_reverse_then_stretch() {
let buf = AudioBuffer::from_mono(make_sine(440.0, 0.5, SAMPLE_RATE), SAMPLE_RATE);
let rev = buf.reverse();
let params = StretchParams::new(2.0)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE)
.with_preset(EdmPreset::Halftime);
let output = timestretch::stretch_buffer(&rev, ¶ms).unwrap();
assert!(!output.is_empty());
assert!(output.data.iter().all(|s| s.is_finite()));
}
#[test]
fn test_full_dj_workflow_resample_stretch_crossfade() {
let track_a = AudioBuffer::from_mono(make_sine(440.0, 1.0, 48000), 48000);
let track_b = AudioBuffer::from_mono(make_sine(330.0, 1.0, SAMPLE_RATE), SAMPLE_RATE);
let track_a_resampled = track_a.resample(SAMPLE_RATE);
assert_eq!(track_a_resampled.sample_rate, SAMPLE_RATE);
let params = StretchParams::new(126.0 / 128.0)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE)
.with_preset(EdmPreset::DjBeatmatch);
let stretched_a = timestretch::stretch_buffer(&track_a_resampled, ¶ms).unwrap();
let params_b = StretchParams::new(130.0 / 128.0)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE)
.with_preset(EdmPreset::DjBeatmatch);
let stretched_b = timestretch::stretch_buffer(&track_b, ¶ms_b).unwrap();
let mixed = stretched_a.crossfade_into(&stretched_b, 4410); assert!(!mixed.is_empty());
assert!(mixed.data.iter().all(|s| s.is_finite()));
}
#[test]
fn test_reverse_cymbal_build() {
let buf = AudioBuffer::from_mono(make_sine(1000.0, 0.5, SAMPLE_RATE), SAMPLE_RATE);
let reversed = buf.reverse().fade_in(buf.num_frames());
let mixed = reversed.crossfade_into(&buf, 2000);
assert!(!mixed.is_empty());
assert!(mixed.data.iter().all(|s| s.is_finite()));
let crossfade_start = reversed.num_frames() - 2000;
for i in crossfade_start..(crossfade_start + 2000).min(mixed.num_frames() - 1) {
let diff = (mixed.data[i + 1] - mixed.data[i]).abs();
assert!(diff < 1.0, "Unexpected discontinuity at sample {}", i);
}
}
#[test]
fn test_slice_stretch_concatenate() {
let buf = AudioBuffer::from_mono(make_sine(440.0, 1.0, SAMPLE_RATE), SAMPLE_RATE);
let first_half = buf.slice(0, SAMPLE_RATE as usize / 2);
let second_half = buf.slice(SAMPLE_RATE as usize / 2, SAMPLE_RATE as usize / 2);
let params_slow = StretchParams::new(1.5)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE);
let params_fast = StretchParams::new(0.75)
.with_channels(1)
.with_sample_rate(SAMPLE_RATE);
let stretched_first = timestretch::stretch_buffer(&first_half, ¶ms_slow).unwrap();
let stretched_second = timestretch::stretch_buffer(&second_half, ¶ms_fast).unwrap();
let combined = stretched_first.crossfade_into(&stretched_second, 1000);
assert!(!combined.is_empty());
assert!(combined.data.iter().all(|s| s.is_finite()));
}
#[test]
fn test_channel_count_after_operations() {
let mono = AudioBuffer::from_mono(vec![0.0; 100], SAMPLE_RATE);
let stereo = AudioBuffer::from_stereo(vec![0.0; 200], SAMPLE_RATE);
assert_eq!(mono.channel_count(), 1);
assert_eq!(stereo.channel_count(), 2);
assert_eq!(mono.resample(48000).channel_count(), 1);
assert_eq!(stereo.resample(48000).channel_count(), 2);
assert_eq!(mono.reverse().channel_count(), 1);
assert_eq!(stereo.reverse().channel_count(), 2);
}