#![allow(dead_code)]
use std::f64::consts::PI;
#[derive(Clone, Debug)]
pub struct SpectrumBand {
pub center_hz: f64,
pub low_hz: f64,
pub high_hz: f64,
pub label: String,
}
impl SpectrumBand {
#[must_use]
pub fn new(center_hz: f64, low_hz: f64, high_hz: f64, label: impl Into<String>) -> Self {
Self {
center_hz,
low_hz,
high_hz,
label: label.into(),
}
}
}
#[must_use]
pub fn octave_bands() -> Vec<SpectrumBand> {
let centers = [
(31.5, "31.5Hz"),
(63.0, "63Hz"),
(125.0, "125Hz"),
(250.0, "250Hz"),
(500.0, "500Hz"),
(1000.0, "1kHz"),
(2000.0, "2kHz"),
(4000.0, "4kHz"),
(8000.0, "8kHz"),
(16000.0, "16kHz"),
];
let factor = 2.0_f64.sqrt(); centers
.iter()
.map(|&(center, label)| SpectrumBand {
center_hz: center,
low_hz: center / factor,
high_hz: center * factor,
label: label.to_string(),
})
.collect()
}
#[must_use]
pub fn third_octave_bands() -> Vec<SpectrumBand> {
let centers = [
(20.0, "20Hz"),
(25.0, "25Hz"),
(31.5, "31.5Hz"),
(40.0, "40Hz"),
(50.0, "50Hz"),
(63.0, "63Hz"),
(80.0, "80Hz"),
(100.0, "100Hz"),
(125.0, "125Hz"),
(160.0, "160Hz"),
(200.0, "200Hz"),
(250.0, "250Hz"),
(315.0, "315Hz"),
(400.0, "400Hz"),
(500.0, "500Hz"),
(630.0, "630Hz"),
(800.0, "800Hz"),
(1000.0, "1kHz"),
(1250.0, "1.25kHz"),
(1600.0, "1.6kHz"),
(2000.0, "2kHz"),
(2500.0, "2.5kHz"),
(3150.0, "3.15kHz"),
(4000.0, "4kHz"),
(5000.0, "5kHz"),
(6300.0, "6.3kHz"),
(8000.0, "8kHz"),
(10000.0, "10kHz"),
(12500.0, "12.5kHz"),
(16000.0, "16kHz"),
(20000.0, "20kHz"),
];
let factor = 2.0_f64.powf(1.0 / 6.0);
centers
.iter()
.map(|&(center, label)| SpectrumBand {
center_hz: center,
low_hz: center / factor,
high_hz: center * factor,
label: label.to_string(),
})
.collect()
}
#[must_use]
pub fn dft_magnitude_at_freq(samples: &[f64], freq_hz: f64, sample_rate: f64) -> f64 {
let n = samples.len();
if n == 0 || sample_rate <= 0.0 || freq_hz <= 0.0 {
return 0.0;
}
let normalized_freq = freq_hz / sample_rate;
let omega = 2.0 * PI * normalized_freq;
let coeff = 2.0 * omega.cos();
let mut s_prev2 = 0.0f64;
let mut s_prev1 = 0.0f64;
for &sample in samples {
let s = sample + coeff * s_prev1 - s_prev2;
s_prev2 = s_prev1;
s_prev1 = s;
}
let real = s_prev1 - s_prev2 * omega.cos();
let imag = s_prev2 * omega.sin();
(real * real + imag * imag).sqrt() / n as f64
}
#[derive(Clone, Debug)]
pub struct SpectrumFrame {
pub band_levels_db: Vec<f64>,
pub peaks_db: Vec<f64>,
pub timestamp_ms: u64,
}
impl SpectrumFrame {
#[must_use]
pub fn new(band_levels_db: Vec<f64>, peaks_db: Vec<f64>, timestamp_ms: u64) -> Self {
Self {
band_levels_db,
peaks_db,
timestamp_ms,
}
}
#[must_use]
pub fn band_count(&self) -> usize {
self.band_levels_db.len()
}
}
pub struct SpectrumBandAnalyzer {
pub bands: Vec<SpectrumBand>,
pub peak_hold_ms: u64,
pub sample_rate: f64,
peak_levels: Vec<f64>,
peak_timestamps: Vec<u64>,
}
impl SpectrumBandAnalyzer {
#[must_use]
pub fn new(bands: Vec<SpectrumBand>, sample_rate: f64, peak_hold_ms: u64) -> Self {
let n = bands.len();
Self {
peak_levels: vec![f64::NEG_INFINITY; n],
peak_timestamps: vec![0; n],
bands,
sample_rate,
peak_hold_ms,
}
}
#[must_use]
pub fn octave(sample_rate: f64) -> Self {
let bands = octave_bands();
let n = bands.len();
Self {
peak_levels: vec![f64::NEG_INFINITY; n],
peak_timestamps: vec![0; n],
bands,
sample_rate,
peak_hold_ms: 2000,
}
}
#[must_use]
pub fn third_octave(sample_rate: f64) -> Self {
let bands = third_octave_bands();
let n = bands.len();
Self {
peak_levels: vec![f64::NEG_INFINITY; n],
peak_timestamps: vec![0; n],
bands,
sample_rate,
peak_hold_ms: 2000,
}
}
pub fn analyze(&mut self, samples: &[f64], timestamp_ms: u64) -> SpectrumFrame {
let mut band_levels_db = Vec::with_capacity(self.bands.len());
for (i, band) in self.bands.iter().enumerate() {
let mag = dft_magnitude_at_freq(samples, band.center_hz, self.sample_rate);
let db = if mag <= 0.0 {
-120.0
} else {
(20.0 * mag.log10()).max(-120.0)
};
band_levels_db.push(db);
if db > self.peak_levels[i] {
self.peak_levels[i] = db;
self.peak_timestamps[i] = timestamp_ms + self.peak_hold_ms;
} else if timestamp_ms > self.peak_timestamps[i] {
self.peak_levels[i] = (self.peak_levels[i] - 0.5).max(-120.0);
}
}
let peaks_db = self.peak_levels.clone();
SpectrumFrame {
band_levels_db,
peaks_db,
timestamp_ms,
}
}
#[must_use]
pub fn band_count(&self) -> usize {
self.bands.len()
}
}
#[must_use]
pub fn magnitude_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
-120.0
} else {
(20.0 * linear.log10()).max(-120.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn sine_at(freq: f64, sample_rate: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| (2.0 * PI * freq * i as f64 / sample_rate).sin())
.collect()
}
#[test]
fn test_octave_bands_count() {
let bands = octave_bands();
assert_eq!(bands.len(), 10);
}
#[test]
fn test_octave_bands_centers() {
let bands = octave_bands();
assert!((bands[0].center_hz - 31.5).abs() < 0.1);
assert!((bands[5].center_hz - 1000.0).abs() < 0.1);
assert!((bands[9].center_hz - 16000.0).abs() < 0.1);
}
#[test]
fn test_third_octave_bands_count() {
let bands = third_octave_bands();
assert_eq!(bands.len(), 31);
}
#[test]
fn test_third_octave_bands_labels() {
let bands = third_octave_bands();
assert_eq!(bands[17].label, "1kHz");
}
#[test]
fn test_dft_magnitude_at_freq_pure_sine() {
let sr = 48000.0;
let freq = 1000.0;
let n = 4800;
let samples = sine_at(freq, sr, n);
let mag = dft_magnitude_at_freq(&samples, freq, sr);
assert!(
mag > 0.1,
"Expected significant magnitude at {freq}Hz, got {mag}"
);
}
#[test]
fn test_dft_magnitude_at_freq_silence() {
let samples = vec![0.0f64; 4800];
let mag = dft_magnitude_at_freq(&samples, 1000.0, 48000.0);
assert!((mag - 0.0).abs() < 1e-10);
}
#[test]
fn test_dft_magnitude_at_freq_empty() {
let mag = dft_magnitude_at_freq(&[], 1000.0, 48000.0);
assert_eq!(mag, 0.0);
}
#[test]
fn test_dft_magnitude_frequency_discrimination() {
let sr = 48000.0;
let n = 4800;
let samples = sine_at(1000.0, sr, n);
let mag_1k = dft_magnitude_at_freq(&samples, 1000.0, sr);
let mag_100 = dft_magnitude_at_freq(&samples, 100.0, sr);
assert!(mag_1k > mag_100 * 5.0, "1kHz: {mag_1k}, 100Hz: {mag_100}");
}
#[test]
fn test_spectrum_band_analyzer_octave_band_count() {
let analyzer = SpectrumBandAnalyzer::octave(48000.0);
assert_eq!(analyzer.band_count(), 10);
}
#[test]
fn test_spectrum_band_analyzer_third_octave_band_count() {
let analyzer = SpectrumBandAnalyzer::third_octave(48000.0);
assert_eq!(analyzer.band_count(), 31);
}
#[test]
fn test_spectrum_frame_analyze_returns_correct_band_count() {
let mut analyzer = SpectrumBandAnalyzer::octave(48000.0);
let samples = sine_at(1000.0, 48000.0, 4800);
let frame = analyzer.analyze(&samples, 0);
assert_eq!(frame.band_count(), 10);
assert_eq!(frame.peaks_db.len(), 10);
}
#[test]
fn test_spectrum_band_analyzer_detects_1khz() {
let mut analyzer = SpectrumBandAnalyzer::octave(48000.0);
let samples = sine_at(1000.0, 48000.0, 4800);
let frame = analyzer.analyze(&samples, 0);
let level_1k = frame.band_levels_db[5];
assert!(level_1k > -80.0, "1kHz level: {level_1k} dB");
}
#[test]
fn test_spectrum_frame_new() {
let frame = SpectrumFrame::new(vec![-12.0, -24.0], vec![-6.0, -18.0], 1000);
assert_eq!(frame.band_count(), 2);
assert_eq!(frame.timestamp_ms, 1000);
}
#[test]
fn test_magnitude_to_db_zero() {
assert_eq!(magnitude_to_db(0.0), -120.0);
}
#[test]
fn test_magnitude_to_db_unity() {
assert!((magnitude_to_db(1.0) - 0.0).abs() < 1e-10);
}
#[test]
fn test_spectrum_band_new() {
let band = SpectrumBand::new(1000.0, 707.0, 1414.0, "1kHz");
assert_eq!(band.label, "1kHz");
assert!((band.center_hz - 1000.0).abs() < 0.1);
}
}