denoize 0.16.0

Pure-Rust audio denoiser with classical DSP and optional RNNoise
Documentation
//! Objective quality and stereo-imaging reports.

use crate::Audio;

#[derive(Clone, Debug)]
pub struct BenchmarkReport {
    pub frames: usize,
    pub sample_rate: u32,
    pub channels: usize,
    pub si_sdr_db: f64,
    pub si_snr_db: f64,
    pub snr_db: f64,
    pub segmental_snr_db: f64,
    pub stereo_side_sdr_db: Option<f64>,
    pub correlation_error: Option<f64>,
    pub stoi: Option<f64>,
    pub pesq: Option<f64>,
    pub elapsed_ms: Option<f64>,
    pub peak_rss_bytes: Option<u64>,
}

impl BenchmarkReport {
    pub fn compare(reference: &Audio, test: &Audio) -> Result<Self, String> {
        if reference.sample_rate != test.sample_rate {
            return Err("benchmark sample rates differ".into());
        }
        if reference.channels.len() != test.channels.len() || reference.channels.is_empty() {
            return Err("benchmark channel counts differ or are empty".into());
        }
        let frames = reference.frames().min(test.frames());
        if frames == 0 {
            return Err("benchmark inputs are empty".into());
        }
        let r = downmix(reference, frames);
        let t = downmix(test, frames);
        let (side_sdr, correlation_error) = if reference.channels.len() == 2 {
            let rs = side(reference, frames);
            let ts = side(test, frames);
            (
                Some(si_sdr(&rs, &ts)),
                Some(
                    (correlation(
                        &reference.channels[0][..frames],
                        &reference.channels[1][..frames],
                    ) - correlation(&test.channels[0][..frames], &test.channels[1][..frames]))
                    .abs(),
                ),
            )
        } else {
            (None, None)
        };
        Ok(Self {
            frames,
            sample_rate: reference.sample_rate,
            channels: reference.channels.len(),
            si_sdr_db: si_sdr(&r, &t),
            si_snr_db: si_snr(&r, &t),
            snr_db: snr(&r, &t),
            segmental_snr_db: segmental_snr(&r, &t, reference.sample_rate),
            stereo_side_sdr_db: side_sdr,
            correlation_error,
            stoi: None,
            pesq: None,
            elapsed_ms: None,
            peak_rss_bytes: None,
        })
    }

    pub fn json(&self) -> String {
        format!("{{\"frames\":{},\"sample_rate\":{},\"channels\":{},\"si_sdr_db\":{:.6},\"si_snr_db\":{:.6},\"snr_db\":{:.6},\"segmental_snr_db\":{:.6},\"stereo_side_sdr_db\":{},\"correlation_error\":{},\"stoi\":{},\"pesq\":{},\"elapsed_ms\":{},\"peak_rss_bytes\":{}}}", self.frames, self.sample_rate, self.channels, self.si_sdr_db, self.si_snr_db, self.snr_db, self.segmental_snr_db, optional(self.stereo_side_sdr_db), optional(self.correlation_error), optional(self.stoi), optional(self.pesq), optional(self.elapsed_ms), self.peak_rss_bytes.map_or_else(|| "null".into(), |v| v.to_string()))
    }

    pub fn markdown(&self) -> String {
        format!("| Metric | Value |\n|---|---:|\n| SI-SDR | {:.3} dB |\n| SI-SNR | {:.3} dB |\n| SNR | {:.3} dB |\n| Segmental SNR | {:.3} dB |\n| Stereo side SDR | {} |\n| Correlation error | {} |\n| STOI | {} |\n| PESQ | {} |", self.si_sdr_db, self.si_snr_db, self.snr_db, self.segmental_snr_db, db(self.stereo_side_sdr_db), display(self.correlation_error, 6), display(self.stoi, 4), display(self.pesq, 3))
    }
}

#[derive(Clone, Debug)]
pub struct ComparisonReport {
    pub noisy: BenchmarkReport,
    pub enhanced: BenchmarkReport,
}

