use super::loudness::total_loudness;
pub const SHARPNESS_WEIGHT: [f64; 24] = [
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.2, 1.5, 1.8,
2.2, 2.7, 3.3, 4.0, 5.0, 6.2,
];
pub fn sharpness(specific_loudness: &[f64; 24]) -> f64 {
let mut numerator = 0.0_f64;
for z in 0..24 {
let z_center = (z + 1) as f64; numerator += specific_loudness[z] * SHARPNESS_WEIGHT[z] * z_center;
}
let denominator = total_loudness(specific_loudness).max(0.001);
0.11 * numerator / denominator
}
#[cfg(test)]
mod tests {
use super::*;
use crate::epa::loudness::specific_loudness;
fn make_bandlimited_response(lo_hz: f64, hi_hz: f64, level_db: f64) -> (Vec<f64>, Vec<f64>) {
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| if f >= lo_hz && f <= hi_hz { level_db } else { -40.0 })
.collect();
(freqs, spl)
}
#[test]
fn test_sharpness_low_freq_only() {
let (freqs, spl) = make_bandlimited_response(20.0, 1000.0, 70.0);
let spec = specific_loudness(&freqs, &spl, 70.0);
let s = sharpness(&spec);
assert!(
s < 1.0,
"Low-frequency-only signal should have low sharpness, got {s} acum"
);
}
#[test]
fn test_sharpness_high_freq_only() {
let (freqs, spl) = make_bandlimited_response(5000.0, 15000.0, 70.0);
let spec = specific_loudness(&freqs, &spl, 70.0);
let s = sharpness(&spec);
assert!(
s > 2.0,
"High-frequency-only signal should have high sharpness, got {s} acum"
);
}
#[test]
fn test_sharpness_broadband() {
let (freqs, spl) = make_bandlimited_response(20.0, 15000.0, 70.0);
let spec = specific_loudness(&freqs, &spl, 70.0);
let s = sharpness(&spec);
assert!(
(0.5..=2.5).contains(&s),
"Broadband signal should have moderate sharpness, got {s} acum"
);
}
}