use crate::spectral::SpectralAnalyzer;
use crate::{AnalysisConfig, Result};
pub struct TimbralAnalyzer {
spectral_analyzer: SpectralAnalyzer,
}
impl TimbralAnalyzer {
#[must_use]
pub fn new(config: AnalysisConfig) -> Self {
Self {
spectral_analyzer: SpectralAnalyzer::new(config),
}
}
pub fn analyze(&self, samples: &[f32], sample_rate: f32) -> Result<TimbralFeatures> {
let spectral = self.spectral_analyzer.analyze(samples, sample_rate)?;
let brightness = self.compute_brightness(&spectral.magnitude_spectrum, sample_rate);
let roughness = self.compute_roughness(&spectral.magnitude_spectrum);
let warmth = self.compute_warmth(&spectral.magnitude_spectrum, sample_rate);
Ok(TimbralFeatures {
brightness,
warmth,
roughness,
spectral_centroid: spectral.centroid,
spectral_flatness: spectral.flatness,
})
}
#[allow(clippy::unused_self)]
fn compute_brightness(&self, spectrum: &[f32], sample_rate: f32) -> f32 {
let threshold_freq = 1500.0; let threshold_bin = (threshold_freq * spectrum.len() as f32 / (sample_rate / 2.0)) as usize;
if threshold_bin >= spectrum.len() {
return 0.0;
}
let low_energy: f32 = spectrum[..threshold_bin].iter().map(|&x| x * x).sum();
let high_energy: f32 = spectrum[threshold_bin..].iter().map(|&x| x * x).sum();
let total = low_energy + high_energy;
if total > 0.0 {
high_energy / total
} else {
0.0
}
}
#[allow(clippy::unused_self)]
fn compute_roughness(&self, spectrum: &[f32]) -> f32 {
if spectrum.is_empty() {
return 0.0;
}
let mean = spectrum.iter().sum::<f32>() / spectrum.len() as f32;
let variance =
spectrum.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / spectrum.len() as f32;
variance.sqrt() / (mean + 1e-6)
}
#[allow(clippy::unused_self)]
fn compute_warmth(&self, spectrum: &[f32], sample_rate: f32) -> f32 {
let warmth_freq = 500.0; let warmth_bin = (warmth_freq * spectrum.len() as f32 / (sample_rate / 2.0)) as usize;
if warmth_bin >= spectrum.len() {
return 1.0;
}
let low_energy: f32 = spectrum[..warmth_bin].iter().map(|&x| x * x).sum();
let total_energy: f32 = spectrum.iter().map(|&x| x * x).sum();
if total_energy > 0.0 {
low_energy / total_energy
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct TimbralFeatures {
pub brightness: f32,
pub warmth: f32,
pub roughness: f32,
pub spectral_centroid: f32,
pub spectral_flatness: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timbral_analyzer() {
let config = AnalysisConfig::default();
let analyzer = TimbralAnalyzer::new(config);
let sample_rate = 44100.0;
let samples: Vec<f32> = (0..8192)
.map(|i| (2.0 * std::f32::consts::PI * 3000.0 * i as f32 / sample_rate).sin())
.collect();
let result = analyzer
.analyze(&samples, sample_rate)
.expect("analysis should succeed");
assert!(result.brightness > 0.5);
assert!(result.warmth < 0.5);
}
}