impl ComparisonReport {
    pub fn compare(clean: &Audio, noisy: &Audio, enhanced: &Audio) -> Result<Self, String> {
        Ok(Self {
            noisy: BenchmarkReport::compare(clean, noisy)?,
            enhanced: BenchmarkReport::compare(clean, enhanced)?,
        })
    }

    pub fn json(&self) -> String {
        format!(
            "{{\"noisy\":{},\"enhanced\":{},\"improvement\":{{\"si_sdr_db\":{:.6},\"si_snr_db\":{:.6},\"snr_db\":{:.6},\"segmental_snr_db\":{:.6}}}}}",
            self.noisy.json(), self.enhanced.json(),
            self.enhanced.si_sdr_db - self.noisy.si_sdr_db,
            self.enhanced.si_snr_db - self.noisy.si_snr_db,
            self.enhanced.snr_db - self.noisy.snr_db,
            self.enhanced.segmental_snr_db - self.noisy.segmental_snr_db,
        )
    }

    pub fn markdown(&self) -> String {
        format!(
            "| Metric | Noisy | Enhanced | Improvement |\n|---|---:|---:|---:|\n| SI-SDR | {:.3} dB | {:.3} dB | {:+.3} dB |\n| SI-SNR | {:.3} dB | {:.3} dB | {:+.3} dB |\n| SNR | {:.3} dB | {:.3} dB | {:+.3} dB |\n| Segmental SNR | {:.3} dB | {:.3} dB | {:+.3} dB |\n\nSTOI, PESQ, and DNSMOS: not measured (no external model or licensed reference implementation configured).",
            self.noisy.si_sdr_db, self.enhanced.si_sdr_db, self.enhanced.si_sdr_db - self.noisy.si_sdr_db,
            self.noisy.si_snr_db, self.enhanced.si_snr_db, self.enhanced.si_snr_db - self.noisy.si_snr_db,
            self.noisy.snr_db, self.enhanced.snr_db, self.enhanced.snr_db - self.noisy.snr_db,
            self.noisy.segmental_snr_db, self.enhanced.segmental_snr_db, self.enhanced.segmental_snr_db - self.noisy.segmental_snr_db,
        )
    }

    pub fn html(&self) -> String {
        let rows = self
            .markdown()
            .lines()
            .skip(2)
            .take(4)
            .map(|line| {
                let cells = line
                    .trim_matches('|')
                    .split('|')
                    .map(str::trim)
                    .map(|cell| format!("<td>{cell}</td>"))
                    .collect::<String>();
                format!("<tr>{cells}</tr>")
            })
            .collect::<String>();
        format!("<!doctype html><meta charset=\"utf-8\"><title>denoize comparison</title><style>body{{font-family:system-ui;max-width:900px;margin:3rem auto}}table{{border-collapse:collapse}}td,th{{padding:.5rem 1rem;border:1px solid #ccc;text-align:right}}td:first-child{{text-align:left}}</style><h1>denoize quality comparison</h1><table><thead><tr><th>Metric</th><th>Noisy</th><th>Enhanced</th><th>Improvement</th></tr></thead><tbody>{rows}</tbody></table><p>STOI, PESQ, and DNSMOS were not measured.</p>")
    }
}

fn optional(v: Option<f64>) -> String {
    v.map_or_else(|| "null".into(), |v| format!("{v:.6}"))
}
fn display(v: Option<f64>, precision: usize) -> String {
    v.map_or_else(|| "n/a".into(), |v| format!("{v:.precision$}"))
}
fn db(v: Option<f64>) -> String {
    v.map_or_else(|| "n/a".into(), |v| format!("{v:.3} dB"))
}
fn downmix(a: &Audio, n: usize) -> Vec<f64> {
    (0..n)
        .map(|i| a.channels.iter().map(|c| c[i]).sum::<f64>() / a.channels.len() as f64)
        .collect()
}
fn side(a: &Audio, n: usize) -> Vec<f64> {
    (0..n)
        .map(|i| (a.channels[0][i] - a.channels[1][i]) * 0.5)
        .collect()
}

