denoize 0.4.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 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),
            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},\"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, 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| Stereo side SDR | {} |\n| Correlation error | {} |\n| STOI | {} |\n| PESQ | {} |", self.si_sdr_db, self.si_snr_db, db(self.stereo_side_sdr_db), display(self.correlation_error, 6), display(self.stoi, 4), display(self.pesq, 3))
    }
}

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<_>>(),
    )
}

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);
    }
}