use crate::{AnalysisConfig, Result};
pub struct EchoDetector {
#[allow(dead_code)]
config: AnalysisConfig,
}
impl EchoDetector {
#[must_use]
pub fn new(config: AnalysisConfig) -> Self {
Self { config }
}
pub fn detect(&self, samples: &[f32], sample_rate: f32) -> Result<EchoResult> {
let max_delay = (sample_rate * 2.0) as usize; let autocorr = self.compute_autocorrelation(samples, max_delay);
let echo_delays = self.find_echo_delays(&autocorr, sample_rate);
let reverb_amount = self.estimate_reverb(&autocorr);
Ok(EchoResult {
has_echo: !echo_delays.is_empty(),
echo_delays,
reverb_amount,
})
}
#[allow(clippy::unused_self)]
fn compute_autocorrelation(&self, samples: &[f32], max_lag: usize) -> Vec<f32> {
let max_lag = max_lag.min(samples.len());
let mut autocorr = vec![0.0; max_lag];
for lag in 0..max_lag {
let mut sum = 0.0;
let mut norm = 0.0;
for i in 0..(samples.len() - lag) {
sum += samples[i] * samples[i + lag];
norm += samples[i] * samples[i];
}
autocorr[lag] = if norm > 0.0 { sum / norm } else { 0.0 };
}
autocorr
}
#[allow(clippy::unused_self)]
fn find_echo_delays(&self, autocorr: &[f32], sample_rate: f32) -> Vec<f32> {
let min_delay_samples = (sample_rate * 0.02) as usize; let threshold = 0.3;
let mut delays = Vec::new();
for i in min_delay_samples..(autocorr.len() - 1) {
if autocorr[i] > threshold
&& autocorr[i] > autocorr[i - 1]
&& autocorr[i] > autocorr[i + 1]
{
let delay_seconds = i as f32 / sample_rate;
delays.push(delay_seconds);
if delays.len() >= 5 {
break;
}
}
}
delays
}
#[allow(clippy::unused_self)]
fn estimate_reverb(&self, autocorr: &[f32]) -> f32 {
if autocorr.len() < 100 {
return 0.0;
}
let early = autocorr[10..50].iter().sum::<f32>() / 40.0;
let late = autocorr[100..(autocorr.len().min(500))].iter().sum::<f32>()
/ (autocorr.len().min(500) - 100) as f32;
(late / (early + 1e-6)).min(1.0)
}
}
#[derive(Debug, Clone)]
pub struct EchoResult {
pub has_echo: bool,
pub echo_delays: Vec<f32>,
pub reverb_amount: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_echo_detector() {
let config = AnalysisConfig::default();
let detector = EchoDetector::new(config);
let sample_rate = 4000.0;
let mut samples = vec![0.0; (sample_rate * 0.5) as usize];
for i in 0..200 {
samples[i] = (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate).sin();
}
let echo_delay = (sample_rate * 0.1) as usize;
for i in 0..200 {
if i + echo_delay < samples.len() {
samples[i + echo_delay] += 0.5 * samples[i];
}
}
let result = detector
.detect(&samples, sample_rate)
.expect("detection should succeed");
assert!(result.has_echo || result.reverb_amount > 0.0);
}
}