#[allow(dead_code)]
#[path = "ab/mod.rs"]
mod ab;
use ab::{render_new_keylock_with_artifact, render_with_rate_schedule, Arm};
use timestretch::{PreAnalysisArtifact, StreamProfile};
const SAMPLE_RATE: u32 = 44_100;
const CALLBACK_FRAMES: usize = 256;
const CLICK_PERIOD: usize = SAMPLE_RATE as usize / 2;
const FIRST_CLICK: usize = CLICK_PERIOD / 2;
fn edm_click_train(num_samples: usize) -> Vec<f32> {
let mut input: Vec<f32> = (0..num_samples)
.map(|i| 0.2 * (2.0 * std::f32::consts::PI * 220.0 * i as f32 / SAMPLE_RATE as f32).sin())
.collect();
for start in (FIRST_CLICK..num_samples).step_by(CLICK_PERIOD) {
for s in input.iter_mut().skip(start).take(24) {
*s += 1.5;
}
}
input
}
fn onset_sharpness(output: &[f32], expected: usize, search: usize) -> Option<f32> {
let lo = expected.saturating_sub(search);
let hi = (expected + search).min(output.len());
if hi <= lo + 1 {
return None;
}
Some(
output[lo..hi]
.windows(2)
.map(|w| w[1] - w[0])
.fold(0.0f32, f32::max),
)
}
fn median_sharpness(output: &[f32], rate: f64) -> f64 {
let mut scores: Vec<f32> = Vec::new();
let mut k = 0usize;
loop {
let source_pos = FIRST_CLICK + k * CLICK_PERIOD;
let expected = (source_pos as f64 / rate) as usize + 560;
if expected + 2_048 >= output.len() {
break;
}
if k > 0 {
if let Some(s) = onset_sharpness(output, expected, 1_024) {
scores.push(s);
}
}
k += 1;
}
assert!(
scores.len() >= 10,
"too few onsets scored: {}",
scores.len()
);
scores.sort_by(|a, b| a.total_cmp(b));
scores[scores.len() / 2] as f64
}
fn click_train_artifact(num_samples: usize) -> std::sync::Arc<PreAnalysisArtifact> {
let onsets: Vec<usize> = (0..)
.map(|k| FIRST_CLICK + k * CLICK_PERIOD)
.take_while(|&p| p < num_samples)
.collect();
std::sync::Arc::new(PreAnalysisArtifact {
version: timestretch::PREANALYSIS_VERSION,
sample_rate: SAMPLE_RATE,
bpm: 120.0,
confidence: 0.95,
transient_strengths: vec![0.9; onsets.len()],
transient_onsets: onsets.clone(),
beat_positions: onsets,
..Default::default()
})
}
#[test]
fn keylock_artifact_guidance_preserves_transients_at_least_as_well_as_online() {
let num_samples = SAMPLE_RATE as usize * 10;
let input = edm_click_train(num_samples);
let artifact = click_train_artifact(num_samples);
for rate in [1.04f64, 0.96] {
let with_artifact = render_new_keylock_with_artifact(
artifact.clone(),
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let online = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let artifact_sharpness = median_sharpness(&with_artifact.output, rate);
let online_sharpness = median_sharpness(&online.output, rate);
println!(
"artifact guidance @rate {rate}: artifact {artifact_sharpness:.3} | online {online_sharpness:.3}"
);
assert!(
artifact_sharpness >= online_sharpness * 0.95,
"rate {rate}: artifact-guided sharpness {artifact_sharpness:.3} \
below online {online_sharpness:.3}"
);
assert_eq!(with_artifact.underrun_frames, 0);
}
}
#[test]
fn keylock_transient_sharpness_at_dj_rates_beats_old_engine() {
let input = edm_click_train(SAMPLE_RATE as usize * 10);
for rate in [1.04f64, 0.96] {
let new = render_with_rate_schedule(
Arm::NewKeylock,
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let old = render_with_rate_schedule(
Arm::OldVarispeed(StreamProfile::Live),
&input,
1,
SAMPLE_RATE,
CALLBACK_FRAMES,
&|_| rate,
);
let new_sharpness = median_sharpness(&new.output, rate);
let old_sharpness = median_sharpness(&old.output, rate);
println!(
"transient sharpness @rate {rate}: new {new_sharpness:.3} | old {old_sharpness:.3} (source 1.5)"
);
assert!(
new_sharpness >= old_sharpness * 0.95,
"rate {rate}: new sharpness {new_sharpness:.3} below old {old_sharpness:.3}"
);
assert_eq!(new.underrun_frames, 0);
}
}