ferraus-denoise 0.1.2

Noise reduction for ferraus: noise profiles, spectral gating, adaptive tracking, RNNoise
Documentation
//! Synthetic fixtures for the denoise acceptance tests (generated by code, never committed).
#![allow(dead_code)]

use ferraus_core::testing::noise;
use ferraus_core::AudioBuffer;
use std::f64::consts::PI;

pub const SR: u32 = 48_000;

/// A clean test signal with a mask of "voiced" samples (where the clean signal is active).
pub struct Clean {
    pub buf: AudioBuffer,
    pub voiced: Vec<bool>,
}

fn formant_gain(f: f64, formants: &[(f64, f64)]) -> f64 {
    // sum of resonance magnitudes (2-pole style) plus a small floor
    let mut g = 0.02;
    for (fc, bw) in formants {
        let x = (f - fc) / (bw / 2.0);
        g += 1.0 / (1.0 + x * x).sqrt();
    }
    g
}

/// Speech-like: a glottal-pulse-like harmonic series (1/h rolloff) with a pitch contour,
/// shaped by vowel formants that change per syllable, under syllable envelopes grouped
/// into words with pauses between them (about 60% of the time voiced, like read speech).
/// `lead_s` seconds of silence first.
pub fn speech_like(lead_s: f64, secs: f64, amp: f32) -> Clean {
    let sr = SR as f64;
    let n_lead = (lead_s * sr) as usize;
    let n = (secs * sr) as usize;
    let vowels: [[(f64, f64); 3]; 5] = [
        [(730.0, 90.0), (1090.0, 110.0), (2440.0, 170.0)], // a
        [(270.0, 60.0), (2290.0, 100.0), (3010.0, 120.0)], // i
        [(300.0, 60.0), (870.0, 90.0), (2240.0, 120.0)],   // u
        [(530.0, 70.0), (1840.0, 100.0), (2480.0, 140.0)], // e
        [(570.0, 80.0), (840.0, 90.0), (2410.0, 140.0)],   // o
    ];
    // syllable schedule (start, duration): words of 2-3 syllables, 40 ms gaps inside a
    // word, 220 ms pauses between words, repeated to fill `secs`
    let pattern: [(f64, usize); 3] = [(0.16, 2), (0.13, 3), (0.19, 2)];
    let mut sched = Vec::new();
    let mut t = 0.05;
    let mut done = false;
    while !done {
        for (dur, count) in pattern {
            for _ in 0..count {
                if t + dur > secs {
                    done = true;
                    break;
                }
                sched.push((t, dur));
                t += dur + 0.04;
            }
            if done {
                break;
            }
            t += 0.18;
        }
    }
    let mut out = vec![0f32; n_lead + n];
    let mut voiced = vec![false; n_lead + n];
    let mut phase = 0.0f64;
    let max_h = 60;
    let mut si = 0;
    for i in 0..n {
        let t = i as f64 / sr;
        while si < sched.len() && t >= sched[si].0 + sched[si].1 {
            si += 1;
        }
        let f0_base = 125.0 + 25.0 * (2.0 * PI * 0.6 * t).sin();
        if si >= sched.len() || t < sched[si].0 {
            phase += 2.0 * PI * f0_base / sr;
            continue;
        }
        let (s0, dur) = sched[si];
        let ts = (t - s0) / dur;
        let f0 = f0_base + 15.0 * (ts - 0.5);
        phase += 2.0 * PI * f0 / sr;
        let env = (PI * ts).sin().powf(0.6);
        let v = &vowels[si % vowels.len()];
        let mut s = 0.0;
        for h in 1..=max_h {
            let f = h as f64 * f0;
            if f > 5500.0 {
                break;
            }
            s += formant_gain(f, v) / h as f64 * (h as f64 * phase).sin();
        }
        out[n_lead + i] = (s * env) as f32;
        voiced[n_lead + i] = env > 0.5;
    }
    normalize(&mut out, amp);
    Clean { buf: AudioBuffer::from_mono(SR, out), voiced }
}

/// Music-like: chords of harmonic tones (8 partials, 1/h), a new chord every 0.5 s with a
/// soft attack and a decay, released before the next chord.
pub fn music_like(lead_s: f64, secs: f64, amp: f32) -> Clean {
    let sr = SR as f64;
    let n_lead = (lead_s * sr) as usize;
    let n = (secs * sr) as usize;
    let chords: [[f64; 4]; 4] = [
        [261.63, 329.63, 392.00, 523.25],
        [220.00, 261.63, 329.63, 440.00],
        [174.61, 220.00, 261.63, 349.23],
        [196.00, 246.94, 293.66, 392.00],
    ];
    let dur = 0.5;
    let mut out = vec![0f32; n_lead + n];
    let mut voiced = vec![false; n_lead + n];
    for i in 0..n {
        let t = i as f64 / sr;
        let ci = (t / dur).floor() as usize;
        let tc = t - ci as f64 * dur;
        let env = (tc / 0.02).min(1.0) * (-tc * 3.0).exp() * ((dur - tc) / 0.12).clamp(0.0, 1.0);
        let mut s = 0.0;
        for f in chords[ci % chords.len()] {
            for h in 1..=8 {
                s += (2.0 * PI * f * h as f64 * t).sin() / h as f64;
            }
        }
        out[n_lead + i] = (s * env) as f32;
        voiced[n_lead + i] = env > 0.5;
    }
    normalize(&mut out, amp);
    Clean { buf: AudioBuffer::from_mono(SR, out), voiced }
}

