use crate::error::AnalysisError;
const EPSILON: f32 = 1e-10;
#[derive(Debug, Clone)]
pub struct AutocorrTempogramResult {
pub bpm: f32,
pub confidence: f32,
pub strength: f32,
}
pub fn autocorrelation_tempogram(
novelty_curve: &[f32],
sample_rate: u32,
hop_size: u32,
min_bpm: f32,
max_bpm: f32,
bpm_resolution: f32,
) -> Result<Vec<(f32, f32)>, AnalysisError> {
if novelty_curve.is_empty() {
return Err(AnalysisError::InvalidInput("Novelty curve is empty".to_string()));
}
if sample_rate == 0 {
return Err(AnalysisError::InvalidInput("Sample rate must be > 0".to_string()));
}
if hop_size == 0 {
return Err(AnalysisError::InvalidInput("Hop size must be > 0".to_string()));
}
if min_bpm <= 0.0 || max_bpm <= min_bpm {
return Err(AnalysisError::InvalidInput(
format!("Invalid BPM range: min={}, max={}", min_bpm, max_bpm)
));
}
if bpm_resolution <= 0.0 {
return Err(AnalysisError::InvalidInput(
format!("BPM resolution must be > 0, got {}", bpm_resolution)
));
}
let frame_rate = sample_rate as f32 / hop_size as f32;
log::debug!("Computing autocorrelation tempogram: {} novelty values, frame_rate={:.2} Hz, BPM range=[{:.1}, {:.1}], resolution={:.1}",
novelty_curve.len(), frame_rate, min_bpm, max_bpm, bpm_resolution);
let mut tempogram = Vec::new();
let mut bpm = min_bpm;
while bpm <= max_bpm {
let beats_per_second = bpm / 60.0;
let frames_per_beat = frame_rate / beats_per_second;
let mut autocorr_sum = 0.0;
let mut autocorr_count = 0;
let lag_frames = frames_per_beat as usize;
for i in 0..novelty_curve.len() {
let j = i + lag_frames;
if j < novelty_curve.len() {
autocorr_sum += novelty_curve[i] * novelty_curve[j];
autocorr_count += 1;
}
}
let strength = if autocorr_count > 0 {
autocorr_sum / autocorr_count as f32
} else {
0.0
};
tempogram.push((bpm, strength));
bpm += bpm_resolution;
}
tempogram.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
log::debug!("Autocorrelation tempogram: {} BPM candidates, top BPM={:.1} (strength={:.6})",
tempogram.len(),
tempogram.first().map(|(bpm, _)| *bpm).unwrap_or(0.0),
tempogram.first().map(|(_, strength)| *strength).unwrap_or(0.0));
Ok(tempogram)
}
pub fn find_best_bpm_autocorr(
tempogram: &[(f32, f32)],
) -> Option<AutocorrTempogramResult> {
if tempogram.is_empty() {
return None;
}
let (best_bpm, best_strength) = tempogram[0];
let confidence = if tempogram.len() > 1 {
let second_strength = tempogram[1].1;
if best_strength > EPSILON {
let prom = (best_strength - second_strength).max(0.0) / best_strength;
prom.clamp(0.0, 1.0)
} else {
0.0
}
} else {
0.5
};
Some(AutocorrTempogramResult {
bpm: best_bpm,
confidence,
strength: best_strength,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autocorrelation_tempogram_periodic() {
let sample_rate = 44100;
let hop_size = 512;
let frame_rate = sample_rate as f32 / hop_size as f32;
let beats_per_second = 120.0 / 60.0;
let frames_per_beat = frame_rate / beats_per_second;
let period = frames_per_beat as usize;
let mut novelty = vec![0.0f32; 500];
for i in 0..novelty.len() {
if i % period == 0 {
novelty[i] = 1.0;
}
}
let tempogram = autocorrelation_tempogram(&novelty, sample_rate, hop_size, 100.0, 140.0, 1.0).unwrap();
let best = find_best_bpm_autocorr(&tempogram).unwrap();
assert!(best.bpm >= 115.0 && best.bpm <= 125.0,
"Expected BPM around 120, got {:.1}", best.bpm);
}
#[test]
fn test_autocorrelation_tempogram_empty() {
let novelty = vec![];
let result = autocorrelation_tempogram(&novelty, 44100, 512, 40.0, 240.0, 0.5);
assert!(result.is_err());
}
#[test]
fn test_autocorrelation_tempogram_invalid_params() {
let novelty = vec![0.5f32; 100];
let result = autocorrelation_tempogram(&novelty, 0, 512, 40.0, 240.0, 0.5);
assert!(result.is_err());
let result = autocorrelation_tempogram(&novelty, 44100, 0, 40.0, 240.0, 0.5);
assert!(result.is_err());
let result = autocorrelation_tempogram(&novelty, 44100, 512, 240.0, 40.0, 0.5);
assert!(result.is_err());
}
#[test]
fn test_find_best_bpm_autocorr() {
let tempogram = vec![
(120.0, 0.9),
(60.0, 0.3),
(180.0, 0.2),
];
let result = find_best_bpm_autocorr(&tempogram).unwrap();
assert_eq!(result.bpm, 120.0);
assert_eq!(result.strength, 0.9);
assert!((result.confidence - (2.0 / 3.0)).abs() < 1e-6);
}
#[test]
fn test_find_best_bpm_autocorr_empty() {
let tempogram = vec![];
let result = find_best_bpm_autocorr(&tempogram);
assert!(result.is_none());
}
}