use rustfft::{num_complex::Complex, FftPlanner};
use crate::core::fft::COMPLEX_ZERO;
pub type BandSpectra = (
Vec<Complex<f32>>,
Vec<Complex<f32>>,
Vec<Complex<f32>>,
Vec<Complex<f32>>,
);
#[derive(Debug, Clone, Copy)]
pub struct FrequencyBands {
pub sub_bass: f32,
pub low: f32,
pub mid: f32,
}
impl Default for FrequencyBands {
fn default() -> Self {
Self {
sub_bass: 120.0,
low: 500.0,
mid: 4000.0,
}
}
}
#[inline]
pub fn freq_to_bin(freq: f32, fft_size: usize, sample_rate: u32) -> usize {
let bin = (freq * fft_size as f32 / sample_rate as f32).round() as usize;
bin.min(fft_size / 2)
}
#[inline]
pub fn bin_to_freq(bin: usize, fft_size: usize, sample_rate: u32) -> f32 {
bin as f32 * sample_rate as f32 / fft_size as f32
}
#[derive(Debug, Clone, Copy)]
enum Band {
SubBass,
Low,
Mid,
High,
}
#[inline]
fn classify_bin(bin: usize, sub_bass_bin: usize, low_bin: usize, mid_bin: usize) -> Band {
if bin < sub_bass_bin {
Band::SubBass
} else if bin < low_bin {
Band::Low
} else if bin < mid_bin {
Band::Mid
} else {
Band::High
}
}
pub fn split_spectrum_into_bands(
spectrum: &[Complex<f32>],
fft_size: usize,
sample_rate: u32,
bands: &FrequencyBands,
) -> BandSpectra {
let sub_bass_bin = freq_to_bin(bands.sub_bass, fft_size, sample_rate);
let low_bin = freq_to_bin(bands.low, fft_size, sample_rate);
let mid_bin = freq_to_bin(bands.mid, fft_size, sample_rate);
let num_bins = fft_size / 2 + 1;
let mut sub_bass = vec![COMPLEX_ZERO; spectrum.len()];
let mut low = vec![COMPLEX_ZERO; spectrum.len()];
let mut mid = vec![COMPLEX_ZERO; spectrum.len()];
let mut high = vec![COMPLEX_ZERO; spectrum.len()];
let mut assign = |class_bin: usize, idx: usize| match classify_bin(
class_bin,
sub_bass_bin,
low_bin,
mid_bin,
) {
Band::SubBass => sub_bass[idx] = spectrum[idx],
Band::Low => low[idx] = spectrum[idx],
Band::Mid => mid[idx] = spectrum[idx],
Band::High => high[idx] = spectrum[idx],
};
for bin in 0..num_bins.min(spectrum.len()) {
assign(bin, bin);
}
if spectrum.len() == fft_size {
for bin in 1..num_bins - 1 {
assign(bin, fft_size - bin);
}
}
(sub_bass, low, mid, high)
}
pub fn compute_band_energy(
samples: &[f32],
fft_size: usize,
sample_rate: u32,
bands: &FrequencyBands,
) -> (f32, f32, f32, f32) {
if samples.len() < fft_size {
return (0.0, 0.0, 0.0, 0.0);
}
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(fft_size);
let window =
crate::core::window::generate_window(crate::core::window::WindowType::Hann, fft_size);
let mut buffer: Vec<Complex<f32>> = samples[..fft_size]
.iter()
.zip(window.iter())
.map(|(&s, &w)| Complex::new(s * w, 0.0))
.collect();
fft.process(&mut buffer);
let sub_bass_bin = freq_to_bin(bands.sub_bass, fft_size, sample_rate);
let low_bin = freq_to_bin(bands.low, fft_size, sample_rate);
let mid_bin = freq_to_bin(bands.mid, fft_size, sample_rate);
let num_bins = fft_size / 2 + 1;
let mut sub_e = 0.0f32;
let mut low_e = 0.0f32;
let mut mid_e = 0.0f32;
let mut high_e = 0.0f32;
for (bin, val) in buffer.iter().enumerate().take(num_bins) {
let energy = val.norm_sqr();
match classify_bin(bin, sub_bass_bin, low_bin, mid_bin) {
Band::SubBass => sub_e += energy,
Band::Low => low_e += energy,
Band::Mid => mid_e += energy,
Band::High => high_e += energy,
}
}
(sub_e, low_e, mid_e, high_e)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freq_to_bin() {
assert_eq!(freq_to_bin(0.0, 4096, 44100), 0);
let bin_1000 = freq_to_bin(1000.0, 4096, 44100);
assert!((92..=94).contains(&bin_1000));
}
#[test]
fn test_bin_to_freq() {
let freq = bin_to_freq(100, 4096, 44100);
assert!((freq - 1076.66).abs() < 1.0);
}
#[test]
fn test_band_energy_sine() {
let sample_rate = 44100u32;
let fft_size = 4096;
let samples: Vec<f32> = (0..fft_size)
.map(|i| (2.0 * std::f32::consts::PI * 100.0 * i as f32 / sample_rate as f32).sin())
.collect();
let bands = FrequencyBands::default();
let (sub_e, low_e, mid_e, high_e) =
compute_band_energy(&samples, fft_size, sample_rate, &bands);
assert!(
sub_e > low_e + mid_e + high_e,
"Sub-bass energy {} should dominate (low={}, mid={}, high={})",
sub_e,
low_e,
mid_e,
high_e
);
}
#[test]
fn test_band_energy_high_freq() {
let sample_rate = 44100u32;
let fft_size = 4096;
let samples: Vec<f32> = (0..fft_size)
.map(|i| (2.0 * std::f32::consts::PI * 5000.0 * i as f32 / sample_rate as f32).sin())
.collect();
let bands = FrequencyBands::default();
let (sub_e, low_e, mid_e, high_e) =
compute_band_energy(&samples, fft_size, sample_rate, &bands);
assert!(
high_e > sub_e + low_e + mid_e,
"High energy {} should dominate (sub={}, low={}, mid={})",
high_e,
sub_e,
low_e,
mid_e
);
}
}