use std::f32::consts::PI;
use timestretch::{AudioBuffer, EdmPreset, StretchError, StretchParams};
fn generate_sine(freq: f32, sample_rate: u32, duration_secs: f64) -> Vec<f32> {
let num_samples = (sample_rate as f64 * duration_secs) as usize;
(0..num_samples)
.map(|i| (2.0 * PI * freq * i as f32 / sample_rate as f32).sin())
.collect()
}
fn generate_click_train(bpm: f64, sample_rate: u32, duration_secs: f64) -> Vec<f32> {
let num_samples = (sample_rate as f64 * duration_secs) as usize;
let beat_interval = (60.0 * sample_rate as f64 / bpm) as usize;
let mut samples = vec![0.0f32; num_samples];
for pos in (0..num_samples).step_by(beat_interval) {
for j in 0..10.min(num_samples - pos) {
samples[pos + j] = if j < 5 { 0.9 } else { -0.4 };
}
}
for (i, sample) in samples.iter_mut().enumerate() {
*sample += 0.2 * (2.0 * PI * 200.0 * i as f32 / sample_rate as f32).sin();
}
samples
}
#[test]
fn test_bpm_stretch_126_to_128() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::DjBeatmatch);
let output = timestretch::stretch_to_bpm(&input, 126.0, 128.0, ¶ms).unwrap();
let expected_ratio = 126.0 / 128.0;
let actual_ratio = output.len() as f64 / input.len() as f64;
assert!(
(actual_ratio - expected_ratio).abs() < 0.3,
"126->128 BPM: expected ratio ~{:.4}, got {:.4}",
expected_ratio,
actual_ratio
);
}
#[test]
fn test_bpm_stretch_128_to_126() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::DjBeatmatch);
let output = timestretch::stretch_to_bpm(&input, 128.0, 126.0, ¶ms).unwrap();
assert!(
output.len() > input.len(),
"Slowing from 128 to 126 BPM should produce longer output"
);
}
#[test]
fn test_bpm_stretch_halftime() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::Halftime);
let output = timestretch::stretch_to_bpm(&input, 128.0, 64.0, ¶ms).unwrap();
let actual_ratio = output.len() as f64 / input.len() as f64;
assert!(
(actual_ratio - 2.0).abs() < 0.5,
"Halftime (128->64): expected ratio ~2.0, got {:.4}",
actual_ratio
);
}
#[test]
fn test_bpm_stretch_doubletime() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1);
let output = timestretch::stretch_to_bpm(&input, 128.0, 256.0, ¶ms).unwrap();
let actual_ratio = output.len() as f64 / input.len() as f64;
assert!(
(actual_ratio - 0.5).abs() < 0.3,
"Doubletime (128->256): expected ratio ~0.5, got {:.4}",
actual_ratio
);
}
#[test]
fn test_bpm_stretch_preserves_rms_energy() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let input_rms = (input.iter().map(|x| x * x).sum::<f32>() / input.len() as f32).sqrt();
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::DjBeatmatch);
let output = timestretch::stretch_to_bpm(&input, 126.0, 128.0, ¶ms).unwrap();
let output_rms = (output.iter().map(|x| x * x).sum::<f32>() / output.len() as f32).sqrt();
assert!(
(output_rms - input_rms).abs() < input_rms * 0.5,
"RMS not preserved: input={:.4}, output={:.4}",
input_rms,
output_rms
);
}
#[test]
fn test_bpm_stretch_no_nan_or_inf() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1);
for (source, target) in &[(120.0, 128.0), (128.0, 90.0), (140.0, 70.0), (100.0, 160.0)] {
let output = timestretch::stretch_to_bpm(&input, *source, *target, ¶ms).unwrap();
for (i, &sample) in output.iter().enumerate() {
assert!(
sample.is_finite(),
"NaN/Inf at sample {} for {}->{}BPM",
i,
source,
target
);
}
}
}
#[test]
fn test_bpm_stretch_stereo() {
let sample_rate = 44100u32;
let num_frames = 44100 * 2;
let mut input = vec![0.0f32; num_frames * 2];
for i in 0..num_frames {
let t = i as f32 / sample_rate as f32;
input[i * 2] = (2.0 * PI * 440.0 * t).sin();
input[i * 2 + 1] = (2.0 * PI * 880.0 * t).sin();
}
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(2)
.with_preset(EdmPreset::DjBeatmatch);
let output = timestretch::stretch_to_bpm(&input, 126.0, 128.0, ¶ms).unwrap();
assert!(!output.is_empty());
assert_eq!(output.len() % 2, 0, "Stereo output must have even length");
}
#[test]
fn test_bpm_stretch_with_all_presets() {
let sample_rate = 44100u32;
let input = generate_sine(440.0, sample_rate, 2.0);
for preset in &[
EdmPreset::DjBeatmatch,
EdmPreset::HouseLoop,
EdmPreset::Halftime,
EdmPreset::Ambient,
EdmPreset::VocalChop,
] {
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(*preset);
let output = timestretch::stretch_to_bpm(&input, 126.0, 128.0, ¶ms).unwrap();
assert!(
!output.is_empty(),
"Preset {:?} produced empty output",
preset
);
}
}
#[test]
fn test_bpm_stretch_buffer_api() {
let sample_rate = 44100u32;
let buffer = AudioBuffer::from_mono(generate_sine(440.0, sample_rate, 2.0), sample_rate);
let params = StretchParams::new(1.0).with_preset(EdmPreset::DjBeatmatch);
let output = timestretch::stretch_bpm_buffer(&buffer, 126.0, 128.0, ¶ms).unwrap();
assert_eq!(output.sample_rate, sample_rate);
assert_eq!(output.channels, timestretch::Channels::Mono);
assert!(!output.data.is_empty());
assert!(output.data.len() < buffer.data.len());
}
#[test]
fn test_bpm_stretch_auto_with_clicks() {
let sample_rate = 44100u32;
let input = generate_click_train(120.0, sample_rate, 4.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1);
let result = timestretch::stretch_to_bpm_auto(&input, 128.0, ¶ms);
match result {
Ok(output) => {
assert!(!output.is_empty());
}
Err(StretchError::BpmDetectionFailed(_)) => {
}
Err(e) => panic!("Unexpected error: {}", e),
}
}
#[test]
fn test_bpm_stretch_auto_invalid_target() {
let params = StretchParams::new(1.0)
.with_sample_rate(44100)
.with_channels(1);
let input = vec![0.0f32; 44100];
let result = timestretch::stretch_to_bpm_auto(&input, 0.0, ¶ms);
assert!(result.is_err());
match result {
Err(StretchError::BpmDetectionFailed(_)) => {} other => panic!("Expected BpmDetectionFailed, got {:?}", other),
}
let result = timestretch::stretch_to_bpm_auto(&input, -128.0, ¶ms);
assert!(result.is_err());
}
#[test]
fn test_bpm_ratio_utility() {
assert!((timestretch::bpm_ratio(120.0, 120.0) - 1.0).abs() < 1e-10);
assert!((timestretch::bpm_ratio(120.0, 60.0) - 2.0).abs() < 1e-10);
assert!((timestretch::bpm_ratio(60.0, 120.0) - 0.5).abs() < 1e-10);
let ratio = timestretch::bpm_ratio(126.0, 128.0);
assert!((ratio - 0.984375).abs() < 1e-6);
let ratio = timestretch::bpm_ratio(128.0, 130.0);
assert!((ratio - 128.0 / 130.0).abs() < 1e-10);
}
#[test]
fn test_bpm_stretch_invalid_bpm_values() {
let params = StretchParams::new(1.0)
.with_sample_rate(44100)
.with_channels(1);
let input = vec![0.0f32; 44100];
assert!(timestretch::stretch_to_bpm(&input, 0.0, 128.0, ¶ms).is_err());
assert!(timestretch::stretch_to_bpm(&input, 128.0, 0.0, ¶ms).is_err());
assert!(timestretch::stretch_to_bpm(&input, -120.0, 128.0, ¶ms).is_err());
assert!(timestretch::stretch_to_bpm(&input, 120.0, -128.0, ¶ms).is_err());
}
#[test]
fn test_bpm_stretch_48khz() {
let sample_rate = 48000u32;
let input = generate_sine(440.0, sample_rate, 2.0);
let params = StretchParams::new(1.0)
.with_sample_rate(sample_rate)
.with_channels(1)
.with_preset(EdmPreset::HouseLoop);
let output = timestretch::stretch_to_bpm(&input, 126.0, 128.0, ¶ms).unwrap();
assert!(!output.is_empty());
assert!(output.len() < input.len());
}