fn normalize(x: &mut [f32], peak: f32) {
    let m = x.iter().fold(0f32, |a, v| a.max(v.abs()));
    if m > 0.0 {
        for v in x.iter_mut() {
            *v *= peak / m;
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NoiseKind {
    /// White hiss.
    Hiss,
    /// Pink-ish fan noise (Kellet pink plus broadband airflow hiss) with a faint blade tone.
    Fan,
    /// 60 Hz hum with harmonics.
    Hum,
    /// Brown "street" rumble (leaky integrated white noise).
    Street,
}

impl NoiseKind {
    pub const ALL: [NoiseKind; 4] = [NoiseKind::Hiss, NoiseKind::Fan, NoiseKind::Hum, NoiseKind::Street];
}

/// Unit-RMS stationary noise of `kind`.
pub fn make_noise(kind: NoiseKind, secs: f64, seed: u64) -> Vec<f32> {
    let w = noise(SR, secs, 1.0, seed).data.remove(0);
    let n = w.len();
    let mut x = match kind {
        NoiseKind::Hiss => w,
        NoiseKind::Fan => {
            let (mut b0, mut b1, mut b2, mut b3, mut b4, mut b5, mut b6) = (0f32, 0f32, 0f32, 0f32, 0f32, 0f32, 0f32);
            let mut out = Vec::with_capacity(n);
            for v in w.iter() {
                b0 = 0.99886 * b0 + v * 0.0555179;
                b1 = 0.99332 * b1 + v * 0.0750759;
                b2 = 0.96900 * b2 + v * 0.153_852;
                b3 = 0.86650 * b3 + v * 0.3104856;
                b4 = 0.55000 * b4 + v * 0.5329522;
                b5 = -0.7616 * b5 - v * 0.0168980;
                let pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + v * 0.5362;
                b6 = v * 0.115926;
                out.push(pink);
            }
            // pink-ish: pink rumble plus the broadband hiss of the airflow and a faint
            // blade-pass tone
            let (pr, wr) = (rms(&out) as f32, rms(&w) as f32);
            for (i, (o, v)) in out.iter_mut().zip(&w).enumerate() {
                let t = i as f64 / SR as f64;
                let tone = 0.1 * (2.0 * PI * 180.0 * t).sin() as f32;
                *o = *o / pr + 0.7 * v / wr + tone;
            }
            out
        }
        NoiseKind::Hum => (0..n)
            .map(|i| {
                let t = i as f64 / SR as f64;
                let mut s = 0.0;
                for (h, a) in [(1.0, 1.0), (2.0, 0.5), (3.0, 0.35), (4.0, 0.2), (5.0, 0.12), (6.0, 0.08)] {
                    s += a * (2.0 * PI * 60.0 * h * t + h).sin();
                }
                // a little broadband floor, as real hum always comes with some hiss
                (s + 0.02 * w[i] as f64) as f32
            })
            .collect(),
        NoiseKind::Street => {
            let mut y = 0f32;
            let mut out = Vec::with_capacity(n);
            for v in &w {
                y = 0.995 * y + 0.05 * v;
                out.push(y);
            }
            out
        }
    };
    let rms = (x.iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / n as f64).sqrt() as f32;
    for v in x.iter_mut() {
        *v /= rms.max(1e-12);
    }
    x
}

pub fn rms(x: &[f32]) -> f64 {
    (x.iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / x.len().max(1) as f64).sqrt()
}

/// Mix `clean` with `noise` scaled so the clean signal's RMS over its active region
/// (after `lead` samples) is `snr_db` above the noise RMS.
pub fn mix(clean: &AudioBuffer, noise: &[f32], snr_db: f64, lead: usize) -> (AudioBuffer, Vec<f32>) {
    let c = &clean.data[0];
    let sig_rms = rms(&c[lead..]);
    let g = (sig_rms / 10f64.powf(snr_db / 20.0)) as f32;
    let scaled: Vec<f32> = noise[..c.len()].iter().map(|v| v * g).collect();
    let mixed: Vec<f32> = c.iter().zip(&scaled).map(|(a, b)| a + b).collect();
    (AudioBuffer::from_mono(clean.sample_rate, mixed), scaled)
}

/// Gain (dB) of the projection of `out` onto `clean` over the samples where `mask` is
/// true: how much of the clean signal's level survives (0 dB = untouched).
pub fn retained_db(clean: &[f32], out: &[f32], mask: &[bool]) -> f64 {
    let (mut dot, mut cc) = (0f64, 0f64);
    for i in 0..clean.len().min(out.len()) {
        if mask[i] {
            dot += clean[i] as f64 * out[i] as f64;
            cc += (clean[i] as f64).powi(2);
        }
    }
    20.0 * (dot / cc.max(1e-20)).log10()
}