pub fn si_sdr(reference: &[f64], estimate: &[f64]) -> f64 {
    let dot = reference
        .iter()
        .zip(estimate)
        .map(|(a, b)| a * b)
        .sum::<f64>();
    let scale = dot / reference.iter().map(|x| x * x).sum::<f64>().max(1e-30);
    let target_energy = reference.iter().map(|x| (x * scale).powi(2)).sum::<f64>();
    let noise_energy = reference
        .iter()
        .zip(estimate)
        .map(|(a, b)| (a * scale - b).powi(2))
        .sum::<f64>();
    10.0 * (target_energy / noise_energy.max(1e-30)).log10()
}

pub fn si_snr(reference: &[f64], estimate: &[f64]) -> f64 {
    let rm = reference.iter().sum::<f64>() / reference.len() as f64;
    let em = estimate.iter().sum::<f64>() / estimate.len() as f64;
    si_sdr(
        &reference.iter().map(|x| x - rm).collect::<Vec<_>>(),
        &estimate.iter().map(|x| x - em).collect::<Vec<_>>(),
    )
}

pub fn snr(reference: &[f64], estimate: &[f64]) -> f64 {
    let signal = reference.iter().map(|sample| sample * sample).sum::<f64>();
    let noise = reference
        .iter()
        .zip(estimate)
        .map(|(a, b)| (a - b).powi(2))
        .sum::<f64>();
    10.0 * (signal / noise.max(1e-30)).log10()
}

pub fn segmental_snr(reference: &[f64], estimate: &[f64], sample_rate: u32) -> f64 {
    let window = (sample_rate as usize / 50).max(1);
    let mut values = Vec::new();
    for (r, e) in reference.chunks(window).zip(estimate.chunks(window)) {
        let signal = r.iter().map(|sample| sample * sample).sum::<f64>();
        if signal > 1e-12 {
            values.push(snr(r, e).clamp(-10.0, 35.0));
        }
    }
    values.iter().sum::<f64>() / values.len().max(1) as f64
}

fn correlation(a: &[f64], b: &[f64]) -> f64 {
    let dot = a.iter().zip(b).map(|(a, b)| a * b).sum::<f64>();
    dot / (a.iter().map(|x| x * x).sum::<f64>() * b.iter().map(|x| x * x).sum::<f64>())
        .sqrt()
        .max(1e-30)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn metrics_ignore_gain() {
        let reference = [1.0, -1.0, 0.5, -0.5];
        let estimate = [0.5, -0.5, 0.25, -0.25];
        assert!(si_sdr(&reference, &estimate) > 250.0);
        assert!(si_snr(&reference, &estimate) > 250.0);
    }

    #[test]
    fn comparison_reports_quality_improvement_in_all_formats() {
        let clean = Audio {
            sample_rate: 16_000,
            channels: vec![(0..1600)
                .map(|index| (index as f64 * 0.031).sin() * 0.5)
                .collect()],
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
            channel_mask: None,
        };
        let noisy = Audio {
            sample_rate: clean.sample_rate,
            channels: vec![clean.channels[0]
                .iter()
                .enumerate()
                .map(|(index, sample)| sample + if index % 2 == 0 { 0.1 } else { -0.1 })
                .collect()],
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
            channel_mask: None,
        };
        let enhanced = Audio {
            sample_rate: clean.sample_rate,
            channels: vec![clean.channels[0]
                .iter()
                .enumerate()
                .map(|(index, sample)| sample + if index % 2 == 0 { 0.02 } else { -0.02 })
                .collect()],
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
            channel_mask: None,
        };

        let report = ComparisonReport::compare(&clean, &noisy, &enhanced).unwrap();
        assert!(report.enhanced.snr_db > report.noisy.snr_db);
        assert!(report.json().contains("\"improvement\""));
        assert!(report.markdown().contains("Segmental SNR"));
        assert!(report.html().starts_with("<!doctype html>"));
    }
}