#[cfg(test)]
mod tests;
use crate::detect::PitchDetector;
const PEAK_THRESHOLD_RATIO: f32 = 0.8;
const MIN_VARIANCE_THRESHOLD: f32 = 1e-6;
pub struct Autocorrelation {
min_lag: usize,
max_lag: usize,
lag_scores: Vec<f32>,
}
impl Autocorrelation {
pub const DEFAULT_MIN_LAG: usize = 20;
pub const DEFAULT_MAX_LAG: usize = 2000;
pub fn new(min_lag: usize, max_lag: usize) -> Self {
Self {
min_lag,
max_lag,
lag_scores: vec![0.0; max_lag + 1],
}
}
}
impl Autocorrelation {
fn find_first_peak(&self, threshold: f32) -> Option<usize> {
for lag in (self.min_lag + 1)..self.max_lag {
let prev = self.lag_scores[lag - 1];
let curr = self.lag_scores[lag];
let next = self.lag_scores[lag + 1];
if curr > prev && curr > next && curr >= threshold {
return Some(lag);
}
}
None
}
}
impl Default for Autocorrelation {
fn default() -> Self {
Self::new(Self::DEFAULT_MIN_LAG, Self::DEFAULT_MAX_LAG)
}
}
impl PitchDetector for Autocorrelation {
fn detect(&mut self, samples: &[f32], sample_rate: f32) -> Option<f32> {
let samples_length = samples.len();
if is_silent(samples) {
return None;
}
let mut best_lag = 0;
let mut best_score = 0.0;
self.lag_scores.fill(0.0);
for lag in self.min_lag..=self.max_lag {
let mut sum = 0.0;
let Some(overlap_len) = samples_length.checked_sub(lag).filter(|n| *n > 0) else {
break;
};
for i in 0..overlap_len {
sum += samples[i] * samples[i + lag];
}
sum /= overlap_len as f32;
self.lag_scores[lag] = sum;
if sum > best_score {
best_score = sum;
best_lag = lag;
}
}
if best_lag == 0 {
return None;
}
let threshold = best_score * PEAK_THRESHOLD_RATIO;
let first_peak = self.find_first_peak(threshold);
let peak_lag = first_peak.unwrap_or(best_lag);
let delta = parabolic_interpolation(&self.lag_scores, peak_lag).unwrap_or_default();
Some(sample_rate / (peak_lag as f32 + delta))
}
}
fn parabolic_interpolation(scores: &[f32], best_lag: usize) -> Option<f32> {
let y_m = best_lag.checked_sub(1).and_then(|i| scores.get(i))?;
let y_0 = scores.get(best_lag)?;
let y_p = best_lag.checked_add(1).and_then(|i| scores.get(i))?;
let numerator = y_m - y_p;
let denominator = 2.0 * (y_m - 2.0 * y_0 + y_p);
if denominator.abs() < f32::EPSILON {
return None;
}
Some(numerator / denominator)
}
fn is_silent(samples: &[f32]) -> bool {
if samples.is_empty() {
return true;
}
let len = samples.len() as f32;
let mean = samples.iter().sum::<f32>() / len;
let variance = samples.iter().map(|s| (s - mean).powi(2)).sum::<f32>() / len;
variance < MIN_VARIANCE_THRESHOLD
}