use crate::{AnalysisConfig, AnalysisError, Result};
pub struct FormantAnalyzer {
config: AnalysisConfig,
lpc_order: usize,
}
impl FormantAnalyzer {
#[must_use]
pub fn new(config: AnalysisConfig) -> Self {
let lpc_order = 12;
Self { config, lpc_order }
}
pub fn analyze(&self, samples: &[f32], sample_rate: f32) -> Result<FormantResult> {
if samples.len() < self.config.fft_size {
return Err(AnalysisError::InsufficientSamples {
needed: self.config.fft_size,
got: samples.len(),
});
}
let emphasized = self.pre_emphasize(samples);
let lpc_coeffs = self.compute_lpc(&emphasized)?;
let formants = self.find_formants(&lpc_coeffs, sample_rate)?;
Ok(FormantResult {
formants,
lpc_coefficients: lpc_coeffs,
})
}
#[allow(clippy::unused_self)]
fn pre_emphasize(&self, samples: &[f32]) -> Vec<f32> {
let alpha = 0.97;
let mut emphasized = Vec::with_capacity(samples.len());
emphasized.push(samples[0]);
for i in 1..samples.len() {
emphasized.push(samples[i] - alpha * samples[i - 1]);
}
emphasized
}
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn compute_lpc(&self, samples: &[f32]) -> Result<Vec<f32>> {
let mut r = vec![0.0; self.lpc_order + 1];
for i in 0..=self.lpc_order {
let mut sum = 0.0;
for j in 0..(samples.len() - i) {
sum += samples[j] * samples[j + i];
}
r[i] = sum;
}
let mut a = vec![0.0; self.lpc_order + 1];
let mut e = r[0];
for i in 1..=self.lpc_order {
let mut lambda = 0.0;
for j in 1..i {
lambda -= a[j] * r[i - j];
}
lambda -= r[i];
let k = if e == 0.0 { 0.0 } else { lambda / e };
a[i] = k;
for j in 1..i {
let temp = a[j];
a[j] += k * a[i - j];
a[i - j] = temp;
}
e *= 1.0 - k * k;
}
Ok(a)
}
#[allow(clippy::unnecessary_wraps)]
fn find_formants(&self, lpc_coeffs: &[f32], sample_rate: f32) -> Result<Vec<f32>> {
let roots = self.find_lpc_roots(lpc_coeffs)?;
let mut formants: Vec<f32> = roots
.iter()
.filter(|(real, imag)| {
let magnitude = (real * real + imag * imag).sqrt();
magnitude < 1.0 && magnitude > 0.7 && *imag > 0.0
})
.map(|(real, imag)| {
let angle = imag.atan2(*real);
(angle * sample_rate) / (2.0 * std::f32::consts::PI)
})
.collect();
formants.sort_by(|a, b| a.partial_cmp(b).unwrap());
formants.truncate(4);
while formants.len() < 4 {
let default_formants = [500.0, 1500.0, 2500.0, 3500.0];
formants.push(default_formants[formants.len()]);
}
Ok(formants)
}
#[allow(clippy::unnecessary_wraps, clippy::needless_range_loop)]
fn find_lpc_roots(&self, coeffs: &[f32]) -> Result<Vec<(f32, f32)>> {
if coeffs.is_empty() {
return Ok(Vec::new());
}
let n = coeffs.len() - 1;
if n == 0 {
return Ok(Vec::new());
}
let mut roots: Vec<(f32, f32)> = (0..n)
.map(|i| {
let angle = 2.0 * std::f32::consts::PI * (i as f32 + 0.4) / n as f32;
(0.9 * angle.cos(), 0.9 * angle.sin())
})
.collect();
for _ in 0..50 {
let mut max_change: f32 = 0.0;
for i in 0..n {
let (re, im) = roots[i];
let (p_re, p_im) = self.eval_poly(coeffs, re, im);
let mut prod_re = 1.0;
let mut prod_im = 0.0;
for j in 0..n {
if i != j {
let (rj_re, rj_im) = roots[j];
let diff_re = re - rj_re;
let diff_im = im - rj_im;
let temp_re = prod_re * diff_re - prod_im * diff_im;
let temp_im = prod_re * diff_im + prod_im * diff_re;
prod_re = temp_re;
prod_im = temp_im;
}
}
let denom = prod_re * prod_re + prod_im * prod_im;
if denom > 1e-10 {
let delta_re = (p_re * prod_re + p_im * prod_im) / denom;
let delta_im = (p_im * prod_re - p_re * prod_im) / denom;
roots[i].0 -= delta_re;
roots[i].1 -= delta_im;
max_change = max_change.max(delta_re.abs() + delta_im.abs());
}
}
if max_change < 1e-6 {
break;
}
}
Ok(roots)
}
#[allow(clippy::unused_self)]
fn eval_poly(&self, coeffs: &[f32], re: f32, im: f32) -> (f32, f32) {
let mut result_re = 0.0;
let mut result_im = 0.0;
let mut power_re = 1.0;
let mut power_im = 0.0;
for &coeff in coeffs {
result_re += coeff * power_re;
result_im += coeff * power_im;
let temp_re = power_re * re - power_im * im;
let temp_im = power_re * im + power_im * re;
power_re = temp_re;
power_im = temp_im;
}
(result_re, result_im)
}
}
#[derive(Debug, Clone)]
pub struct FormantResult {
pub formants: Vec<f32>,
pub lpc_coefficients: Vec<f32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_formant_analyzer() {
let config = AnalysisConfig::default();
let analyzer = FormantAnalyzer::new(config);
let sample_rate = 16000.0;
let samples = vec![0.1; 4096];
let result = analyzer.analyze(&samples, sample_rate);
assert!(result.is_ok());
let formants = result.unwrap().formants;
assert_eq!(formants.len(), 4);
for &f in &formants {
assert!(f > 0.0);
}
}
#[test]
fn test_pre_emphasis() {
let config = AnalysisConfig::default();
let analyzer = FormantAnalyzer::new(config);
let samples = vec![1.0, 2.0, 3.0, 4.0];
let emphasized = analyzer.pre_emphasize(&samples);
assert_eq!(emphasized.len(), samples.len());
assert_eq!(emphasized[0], samples[0]);
}
}