use lookas::dsp::{a_weighting, ema_tc, hann, hz_to_mel, mel_to_hz};
fn to_db(linear: f32) -> f32 {
20.0 * linear.max(1e-12).log10()
}
#[test]
fn a_weighting_reference_values() {
let cases: &[(f32, f32, f32)] = &[
(125.0, -16.1, 1.0),
(250.0, -8.6, 1.0),
(500.0, -3.2, 1.0),
(1_000.0, 0.0, 0.1),
(2_000.0, 1.2, 1.0),
(4_000.0, 1.0, 1.0),
(8_000.0, -1.1, 1.0),
(16_000.0, -6.6, 1.5),
];
for &(hz, expected_db, tol) in cases {
let got_db = to_db(a_weighting(hz));
assert!(
(got_db - expected_db).abs() < tol,
"a_weighting({hz} Hz): got {got_db:.2} dB, expected {expected_db:.2} dB (tol {tol})"
);
}
}
#[test]
fn a_weighting_1khz_is_unity() {
let w = a_weighting(1_000.0);
assert!(
(w - 1.0).abs() < 0.01,
"a_weighting(1000) = {w}, expected ~1.0"
);
}
#[test]
fn a_weighting_peaks_near_3_to_4_khz() {
let w_3k = a_weighting(3_150.0);
let w_1k = a_weighting(1_000.0);
let w_8k = a_weighting(8_000.0);
assert!(
w_3k > w_1k,
"3.15 kHz should have higher weight than 1 kHz"
);
assert!(
w_3k > w_8k,
"3.15 kHz should have higher weight than 8 kHz"
);
}
#[test]
fn a_weighting_monotone_rolloff_in_bass() {
let freqs = [500.0f32, 250.0, 125.0, 63.0, 31.5];
let weights: Vec<f32> =
freqs.iter().map(|&f| a_weighting(f)).collect();
for (i, w) in weights.windows(2).enumerate() {
let (Some(&cur), Some(&nxt)) = (w.first(), w.get(1)) else {
continue;
};
let f_cur = freqs.get(i).copied().unwrap_or(f32::NAN);
let f_nxt = freqs.get(i + 1).copied().unwrap_or(f32::NAN);
assert!(
cur > nxt,
"expected monotone rolloff: a_weighting({f_cur}) = {cur} should be > a_weighting({f_nxt}) = {nxt}",
);
}
}
#[test]
fn a_weighting_floor_clamp() {
let w = a_weighting(0.0);
assert!(w.is_finite(), "a_weighting(0) should be finite");
assert!(w >= 0.0, "a_weighting(0) should be non-negative");
}
#[test]
fn ema_tc_converges_to_target() {
let mut v = 0.0f32;
let tau = 0.1;
let dt = 0.01;
for _ in 0..1000 {
v = ema_tc(v, 1.0, tau, dt);
}
assert!((v - 1.0).abs() < 1e-3, "EMA did not converge: got {v}");
}
#[test]
fn ema_tc_slow_tau_barely_moves() {
let prev = 0.0f32;
let result = ema_tc(prev, 1.0, 10.0, 0.001); assert!(
result < 0.01,
"with tau >> dt, EMA should barely move: got {result}"
);
}
#[test]
fn ema_tc_fast_tau_snaps_quickly() {
let prev = 0.0f32;
let result = ema_tc(prev, 1.0, 0.0001, 0.016); assert!(
result > 0.99,
"with tau << dt, EMA should snap to target: got {result}"
);
}
#[test]
fn ema_tc_output_between_prev_and_target() {
let result = ema_tc(0.0, 1.0, 0.05, 0.016);
assert!((0.0..=1.0).contains(&result));
let result2 = ema_tc(1.0, 0.0, 0.05, 0.016);
assert!((0.0..=1.0).contains(&result2));
}
#[test]
fn hann_length() {
for n in [16, 64, 512, 2048] {
assert_eq!(hann(n).len(), n);
}
}
#[test]
fn hann_endpoints_are_zero() {
let w = hann(1024);
let first = w.first().copied().unwrap_or(f32::NAN);
let last = w.last().copied().unwrap_or(f32::NAN);
assert!(first.abs() < 1e-6, "hann[0] should be ~0, got {first}");
assert!(last.abs() < 1e-4, "hann[N-1] should be ~0, got {last}");
}
#[test]
fn hann_peak_near_centre() {
let n = 1024;
let w = hann(n);
let mid = n / 2;
let w_mid = w.get(mid).copied().unwrap_or(f32::NAN);
let w_prev =
w.get(mid.saturating_sub(1)).copied().unwrap_or(f32::NAN);
let w_next = w.get(mid + 1).copied().unwrap_or(f32::NAN);
assert!(
(w_mid - 1.0).abs() < 1e-4,
"hann centre should be ~1.0, got {w_mid}",
);
assert!(w_mid >= w_prev);
assert!(w_mid >= w_next);
}
#[test]
fn hann_values_in_range() {
for v in hann(512) {
assert!(
(0.0..=1.0).contains(&v),
"hann value out of [0, 1]: {v}"
);
}
}
#[test]
fn mel_hz_roundtrip() {
let freqs = [50.0f32, 200.0, 500.0, 1_000.0, 4_000.0, 12_000.0];
for &hz in &freqs {
let roundtripped = mel_to_hz(hz_to_mel(hz));
assert!(
(roundtripped - hz).abs() / hz < 1e-4,
"roundtrip failed for {hz} Hz: got {roundtripped}"
);
}
}
#[test]
fn mel_scale_is_monotone() {
let freqs = [100.0f32, 500.0, 1_000.0, 4_000.0, 10_000.0];
let mels: Vec<f32> =
freqs.iter().map(|&f| hz_to_mel(f)).collect();
for w in mels.windows(2) {
let (Some(&prev), Some(&curr)) = (w.first(), w.get(1)) else {
continue;
};
assert!(
curr > prev,
"mel scale should be monotonically increasing"
);
}
}