#[must_use]
pub fn spectral_rolloff(magnitude: &[f32], sample_rate: f32, threshold: f32) -> f32 {
if magnitude.is_empty() || threshold <= 0.0 || threshold > 1.0 {
return 0.0;
}
let total_energy: f32 = magnitude.iter().map(|&m| m * m).sum();
if total_energy <= 0.0 {
return 0.0;
}
let target = total_energy * threshold;
let mut cumulative = 0.0;
let n_bins = magnitude.len();
for (i, &mag) in magnitude.iter().enumerate() {
cumulative += mag * mag;
if cumulative >= target {
let freq = bin_to_freq(i, sample_rate, n_bins);
return freq;
}
}
sample_rate / 2.0
}
#[must_use]
pub fn spectral_rolloff_85(magnitude: &[f32], sample_rate: f32) -> f32 {
spectral_rolloff(magnitude, sample_rate, 0.85)
}
#[must_use]
pub fn spectral_rolloff_95(magnitude: &[f32], sample_rate: f32) -> f32 {
spectral_rolloff(magnitude, sample_rate, 0.95)
}
#[must_use]
pub fn spectral_rolloff_track(
spectrogram: &[Vec<f32>],
sample_rate: f32,
threshold: f32,
) -> Vec<f32> {
spectrogram
.iter()
.map(|frame| spectral_rolloff(frame, sample_rate, threshold))
.collect()
}
#[inline]
fn bin_to_freq(bin: usize, sample_rate: f32, n_bins: usize) -> f32 {
bin as f32 * sample_rate / (2.0 * (n_bins - 1) as f32)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
fn sine_spectrum(freq_hz: f32, sample_rate: f32, fft_size: usize) -> Vec<f32> {
let bin = ((freq_hz / sample_rate) * fft_size as f32).round() as usize;
let bin = bin.min(fft_size / 2);
let mut spectrum = vec![0.0_f32; fft_size / 2 + 1];
spectrum[bin] = 1.0;
spectrum
}
#[test]
fn test_rolloff_empty() {
let empty: Vec<f32> = vec![];
assert_eq!(spectral_rolloff(&empty, 44100.0, 0.85), 0.0);
}
#[test]
fn test_rolloff_zero_threshold() {
let mag = vec![1.0; 512];
assert_eq!(spectral_rolloff(&mag, 44100.0, 0.0), 0.0);
}
#[test]
fn test_rolloff_above_one_threshold() {
let mag = vec![1.0; 512];
assert_eq!(spectral_rolloff(&mag, 44100.0, 1.5), 0.0);
}
#[test]
fn test_rolloff_uniform_spectrum_85() {
let n = 100;
let mag = vec![1.0_f32; n];
let sr = 44100.0;
let rolloff = spectral_rolloff(&mag, sr, 0.85);
let nyquist = sr / 2.0;
assert!(rolloff > 0.0 && rolloff <= nyquist);
assert!(rolloff < nyquist * 0.95);
}
#[test]
fn test_rolloff_low_frequency_content() {
let mut mag = vec![0.0_f32; 256];
mag[0] = 1.0;
let rolloff = spectral_rolloff(&mag, 44100.0, 0.85);
assert_eq!(rolloff, 0.0);
}
#[test]
fn test_rolloff_high_frequency_tone() {
let mut mag = vec![0.0_f32; 256];
let last = mag.len() - 1;
mag[last] = 1.0;
let sr = 44100.0;
let rolloff = spectral_rolloff(&mag, sr, 0.85);
assert!(rolloff > sr / 2.0 * 0.9, "High-freq rolloff = {rolloff}");
}
#[test]
fn test_rolloff_85_vs_95() {
let mag: Vec<f32> = (0..256).map(|_| 1.0).collect();
let sr = 44100.0;
let r85 = spectral_rolloff_85(&mag, sr);
let r95 = spectral_rolloff_95(&mag, sr);
assert!(r95 >= r85, "r95={r95} should be >= r85={r85}");
}
#[test]
fn test_rolloff_track_length() {
let spectrogram: Vec<Vec<f32>> = (0..10).map(|_| vec![1.0_f32; 64]).collect();
let track = spectral_rolloff_track(&spectrogram, 44100.0, 0.85);
assert_eq!(track.len(), 10);
}
#[test]
fn test_rolloff_track_monotone_input() {
let sr = 44100.0;
let n_bins = 64;
let spectrogram: Vec<Vec<f32>> = (0..8)
.map(|i| {
let mut frame = vec![0.0_f32; n_bins];
let bin = (i * 8).min(n_bins - 1);
frame[bin] = 1.0;
frame
})
.collect();
let track = spectral_rolloff_track(&spectrogram, sr, 0.85);
for i in 1..track.len() {
assert!(
track[i] >= track[i - 1],
"Expected monotone rolloff, track[{i}]={} < track[{}]={}",
track[i],
i - 1,
track[i - 1]
);
}
}
#[test]
fn test_rolloff_full_energy_threshold() {
let mag = vec![1.0_f32; 128];
let sr = 8000.0;
let rolloff = spectral_rolloff(&mag, sr, 1.0);
assert!(rolloff <= sr / 2.0 + 1.0);
}
#[test]
fn test_bin_to_freq_nyquist() {
let sr = 44100.0;
let n_bins = 1025; let nyquist_bin = n_bins - 1;
let freq = bin_to_freq(nyquist_bin, sr, n_bins);
assert!((freq - sr / 2.0).abs() < 1.0);
}
#[test]
fn test_rolloff_sine_440hz() {
let sr = 44100.0;
let fft_size = 2048;
let spectrum = sine_spectrum(440.0, sr, fft_size);
let rolloff = spectral_rolloff(&spectrum, sr, 0.85);
assert!(rolloff > 0.0);
let _ = PI; }
}