#![allow(dead_code)]
pub struct SpectralCentroid {
pub min_freq: f32,
pub max_freq: f32,
}
impl SpectralCentroid {
#[must_use]
pub fn new() -> Self {
Self {
min_freq: 20.0,
max_freq: 20_000.0,
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn compute(&self, freqs: &[f32], magnitudes: &[f32]) -> f32 {
assert_eq!(
freqs.len(),
magnitudes.len(),
"freqs and magnitudes must have the same length"
);
let mut weighted_sum = 0.0_f64;
let mut total_mag = 0.0_f64;
for (&f, &m) in freqs.iter().zip(magnitudes.iter()) {
if f < self.min_freq || f > self.max_freq {
continue;
}
let mag = f64::from(m);
weighted_sum += f64::from(f) * mag;
total_mag += mag;
}
if total_mag == 0.0 {
return 0.0;
}
(weighted_sum / total_mag) as f32
}
}
impl Default for SpectralCentroid {
fn default() -> Self {
Self::new()
}
}
pub struct SpectralFlux {
prev_magnitudes: Vec<f32>,
pub half_wave: bool,
}
impl SpectralFlux {
#[must_use]
pub fn new(half_wave: bool) -> Self {
Self {
prev_magnitudes: Vec::new(),
half_wave,
}
}
#[allow(clippy::cast_precision_loss)]
pub fn compute_flux(&mut self, magnitudes: &[f32]) -> Option<f32> {
if self.prev_magnitudes.is_empty() {
self.prev_magnitudes = magnitudes.to_vec();
return None;
}
let n = self.prev_magnitudes.len().min(magnitudes.len());
let mut flux = 0.0_f64;
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let diff = f64::from(magnitudes[i]) - f64::from(self.prev_magnitudes[i]);
let val = if self.half_wave { diff.max(0.0) } else { diff };
flux += val * val;
}
self.prev_magnitudes = magnitudes.to_vec();
Some((flux / n as f64).sqrt() as f32)
}
pub fn reset(&mut self) {
self.prev_magnitudes.clear();
}
}
pub struct SpectralRolloff {
pub threshold: f32,
}
impl SpectralRolloff {
#[must_use]
pub fn new(threshold: f32) -> Self {
Self {
threshold: threshold.clamp(0.0, 1.0),
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn compute_rolloff(&self, freqs: &[f32], magnitudes: &[f32]) -> f32 {
assert_eq!(freqs.len(), magnitudes.len());
if freqs.is_empty() {
return 0.0;
}
let total_energy: f64 = magnitudes
.iter()
.map(|&m| f64::from(m) * f64::from(m))
.sum();
if total_energy == 0.0 {
return 0.0;
}
let target = total_energy * f64::from(self.threshold);
let mut cumulative = 0.0_f64;
for (&f, &m) in freqs.iter().zip(magnitudes.iter()) {
cumulative += f64::from(m) * f64::from(m);
if cumulative >= target {
return f;
}
}
*freqs.last().unwrap_or(&0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_freqs(n: usize, max_hz: f32) -> Vec<f32> {
(0..n).map(|i| i as f32 / (n - 1) as f32 * max_hz).collect()
}
#[test]
fn test_centroid_uniform_magnitudes() {
let freqs = vec![100.0, 200.0, 300.0, 400.0];
let mags = vec![1.0, 1.0, 1.0, 1.0];
let centroid = SpectralCentroid::new();
let c = centroid.compute(&freqs, &mags);
assert!((c - 250.0).abs() < 1.0, "centroid={c}");
}
#[test]
fn test_centroid_single_peak() {
let freqs = vec![100.0, 500.0, 1000.0];
let mags = vec![0.0, 1.0, 0.0]; let centroid = SpectralCentroid::new();
let c = centroid.compute(&freqs, &mags);
assert!((c - 500.0).abs() < 0.1, "centroid={c}");
}
#[test]
fn test_centroid_zero_magnitudes() {
let freqs = vec![100.0, 200.0];
let mags = vec![0.0, 0.0];
let centroid = SpectralCentroid::new();
assert_eq!(centroid.compute(&freqs, &mags), 0.0);
}
#[test]
fn test_centroid_empty() {
let centroid = SpectralCentroid::new();
assert_eq!(centroid.compute(&[], &[]), 0.0);
}
#[test]
fn test_centroid_frequency_range_filter() {
let freqs = vec![10.0, 500.0, 50_000.0];
let mags = vec![5.0, 2.0, 5.0];
let centroid = SpectralCentroid {
min_freq: 100.0,
max_freq: 10_000.0,
};
let c = centroid.compute(&freqs, &mags);
assert!((c - 500.0).abs() < 0.1, "centroid={c}");
}
#[test]
fn test_centroid_default() {
let centroid = SpectralCentroid::default();
assert!((centroid.min_freq - 20.0).abs() < f32::EPSILON);
}
#[test]
fn test_flux_first_frame_returns_none() {
let mut flux = SpectralFlux::new(false);
assert!(flux.compute_flux(&[1.0, 2.0, 3.0]).is_none());
}
#[test]
fn test_flux_identical_frames_zero() {
let mut flux = SpectralFlux::new(false);
let mags = vec![1.0, 1.0, 1.0];
flux.compute_flux(&mags);
let f = flux
.compute_flux(&mags)
.expect("flux computation should succeed");
assert!(f.abs() < 1e-6, "flux={f}");
}
#[test]
fn test_flux_increasing_energy() {
let mut flux = SpectralFlux::new(false);
flux.compute_flux(&[0.0, 0.0, 0.0]);
let f = flux
.compute_flux(&[1.0, 1.0, 1.0])
.expect("flux computation should succeed");
assert!(f > 0.0, "Expected positive flux, got {f}");
}
#[test]
fn test_flux_half_wave_suppresses_negative() {
let mut flux_full = SpectralFlux::new(false);
let mut flux_half = SpectralFlux::new(true);
let frame1 = vec![2.0, 2.0, 2.0];
let frame2 = vec![1.0, 1.0, 1.0];
flux_full.compute_flux(&frame1);
flux_half.compute_flux(&frame1);
let full = flux_full
.compute_flux(&frame2)
.expect("flux computation should succeed");
let half = flux_half
.compute_flux(&frame2)
.expect("flux computation should succeed");
assert!(half.abs() < 1e-6, "half={half}");
assert!(full > 0.0, "full={full}");
}
#[test]
fn test_flux_reset_clears_state() {
let mut flux = SpectralFlux::new(false);
flux.compute_flux(&[1.0, 2.0]);
flux.reset();
assert!(flux.compute_flux(&[1.0, 2.0]).is_none());
}
#[test]
fn test_rolloff_finds_correct_bin() {
let freqs: Vec<f32> = (1..=10).map(|i| i as f32 * 100.0).collect();
let mags = vec![1.0_f32; 10];
let rolloff = SpectralRolloff::new(0.85);
let r = rolloff.compute_rolloff(&freqs, &mags);
assert!(r > 0.0, "rolloff={r}");
assert!(r <= 1000.0, "rolloff={r}");
}
#[test]
fn test_rolloff_empty_spectrum() {
let rolloff = SpectralRolloff::new(0.85);
assert_eq!(rolloff.compute_rolloff(&[], &[]), 0.0);
}
#[test]
fn test_rolloff_zero_energy() {
let freqs = vec![100.0, 200.0];
let mags = vec![0.0, 0.0];
let rolloff = SpectralRolloff::new(0.85);
assert_eq!(rolloff.compute_rolloff(&freqs, &mags), 0.0);
}
#[test]
fn test_rolloff_full_threshold() {
let freqs: Vec<f32> = make_freqs(5, 4000.0);
let mags = vec![1.0_f32; 5];
let rolloff = SpectralRolloff::new(1.0);
let r = rolloff.compute_rolloff(&freqs, &mags);
assert!((r - 4000.0).abs() < 1.0, "rolloff={r}");
}
#[test]
fn test_rolloff_clamped_threshold() {
let rolloff = SpectralRolloff::new(1.5);
assert!((rolloff.threshold - 1.0).abs() < f32::EPSILON);
let rolloff2 = SpectralRolloff::new(-0.1);
assert!((rolloff2.threshold - 0.0).abs() < f32::EPSILON);
}
}