use num_complex::Complex;
use realfft::RealFftPlanner;
pub fn autocorrelate(y: &[f64], max_size: Option<usize>) -> Vec<f64> {
let n = y.len();
if n == 0 {
return Vec::new();
}
let max_size = max_size.unwrap_or(n).min(n);
let padded_len = (2 * n - 1).next_power_of_two();
let mut planner = RealFftPlanner::<f64>::new();
let fft_forward = planner.plan_fft_forward(padded_len);
let mut scratch = fft_forward.make_scratch_vec();
let mut input_buf = fft_forward.make_input_vec();
let mut spectrum = fft_forward.make_output_vec();
input_buf[..n].copy_from_slice(y);
fft_forward
.process_with_scratch(&mut input_buf, &mut spectrum, &mut scratch)
.expect("Forward FFT failed");
for val in spectrum.iter_mut() {
*val = Complex::new(val.norm_sqr(), 0.0);
}
let fft_inverse = planner.plan_fft_inverse(padded_len);
let mut scratch_inv = fft_inverse.make_scratch_vec();
let mut output_buf = fft_inverse.make_output_vec();
fft_inverse
.process_with_scratch(&mut spectrum, &mut output_buf, &mut scratch_inv)
.expect("Inverse FFT failed");
let norm = 1.0 / padded_len as f64;
output_buf[..max_size].iter().map(|&v| v * norm).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct AutocorrelateTestCase {
name: String,
input: Vec<f64>,
max_size: Option<usize>,
expected: Vec<f64>,
}
fn load_references() -> Vec<AutocorrelateTestCase> {
let data = include_str!("../tests/references/autocorrelate.json");
serde_json::from_str(data).expect("Failed to parse autocorrelate.json")
}
fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{name}: length mismatch: got {} expected {}",
actual.len(),
expected.len()
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
let diff = (a - e).abs();
let threshold = tol * e.abs().max(1.0);
assert!(
diff < threshold,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {threshold}"
);
}
}
#[test]
fn test_autocorrelate_matches_librosa() {
for case in load_references() {
let result = autocorrelate(&case.input, case.max_size);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
}