mod common;
use common::*;
use ferraus_core::testing::{self, si_sdr};
use ferraus_core::{AudioBuffer, OpSpec};
use ferraus_denoise::metrics::{birdie_score, hard_gate, BIRDIE_THRESHOLD};
use ferraus_denoise::profile;
fn spec(name: &str) -> OpSpec {
ferraus_denoise::ops().into_iter().find(|o| o.name == name).unwrap()
}
const LEAD_S: f64 = 1.0;
const SIG_S: f64 = 2.0;
struct Row {
signal: &'static str,
noise: NoiseKind,
snr: f64,
before: f64,
after: f64,
retained: f64,
}
fn run_case(signal: &'static str, kind: NoiseKind, snr: f64, pairs: &[(&str, &str)]) -> Row {
let clean = match signal {
"speech" => speech_like(LEAD_S, SIG_S, 0.5),
_ => music_like(LEAD_S, SIG_S, 0.5),
};
let lead = (LEAD_S * SR as f64) as usize;
let nz = make_noise(kind, LEAD_S + SIG_S, 7 + snr as u64);
let (noisy, _) = mix(&clean.buf, &nz, snr, lead);
let mut p = pairs.to_vec();
p.push(("noise-range", "0s..1s"));
let out = testing::run(&spec("denoise"), &p, noisy.clone()).unwrap();
assert_eq!(out.frames(), noisy.frames());
let c = &clean.buf.data[0];
let before = si_sdr(c, &noisy.data[0]);
let after = si_sdr(c, &out.data[0]);
let retained = retained_db(c, &out.data[0], &clean.voiced);
Row { signal, noise: kind, snr, before, after, retained }
}
fn matrix(pairs: &[(&str, &str)], snrs: &[f64], label: &str) -> Vec<Row> {
let mut rows = Vec::new();
for signal in ["speech", "music"] {
for kind in NoiseKind::ALL {
for &snr in snrs {
let r = run_case(signal, kind, snr, pairs);
eprintln!(
"[{label}] {:6} {:7} {:4.0} dB SNR: SI-SDR {:6.2} -> {:6.2} ({:+5.2} dB), clean level in voiced regions {:+5.2} dB",
r.signal,
format!("{:?}", r.noise),
r.snr,
r.before,
r.after,
r.after - r.before,
r.retained
);
rows.push(r);
}
}
}
rows
}
#[test]
fn synthetic_mixes_improve_si_sdr() {
let mut failures = Vec::new();
for r in matrix(&[("reduction", "24dB")], &[0.0, 10.0, 20.0], "24dB") {
let gain = r.after - r.before;
if r.snr == 10.0 && gain < 8.0 {
failures.push(format!("{} {:?} 10 dB: SI-SDR gain {gain:.2} < 8 dB", r.signal, r.noise));
}
if r.snr == 10.0 && r.retained < -1.0 {
failures.push(format!(
"{} {:?} 10 dB: clean level in voiced regions {:.2} dB < -1 dB",
r.signal, r.noise, r.retained
));
}
if gain < 1.0 {
failures.push(format!("{} {:?} {} dB: SI-SDR gain only {gain:.2}", r.signal, r.noise, r.snr));
}
}
for r in matrix(&[], &[10.0], "defaults") {
let gain = r.after - r.before;
if r.snr == 10.0 && (gain < 6.0 || r.retained < -1.0) {
failures.push(format!(
"defaults {} {:?} 10 dB: SI-SDR gain {gain:.2} (floor 6), voiced level {:.2}",
r.signal, r.noise, r.retained
));
}
if gain < 1.0 {
failures.push(format!("defaults {} {:?} {} dB: SI-SDR gain only {gain:.2}", r.signal, r.noise, r.snr));
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn identity_at_zero_reduction() {
let clean = speech_like(0.5, 1.0, 0.5);
let nz = make_noise(NoiseKind::Fan, 1.5, 3);
let (noisy, _) = mix(&clean.buf, &nz, 10.0, 24_000);
let stereo = testing::stereo(noisy.clone());
for (buf, mode) in [(noisy, "profile"), (stereo, "adaptive")] {
let out =
testing::run(&spec("denoise"), &[("reduction", "0dB"), ("mode", mode), ("auto", "true")], buf.clone())
.unwrap();
let err = testing::rms_diff(&buf, &out);
assert!(err == 0.0 || 20.0 * err.log10() < -120.0, "{mode}: {err}");
}
}
#[test]
fn mix_zero_is_identity() {
let x = testing::add(&testing::sine(SR, 440.0, 0.5, 0.3), &testing::noise(SR, 0.5, 0.05, 1));
let out = testing::run(&spec("denoise"), &[("mix", "0%")], x.clone()).unwrap();
assert_eq!(out, x);
}
fn noise_only(kind: NoiseKind) -> AudioBuffer {
let mut x = make_noise(kind, 3.0, 11);
for v in &mut x {
*v *= 0.05;
}
AudioBuffer::from_mono(SR, x)
}
#[test]
fn birdie_score_soft_gate_beats_hard_gate() {
for kind in [NoiseKind::Hiss, NoiseKind::Fan] {
let x = noise_only(kind);
let lead = SR as usize;
let prof = profile::learn(&x, 0, lead, Default::default()).unwrap();
let ours = testing::run(&spec("denoise"), &[("noise-range", "0s..1s")], x.clone()).unwrap();
let naive = hard_gate(&x, &prof, 12.0, 6.0);
let seg = |b: &AudioBuffer| b.data[0][(1.2 * SR as f64) as usize..(2.8 * SR as f64) as usize].to_vec();
let s_raw = birdie_score(&seg(&x), SR);
let s_ours = birdie_score(&seg(&ours), SR);
let s_hard = birdie_score(&seg(&naive), SR);
eprintln!("{kind:?}: birdie raw {s_raw:.5}, ours {s_ours:.5}, hard gate {s_hard:.5}");
assert!(s_ours < BIRDIE_THRESHOLD, "{kind:?}: ours {s_ours:.3} >= {BIRDIE_THRESHOLD}");
assert!(s_hard > BIRDIE_THRESHOLD, "{kind:?}: hard gate {s_hard:.3} <= {BIRDIE_THRESHOLD}");
let red = 20.0 * (rms(&seg(&ours)) / rms(&seg(&x))).log10();
assert!(red < -8.0, "{kind:?}: noise only reduced by {red:.1} dB");
}
}
#[test]
fn rnnoise_reduces_noise_on_speech() {
let clean = speech_like(1.5, 1.5, 0.5);
let lead = 3 * SR as usize / 2;
let nz = make_noise(NoiseKind::Fan, 3.0, 5);
let (noisy, _) = mix(&clean.buf, &nz, 5.0, lead);
let out = testing::run(&spec("denoise"), &[("mode", "ml"), ("model", "rnnoise")], noisy.clone()).unwrap();
let c = &clean.buf.data[0];
let before = si_sdr(c, &noisy.data[0]);
let after = si_sdr(c, &out.data[0]);
let lead_before = rms(&noisy.data[0][SR as usize..lead]);
let lead_after = rms(&out.data[0][SR as usize..lead]);
eprintln!(
"rnnoise: SI-SDR {before:.2} -> {after:.2}, lead-in noise {:.1} dB",
20.0 * (lead_after / lead_before).log10()
);
assert!(after > before + 2.0, "SI-SDR {before:.2} -> {after:.2}");
assert!(lead_after < lead_before, "noise-only level did not drop");
}