use super::bark::BARK_BAND_EDGES;
pub fn spectral_roughness(freqs: &[f64], spl_db: &[f64]) -> f64 {
debug_assert_eq!(freqs.len(), spl_db.len());
let mut weighted_sum = 0.0;
let mut weight_total = 0.0;
for band in 0..24 {
let lo = BARK_BAND_EDGES[band];
let hi = BARK_BAND_EDGES[band + 1];
let fc = (lo + hi) * 0.5;
let mut samples = Vec::new();
for (i, &f) in freqs.iter().enumerate() {
if f >= lo && f < hi {
samples.push(spl_db[i]);
}
}
if samples.len() < 2 {
continue;
}
let n = samples.len() as f64;
let mean = samples.iter().sum::<f64>() / n;
let variance = samples.iter().map(|&s| (s - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
let erb = 24.7 * (1.0 + 4.37 * fc / 1000.0);
let w = 1.0 / erb;
weighted_sum += w * std_dev;
weight_total += w;
}
if weight_total <= 0.0 {
return 0.0;
}
let avg_std_dev = weighted_sum / weight_total;
(avg_std_dev / 10.0).min(2.0)
}
pub fn roughness_from_spectrum(freqs: &[f64], spl_db: &[f64]) -> f64 {
spectral_roughness(freqs, spl_db)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flat_spectrum_zero_roughness() {
let n = 1000;
let freqs: Vec<f64> = (0..n)
.map(|i| 20.0 + (16000.0 - 20.0) * i as f64 / n as f64)
.collect();
let spl: Vec<f64> = vec![75.0; n];
let r = spectral_roughness(&freqs, &spl);
assert!(
r < 1e-10,
"Flat spectrum should have zero roughness, got {r}"
);
}
#[test]
fn test_smooth_tilt_low_roughness() {
let n = 1000;
let freqs: Vec<f64> = (0..n)
.map(|i| 20.0 + (16000.0 - 20.0) * i as f64 / n as f64)
.collect();
let spl: Vec<f64> = freqs
.iter()
.map(|&f| 75.0 - 0.5 * (f / 1000.0).log2())
.collect();
let r = spectral_roughness(&freqs, &spl);
assert!(
r < 0.15,
"Smooth tilt should have low roughness, got {r}"
);
}
#[test]
fn test_narrow_peak_high_roughness() {
let n = 1000;
let freqs: Vec<f64> = (0..n)
.map(|i| 20.0 + (16000.0 - 20.0) * i as f64 / n as f64)
.collect();
let mut spl: Vec<f64> = vec![75.0; n];
for (i, &f) in freqs.iter().enumerate() {
if (f - 1000.0).abs() < 10.0 {
spl[i] = 90.0; }
}
let r = spectral_roughness(&freqs, &spl);
assert!(
r > 0.01,
"Narrow peak should produce measurable roughness, got {r}"
);
}
#[test]
fn test_many_peaks_higher_roughness() {
let n = 2000;
let freqs: Vec<f64> = (0..n)
.map(|i| 20.0 + (16000.0 - 20.0) * i as f64 / n as f64)
.collect();
let mut spl: Vec<f64> = vec![75.0; n];
for (i, &f) in freqs.iter().enumerate() {
for &peak_f in &[200.0, 500.0, 1000.0, 2000.0, 4000.0] {
if (f - peak_f).abs() < 15.0 {
spl[i] = 85.0; }
}
}
let r_peaks = spectral_roughness(&freqs, &spl);
let r_flat = spectral_roughness(&freqs, &vec![75.0; n]);
assert!(
r_peaks > r_flat + 0.01,
"Many peaks ({r_peaks}) should be rougher than flat ({r_flat})"
);
}
#[test]
fn test_roughness_output_range() {
let n = 1000;
let freqs: Vec<f64> = (0..n)
.map(|i| 20.0 + (16000.0 - 20.0) * i as f64 / n as f64)
.collect();
let spl: Vec<f64> = (0..n).map(|i| if i % 2 == 0 { 90.0 } else { 60.0 }).collect();
let r = spectral_roughness(&freqs, &spl);
assert!(
r <= 2.0,
"Roughness should be clamped to <= 2.0, got {r}"
);
assert!(
r > 0.5,
"Severely irregular spectrum should have high roughness, got {r}"
);
}
#[test]
fn test_roughness_from_spectrum_delegates() {
let freqs = vec![100.0, 200.0, 300.0, 400.0, 500.0];
let spl = vec![75.0, 80.0, 75.0, 85.0, 75.0];
assert_eq!(
roughness_from_spectrum(&freqs, &spl),
spectral_roughness(&freqs, &spl)
);
}
}