灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-daw/src/dsp/vox.rs
//! Vocal effects.
//!
//! * **Vox Filter** - a steep high-pass / low-pass (12, 24 or 48 dB per octave, Butterworth stages) with a resonant corner, a spectral
//!   tilt and a *dynamic* high-pass that climbs when the voice is quiet, so breaths, room rumble and noise between phrases are cut
//!   while the full body of a loud phrase is kept.
//! * **Vox Reverb** - an 8-line modulated feedback-delay-network reverb built for voices: pre-delay, early reflections, decay,
//!   damping, a low-cut and high-cut *on the wet signal only*, a width control and a **duck** that pulls the reverb down while the
//!   voice sings and lets it bloom back in the gaps.
//! * **Vocal Halo** - makes a voice pop and open up in stereo: a mono-safe decorrelated "halo" of the top end (it cancels exactly in
//!   mono), an artificial double that wanders slightly like a second take, a harmonic presence exciter, an air shelf, a de-esser, a
//!   transient shaper, a bass-centred width control and a ducked bloom.
use super::fx::{Effect, Reverb};
use crate::dsp::TAU;

// ── filters ─────────────────────────────────────────────────────────────────
#[derive(Clone, Copy, Default)]
struct Bq {
    b0: f64,
    b1: f64,
    b2: f64,
    a1: f64,
    a2: f64,
    z1: f64,
    z2: f64,
}
impl Bq {
    #[inline]
    fn tick(&mut self, x: f64) -> f64 {
        let y = self.b0 * x + self.z1;
        self.z1 = self.b1 * x - self.a1 * y + self.z2;
        self.z2 = self.b2 * x - self.a2 * y;
        y
    }
    fn set(&mut self, b0: f64, b1: f64, b2: f64, a0: f64, a1: f64, a2: f64) {
        self.b0 = b0 / a0;
        self.b1 = b1 / a0;
        self.b2 = b2 / a0;
        self.a1 = a1 / a0;
        self.a2 = a2 / a0;
    }
    fn trig(f: f64, q: f64, sr: f64) -> (f64, f64, f64) {
        let w = TAU as f64 * (f.clamp(10.0, sr * 0.49)) / sr;
        (w.cos(), w.sin(), w.sin() / (2.0 * q.max(0.1)))
    }
    fn lowpass(&mut self, f: f64, q: f64, sr: f64) {
        let (c, _, al) = Self::trig(f, q, sr);
        self.set((1.0 - c) / 2.0, 1.0 - c, (1.0 - c) / 2.0, 1.0 + al, -2.0 * c, 1.0 - al);
    }
    fn highpass(&mut self, f: f64, q: f64, sr: f64) {
        let (c, _, al) = Self::trig(f, q, sr);
        self.set((1.0 + c) / 2.0, -(1.0 + c), (1.0 + c) / 2.0, 1.0 + al, -2.0 * c, 1.0 - al);
    }
    fn bandpass(&mut self, f: f64, q: f64, sr: f64) {
        let (c, _, al) = Self::trig(f, q, sr);
        self.set(al, 0.0, -al, 1.0 + al, -2.0 * c, 1.0 - al);
    }
    fn high_shelf(&mut self, f: f64, db: f64, sr: f64) {
        let (c, s, _) = Self::trig(f, 0.7071, sr);
        let a = 10f64.powf(db / 40.0);
        let be = 2.0 * a.sqrt() * (s * 0.70710678);
        self.set(
            a * ((a + 1.0) + (a - 1.0) * c + be),
            -2.0 * a * ((a - 1.0) + (a + 1.0) * c),
            a * ((a + 1.0) + (a - 1.0) * c - be),
            (a + 1.0) - (a - 1.0) * c + be,
            2.0 * ((a - 1.0) - (a + 1.0) * c),
            (a + 1.0) - (a - 1.0) * c - be,
        );
    }
    fn low_shelf(&mut self, f: f64, db: f64, sr: f64) {
        let (c, s, _) = Self::trig(f, 0.7071, sr);
        let a = 10f64.powf(db / 40.0);
        let be = 2.0 * a.sqrt() * (s * 0.70710678);
        self.set(
            a * ((a + 1.0) - (a - 1.0) * c + be),
            2.0 * a * ((a - 1.0) - (a + 1.0) * c),
            a * ((a + 1.0) - (a - 1.0) * c - be),
            (a + 1.0) + (a - 1.0) * c + be,
            -2.0 * ((a - 1.0) + (a + 1.0) * c),
            (a + 1.0) + (a - 1.0) * c - be,
        );
    }
    fn identity() -> Bq {
        Bq { b0: 1.0, ..Bq::default() }
    }
}

/// one-pole level follower with separate attack and release (times in seconds)
#[derive(Clone, Copy)]
struct Follow {
    y: f32,
    a: f32,
    r: f32,
}
impl Follow {
    fn new(sr: f32, attack: f32, release: f32) -> Follow {
        Follow { y: 0.0, a: 1.0 - (-1.0 / (attack * sr)).exp(), r: 1.0 - (-1.0 / (release * sr)).exp() }
    }
    #[inline]
    fn tick(&mut self, x: f32) -> f32 {
        let c = if x > self.y { self.a } else { self.r };
        self.y += (x - self.y) * c;
        self.y
    }
}

fn finite(x: f32) -> f32 {
    if x.is_finite() {
        x
    } else {
        0.0
    }
}

// ── Vox Filter ──────────────────────────────────────────────────────────────
const BUTTER: [&[f64]; 3] = [&[0.70710678], &[0.54119610, 1.30656296], &[0.50979558, 0.60134489, 0.89997622, 2.56291545]];

pub struct VoxFilter {
    sr: f64,
    hp_hz: f64,
    lp_hz: f64,
    slope: usize, // index into BUTTER: 0 = 12 dB, 1 = 24, 2 = 48
    res: f64,
    tilt_db: f64,
    dynamic: f64,
    mix: f32,
    hp: [[Bq; 4]; 2],
    lp: [[Bq; 2]; 2],
    low: [Bq; 2],
    high: [Bq; 2],
    env: Follow,
    eff_hp: f64,
    count: usize,
    dirty: bool,
}
impl VoxFilter {
    pub fn new(sr: f32) -> Self {
        let mut f = VoxFilter {
            sr: sr as f64,
            hp_hz: 90.0,
            lp_hz: 20000.0,
            slope: 1,
            res: 0.0,
            tilt_db: 0.0,
            dynamic: 0.0,
            mix: 1.0,
            hp: [[Bq::identity(); 4]; 2],
            lp: [[Bq::identity(); 2]; 2],
            low: [Bq::identity(); 2],
            high: [Bq::identity(); 2],
            env: Follow::new(sr, 0.01, 0.25),
            eff_hp: 90.0,
            count: 0,
            dirty: true,
        };
        f.update(90.0);
        f
    }
    fn stages(&self) -> usize {
        [1, 2, 4][self.slope]
    }
    fn update(&mut self, hp_hz: f64) {
        self.eff_hp = hp_hz;
        let n = self.stages();
        let qs = BUTTER[self.slope];
        for c in 0..2 {
            for (i, q) in qs.iter().enumerate().take(n) {
                // the resonance lifts only the sharpest stage so the corner gets a peak without ringing the whole slope
                let qq = if i + 1 == n { q * (1.0 + self.res * 4.0) } else { *q };
                self.hp[c][i].highpass(hp_hz, qq, self.sr);
            }
            let lq = BUTTER[self.slope.min(1)];
            for (i, q) in lq.iter().enumerate() {
                let qq = if i + 1 == lq.len() { q * (1.0 + self.res * 2.0) } else { *q };
                self.lp[c][i].lowpass(self.lp_hz, qq, self.sr);
            }
            self.low[c].low_shelf(400.0, -self.tilt_db, self.sr);
            self.high[c].high_shelf(3000.0, self.tilt_db, self.sr);
        }
        self.dirty = false;
    }
}
impl Effect for VoxFilter {
    fn process(&mut self, b: &mut [f32]) {
        if self.dirty {
            self.update(self.hp_hz);
        }
        let n = self.stages();
        let lp_on = self.lp_hz < 17000.0;
        let lp_n = BUTTER[self.slope.min(1)].len();
        for fr in b.chunks_exact_mut(2) {
            // the dynamic high-pass follows the level: quiet -> the corner climbs up to three octaves
            let lvl = self.env.tick((fr[0].abs() + fr[1].abs()) * 0.5);
            self.count += 1;
            if self.dynamic > 0.001 && self.count % 48 == 0 {
                let db = 20.0 * (lvl.max(1e-6) as f64).log10();
                let quiet = 1.0 - ((db + 50.0) / 34.0).clamp(0.0, 1.0);
                let target = (self.hp_hz * 2f64.powf(quiet * self.dynamic * 3.0)).min(2500.0);
                if (target / self.eff_hp - 1.0).abs() > 0.01 {
                    self.update(target);
                }
            }
            let dry = [fr[0], fr[1]];
            for c in 0..2 {
                let mut x = dry[c] as f64;
                for i in 0..n {
                    x = self.hp[c][i].tick(x);
                }
                if lp_on {
                    for i in 0..lp_n {
                        x = self.lp[c][i].tick(x);
                    }
                }
                if self.tilt_db.abs() > 0.05 {
                    x = self.high[c].tick(self.low[c].tick(x));
                }
                fr[c] = finite(dry[c] * (1.0 - self.mix) + x as f32 * self.mix);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0) as f64;
        match n {
            "hp" => self.hp_hz = 20.0 * 40f64.powf(v),
            "slope" => self.slope = if v < 0.34 { 0 } else if v < 0.67 { 1 } else { 2 },
            "lp" => self.lp_hz = 1000.0 * 20f64.powf(v),
            "res" => self.res = v,
            "tilt" => self.tilt_db = (v - 0.5) * 12.0,
            "dyn" => self.dynamic = v,
            "mix" => self.mix = v as f32,
            _ => return,
        }
        self.dirty = true;
    }
    fn name(&self) -> &'static str {
        "Vox Filter"
    }
}

// ── Vox Reverb ──────────────────────────────────────────────────────────────
const NLINE: usize = 8;
const BASE: [f32; NLINE] = [1031.0, 1327.0, 1523.0, 1789.0, 2029.0, 2347.0, 2713.0, 3109.0];

struct Line {
    buf: Vec<f32>,
    w: usize,
    lp: f32,
    ph: f32,
    rate: f32,
}

pub struct VoxReverb {
    sr: f32,
    lines: Vec<Line>,
    pre: [Vec<f32>; 2],
    pre_w: usize,
    er: Vec<f32>,
    er_w: usize,
    mix: f32,
    rt60: f32,
    pre_samples: usize,
    size: f32,
    damp: f32,
    moddepth: f32,
    duck: f32,
    width: f32,
    early: f32,
    gains: [f32; NLINE],
    hp: [Bq; 2],
    lp: [Bq; 2],
    env: Follow,
    duck_g: f32,
    dirty: bool,
    lowcut: f64,
    highcut: f64,
}
impl VoxReverb {
    pub fn new(sr: f32) -> Self {
        let k = sr / 44100.0;
        let lines = (0..NLINE)
            .map(|i| Line { buf: vec![0.0; ((BASE[i] * 1.7 * k) as usize) + 64], w: 0, lp: 0.0, ph: i as f32 * 0.37, rate: 0.11 + i as f32 * 0.043 })
            .collect();
        let mut r = VoxReverb {
            sr,
            lines,
            pre: [vec![0.0; (sr * 0.26) as usize + 8], vec![0.0; (sr * 0.26) as usize + 8]],
            pre_w: 0,
            er: vec![0.0; (sr * 0.12) as usize + 8],
            er_w: 0,
            mix: 0.25,
            rt60: 1.8,
            pre_samples: (0.02 * sr) as usize,
            size: 1.0,
            damp: 0.45,
            moddepth: 0.3,
            duck: 0.35,
            width: 1.0,
            early: 0.4,
            gains: [0.8; NLINE],
            hp: [Bq::identity(); 2],
            lp: [Bq::identity(); 2],
            env: Follow::new(sr, 0.005, 0.18),
            duck_g: 1.0,
            dirty: true,
            lowcut: 200.0,
            highcut: 9000.0,
        };
        r.update();
        r
    }
    fn update(&mut self) {
        let k = self.sr / 44100.0;
        for i in 0..NLINE {
            let d = BASE[i] * k * self.size;
            self.gains[i] = 10f32.powf(-3.0 * d / self.sr / self.rt60.max(0.1)).min(0.9995);
        }
        for c in 0..2 {
            self.hp[c].highpass(self.lowcut, 0.7071, self.sr as f64);
            self.lp[c].lowpass(self.highcut, 0.7071, self.sr as f64);
        }
        self.dirty = false;
    }
}
impl Effect for VoxReverb {
    fn process(&mut self, b: &mut [f32]) {
        if self.dirty {
            self.update();
        }
        let k = self.sr / 44100.0;
        let prelen = self.pre[0].len();
        let erlen = self.er.len();
        let er_taps: [(f32, f32, usize); 6] = [(0.0073, 0.60, 0), (0.0119, 0.50, 1), (0.0171, 0.42, 0), (0.0237, 0.36, 1), (0.0313, 0.28, 0), (0.0419, 0.20, 1)];
        let damp = self.damp.clamp(0.0, 0.93);
        for fr in b.chunks_exact_mut(2) {
            let (l, r) = (fr[0], fr[1]);
            // pre-delay
            self.pre[0][self.pre_w] = l;
            self.pre[1][self.pre_w] = r;
            let rp = (self.pre_w + prelen - self.pre_samples.min(prelen - 1)) % prelen;
            let (pl, pr) = (self.pre[0][rp], self.pre[1][rp]);
            self.pre_w = (self.pre_w + 1) % prelen;
            // early reflections from the mono pre-delayed signal
            self.er[self.er_w] = (pl + pr) * 0.5;
            let (mut el, mut er_) = (0.0f32, 0.0f32);
            for (t, g, side) in er_taps {
                let d = ((t * self.size * self.sr) as usize).min(erlen - 1);
                let v = self.er[(self.er_w + erlen - d) % erlen] * g;
                if side == 0 {
                    el += v;
                } else {
                    er_ += v;
                }
            }
            self.er_w = (self.er_w + 1) % erlen;
            // late tail: eight modulated lines, Hadamard feedback
            let mut v = [0.0f32; NLINE];
            for (i, ln) in self.lines.iter_mut().enumerate() {
                let n = ln.buf.len();
                ln.ph = (ln.ph + ln.rate / self.sr).fract();
                let m = (ln.ph * TAU).sin() * self.moddepth * 5.0 * k;
                let d = (BASE[i] * k * self.size + m).clamp(8.0, (n - 4) as f32);
                let rp = ln.w as f32 + n as f32 - d;
                let i0 = rp.floor();
                let f = rp - i0;
                let a = ln.buf[i0 as usize % n];
                let bb = ln.buf[(i0 as usize + 1) % n];
                let y = a * (1.0 - f) + bb * f;
                ln.lp = y * (1.0 - damp) + ln.lp * damp;
                v[i] = ln.lp * self.gains[i];
            }
            // 8x8 Hadamard (fast butterflies), normalised
            let mut h = v;
            let mut step = 1;
            while step < NLINE {
                for base in (0..NLINE).step_by(step * 2) {
                    for j in base..base + step {
                        let (x, y) = (h[j], h[j + step]);
                        h[j] = x + y;
                        h[j + step] = x - y;
                    }
                }
                step *= 2;
            }
            let norm = 0.35355339;
            for (i, ln) in self.lines.iter_mut().enumerate() {
                let inj = if i % 2 == 0 { pl } else { pr } * if i % 4 < 2 { 0.25 } else { -0.25 };
                let nw = finite(inj + h[i] * norm);
                let n = ln.buf.len();
                ln.buf[ln.w] = nw.clamp(-8.0, 8.0);
                ln.w = (ln.w + 1) % n;
            }
            let late_l = (v[0] - v[2] + v[4] - v[6]) * 0.45;
            let late_r = (v[1] - v[3] + v[5] - v[7]) * 0.45;
            let (mut wl, mut wr) = (late_l + el * self.early, late_r + er_ * self.early);
            // the wet signal only: low cut and high cut
            wl = self.lp[0].tick(self.hp[0].tick(wl as f64)) as f32;
            wr = self.lp[1].tick(self.hp[1].tick(wr as f64)) as f32;
            // duck: the voice pushes the reverb down, the gaps let it bloom
            let lvl = self.env.tick((l.abs() + r.abs()) * 0.5);
            let target = 1.0 - self.duck * (lvl * 7.0).min(1.0) * 0.92;
            self.duck_g += (target - self.duck_g) * 0.002 * (self.sr / 44100.0).recip().min(2.0).max(0.5);
            let (m_, s_) = ((wl + wr) * 0.5, (wl - wr) * 0.5 * self.width * 2.0);
            let g = self.duck_g * self.mix * 1.4;
            fr[0] = finite(l + (m_ + s_) * g);
            fr[1] = finite(r + (m_ - s_) * g);
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "mix" => self.mix = v,
            "decay" => self.rt60 = 0.2 * 40f32.powf(v),
            "predelay" => self.pre_samples = (v * 0.2 * self.sr) as usize,
            "size" => self.size = 0.6 + v,
            "damp" => self.damp = v * 0.9,
            "lowcut" => self.lowcut = 20.0 * 50f64.powf(v as f64),
            "highcut" => self.highcut = 2000.0 * 10f64.powf(v as f64),
            "duck" => self.duck = v,
            "width" => self.width = v,
            "early" => self.early = v,
            "mod" => self.moddepth = v,
            _ => return,
        }
        self.dirty = true;
    }
    fn name(&self) -> &'static str {
        "Vox Reverb"
    }
}

// ── Vocal Halo ──────────────────────────────────────────────────────────────
struct Ring {
    buf: Vec<f32>,
    w: usize,
}
impl Ring {
    fn new(n: usize) -> Ring {
        Ring { buf: vec![0.0; n], w: 0 }
    }
    #[inline]
    fn push(&mut self, x: f32) {
        self.buf[self.w] = x;
        self.w = (self.w + 1) % self.buf.len();
    }
    #[inline]
    fn read(&self, d: f32) -> f32 {
        let n = self.buf.len();
        let d = d.clamp(1.0, (n - 2) as f32);
        let rp = self.w as f32 + n as f32 - d - 1.0;
        let i0 = rp.floor();
        let f = rp - i0;
        let i0 = i0 as usize % n;
        self.buf[i0] * (1.0 - f) + self.buf[(i0 + 1) % n] * f
    }
}

#[derive(Clone, Copy, Default)]
struct Ap1 {
    a: f32,
    x1: f32,
    y1: f32,
}
impl Ap1 {
    #[inline]
    fn tick(&mut self, x: f32) -> f32 {
        let y = -self.a * x + self.x1 + self.a * self.y1;
        self.x1 = x;
        self.y1 = y;
        y
    }
}

pub struct VocalHalo {
    sr: f32,
    // controls
    width: f32,
    halo: f32,
    double: f32,
    presence: f32,
    air: f32,
    deess: f32,
    punch: f32,
    bloom: f32,
    mix: f32,
    // state
    side_hp: Bq,
    dbl_hp: [Bq; 2],
    dbl_lp: [Bq; 2],
    ring: Ring,
    wander: [f32; 2],
    ph: [f32; 4],
    pres_bp: Bq,
    pres_hp: Bq,
    air_shelf: Bq,
    des_bp: Bq,
    halo_hp: Bq,
    halo_ap: [Ap1; 5],
    bloom_hp: [Bq; 2],
    verb: Reverb,
    tmp: Vec<f32>,
    f_fast: Follow,
    f_slow: Follow,
    f_voice: Follow,
    f_des: Follow,
    duck_g: f32,
    count: usize,
    des_g: f32,
    punch_g: f32,
}
impl VocalHalo {
    pub fn new(sr: f32) -> Self {
        let mut verb = Reverb::new(sr);
        verb.set_param("mix", 1.0);
        verb.set_param("size", 0.55);
        verb.set_param("damp", 0.55);
        let mut h = VocalHalo {
            sr,
            width: 0.55,
            halo: 0.4,
            double: 0.3,
            presence: 0.35,
            air: 0.3,
            deess: 0.3,
            punch: 0.25,
            bloom: 0.2,
            mix: 1.0,
            side_hp: Bq::identity(),
            dbl_hp: [Bq::identity(); 2],
            dbl_lp: [Bq::identity(); 2],
            ring: Ring::new((sr * 0.08) as usize + 8),
            wander: [0.0; 2],
            ph: [0.0, 0.25, 0.5, 0.75],
            pres_bp: Bq::identity(),
            pres_hp: Bq::identity(),
            air_shelf: Bq::identity(),
            des_bp: Bq::identity(),
            halo_hp: Bq::identity(),
            halo_ap: [Ap1::default(); 5],
            bloom_hp: [Bq::identity(); 2],
            verb,
            tmp: vec![],
            f_fast: Follow::new(sr, 0.001, 0.015),
            f_slow: Follow::new(sr, 0.02, 0.15),
            f_voice: Follow::new(sr, 0.004, 0.2),
            f_des: Follow::new(sr, 0.0006, 0.04),
            duck_g: 1.0,
            count: 0,
            des_g: 1.0,
            punch_g: 1.0,
        };
        h.setup(150.0);
        h
    }
    fn setup(&mut self, focus_hz: f64) {
        let sr = self.sr as f64;
        self.side_hp.highpass(focus_hz, 0.7071, sr);
        for c in 0..2 {
            self.dbl_hp[c].highpass(180.0, 0.7071, sr);
            self.dbl_lp[c].lowpass(9500.0, 0.7071, sr);
            self.bloom_hp[c].highpass(350.0, 0.7071, sr);
        }
        self.pres_bp.bandpass(4200.0, 0.9, sr);
        self.pres_hp.highpass(3200.0, 0.7071, sr);
        self.air_shelf.high_shelf(9000.0, self.air as f64 * 6.0, sr);
        self.des_bp.bandpass(7000.0, 1.3, sr);
        self.halo_hp.highpass(4000.0, 0.7071, sr);
        for (i, a) in self.halo_ap.iter_mut().enumerate() {
            a.a = [0.62, -0.48, 0.37, -0.27, 0.19][i];
        }
    }
}
impl Effect for VocalHalo {
    fn process(&mut self, b: &mut [f32]) {
        let frames = b.len() / 2;
        // the bloom: a short ambience of the mid signal, rendered for the whole block by the inner reverb
        let bloom_on = self.bloom > 0.01;
        if bloom_on {
            self.tmp.clear();
            for fr in b.chunks_exact(2) {
                let m = (fr[0] + fr[1]) * 0.5;
                self.tmp.push(m);
                self.tmp.push(m);
            }
            let dry: Vec<f32> = self.tmp.clone();
            self.verb.process(&mut self.tmp);
            for (t, d) in self.tmp.iter_mut().zip(dry.iter()) {
                *t -= *d;
            }
        }
        let sr = self.sr;
        for i in 0..frames {
            let (l, r) = (b[i * 2], b[i * 2 + 1]);
            let m = (l + r) * 0.5;
            let mut s = (l - r) * 0.5;
            self.count += 1;
            // width: the side signal, kept out of the bass so the low end stays centred
            s = self.side_hp.tick(s as f64) as f32 * (self.width * 2.0);
            // voice level for ducking the bloom
            let lvl = self.f_voice.tick(m.abs());
            // ── de-esser: only the sibilant band is turned down, by how far it sticks out
            let hi = self.des_bp.tick(m as f64) as f32;
            let env_hi = self.f_des.tick(hi.abs());
            if self.count % 8 == 0 {
                let db = 20.0 * env_hi.max(1e-5).log10();
                let over = (db + 32.0).max(0.0);
                let red = over * self.deess * 0.75;
                self.des_g = 10f32.powf(-red / 20.0);
            }
            let mut mm = m - hi * (1.0 - self.des_g);
            // ── punch: boost the front of every note (fast level above slow level)
            let fast = self.f_fast.tick(mm.abs());
            let slow = self.f_slow.tick(mm.abs());
            let tgt = 1.0 + self.punch * ((fast - slow) / (slow + 0.02)).clamp(0.0, 1.5) * 0.8;
            self.punch_g += (tgt - self.punch_g) * 0.1;
            mm *= self.punch_g;
            // ── presence: harmonics generated from the 3-6 kHz band, added back
            let pb = self.pres_bp.tick(mm as f64) as f32;
            let ex = self.pres_hp.tick((pb * 3.0).tanh() as f64) as f32;
            mm += ex * self.presence * 0.5;
            // ── air shelf
            mm = self.air_shelf.tick(mm as f64) as f32;
            // ── halo: the top end, phase-smeared, pushed to one side and its negative to the other: nothing is left in mono
            let mut hv = self.halo_hp.tick(mm as f64) as f32;
            for a in self.halo_ap.iter_mut() {
                hv = a.tick(hv);
            }
            let halo = hv * self.halo * 0.9;
            // ── artificial double: two delayed, filtered copies that drift like a second take, one each side
            self.ring.push(mm);
            for k in 0..2 {
                self.ph[k] = (self.ph[k] + [0.31, 0.77][k] / sr).fract();
                self.ph[k + 2] = (self.ph[k + 2] + [1.13, 0.53][k] / sr).fract();
                self.wander[k] = (self.ph[k] * TAU).sin() * 0.7 + (self.ph[k + 2] * TAU).sin() * 0.3;
            }
            let da = self.ring.read((0.017 + self.wander[0] * 0.0012) * sr);
            let db_ = self.ring.read((0.027 + self.wander[1] * 0.0016) * sr);
            let dl = self.dbl_lp[0].tick(self.dbl_hp[0].tick(da as f64)) as f32 * self.double * 0.75;
            let dr = self.dbl_lp[1].tick(self.dbl_hp[1].tick(db_ as f64)) as f32 * self.double * 0.75;
            // ── bloom, ducked by the voice
            let (mut bl, mut br) = (0.0, 0.0);
            if bloom_on {
                let target = 1.0 - (lvl * 8.0).min(1.0) * 0.9;
                self.duck_g += (target - self.duck_g) * 0.003;
                bl = self.bloom_hp[0].tick(self.tmp[i * 2] as f64) as f32 * self.bloom * 2.0 * self.duck_g;
                br = self.bloom_hp[1].tick(self.tmp[i * 2 + 1] as f64) as f32 * self.bloom * 2.0 * self.duck_g;
            }
            let wl = mm + s + halo + dl + bl;
            let wr = mm - s - halo + dr + br;
            b[i * 2] = finite(l * (1.0 - self.mix) + wl * self.mix).clamp(-4.0, 4.0);
            b[i * 2 + 1] = finite(r * (1.0 - self.mix) + wr * self.mix).clamp(-4.0, 4.0);
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "width" => self.width = v,
            "halo" => self.halo = v,
            "double" => self.double = v,
            "presence" => self.presence = v,
            "air" => {
                self.air = v;
                self.air_shelf.high_shelf(9000.0, v as f64 * 6.0, self.sr as f64);
            }
            "deess" => self.deess = v,
            "punch" => self.punch = v,
            "bloom" => self.bloom = v,
            "focus" => {
                let f = 80.0 * 4f64.powf(v as f64);
                self.side_hp.highpass(f, 0.7071, self.sr as f64);
            }
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Vocal Halo"
    }
}

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

    const SR: f32 = 48000.0;

    fn sine(f: f32, secs: f32, amp: f32) -> Vec<f32> {
        (0..(SR * secs) as usize).flat_map(|i| {
            let v = (i as f32 / SR * f * TAU).sin() * amp;
            [v, v]
        }).collect()
    }
    fn rms(b: &[f32]) -> f32 {
        (b.iter().map(|x| x * x).sum::<f32>() / b.len().max(1) as f32).sqrt()
    }
    fn tail(b: &[f32]) -> &[f32] {
        &b[b.len() / 2..]
    }
    fn noise(secs: f32, amp: f32) -> Vec<f32> {
        let mut s = 12345u32;
        (0..(SR * secs) as usize).flat_map(|_| {
            s = s.wrapping_mul(1664525).wrapping_add(1013904223);
            let v = ((s >> 8) as f32 / 8388608.0 - 1.0) * amp;
            [v, v]
        }).collect()
    }

    #[test]
    fn the_filter_cuts_the_low_end_steeper_with_a_steeper_slope() {
        let gain_at = |hz: f32, slope: f32| {
            let mut f = VoxFilter::new(SR);
            f.set_param("hp", 0.4);
            f.set_param("slope", slope);
            let mut x = sine(hz, 0.6, 0.5);
            f.process(&mut x);
            rms(tail(&x)) / (0.5 * 0.7071)
        };
        let (g12, g48) = (gain_at(40.0, 0.0), gain_at(40.0, 1.0));
        assert!(g12 < 0.8 && g48 < g12 * 0.35, "{g12} {g48}");
        assert!(gain_at(1000.0, 0.5) > 0.95, "the voice band passes");
        // mix 0 is the dry signal
        let mut f = VoxFilter::new(SR);
        f.set_param("mix", 0.0);
        let src = sine(60.0, 0.1, 0.5);
        let mut x = src.clone();
        f.process(&mut x);
        assert_eq!(x, src);
    }

    #[test]
    fn the_dynamic_high_pass_cleans_quiet_passages_more_than_loud_ones() {
        let at = |amp: f32, dynamic: f32| {
            let mut f = VoxFilter::new(SR);
            f.set_param("hp", 0.35);
            f.set_param("dyn", dynamic);
            let mut x = sine(250.0, 1.0, amp);
            f.process(&mut x);
            rms(tail(&x)) / (amp * 0.7071)
        };
        let quiet_static = at(0.003, 0.0);
        let quiet_dynamic = at(0.003, 1.0);
        let loud_dynamic = at(0.5, 1.0);
        assert!(quiet_dynamic < quiet_static * 0.6, "{quiet_dynamic} vs {quiet_static}");
        assert!(loud_dynamic > quiet_dynamic * 1.4, "{loud_dynamic} vs {quiet_dynamic}");
    }

    #[test]
    fn the_reverb_has_a_tail_that_follows_decay_and_a_wet_low_cut() {
        let tail_energy = |decay: f32, lowcut: f32| {
            let mut r = VoxReverb::new(SR);
            r.set_param("decay", decay);
            r.set_param("lowcut", lowcut);
            r.set_param("duck", 0.0);
            r.set_param("mix", 1.0);
            let mut x = vec![0.0f32; (SR * 2.0) as usize * 2];
            x[0] = 1.0;
            x[1] = 1.0;
            r.process(&mut x);
            assert!(x.iter().all(|v| v.is_finite() && v.abs() < 4.0));
            rms(&x[(SR * 1.0) as usize * 2..])
        };
        let (short, long) = (tail_energy(0.2, 0.2), tail_energy(0.8, 0.2));
        assert!(long > short * 3.0 && short >= 0.0, "{short} {long}");
        assert!(long > 1e-4, "the tail is audible after a second: {long}");
        // a high low-cut removes bass from the wet signal
        let wet_of = |lowcut: f32| {
            let mut r = VoxReverb::new(SR);
            r.set_param("lowcut", lowcut);
            r.set_param("duck", 0.0);
            r.set_param("mix", 1.0);
            let src = sine(60.0, 1.0, 0.3);
            let mut x = src.clone();
            r.process(&mut x);
            let diff: Vec<f32> = x.iter().zip(src.iter()).map(|(a, b)| a - b).collect();
            rms(tail(&diff))
        };
        assert!(wet_of(0.9) < wet_of(0.0) * 0.5, "the low cut takes the bass out of the wet signal: {} vs {}", wet_of(0.9), wet_of(0.0));
    }

    #[test]
    fn the_reverb_ducks_under_a_voice() {
        let wet_while_singing = |duck: f32| {
            let mut r = VoxReverb::new(SR);
            r.set_param("duck", duck);
            r.set_param("mix", 1.0);
            r.set_param("lowcut", 0.0);
            let mut x = noise(1.5, 0.4);
            let dry = x.clone();
            r.process(&mut x);
            let diff: Vec<f32> = x.iter().zip(dry.iter()).map(|(a, b)| a - b).collect();
            rms(tail(&diff))
        };
        assert!(wet_while_singing(1.0) < wet_while_singing(0.0) * 0.6);
    }

    #[test]
    fn the_halo_adds_stereo_that_cancels_in_mono_and_stays_in_bounds() {
        let mut h = VocalHalo::new(SR);
        for (n, v) in [("width", 0.5), ("halo", 1.0), ("double", 0.0), ("presence", 0.0), ("air", 0.0), ("deess", 0.0), ("punch", 0.0), ("bloom", 0.0), ("mix", 1.0)] {
            h.set_param(n, v);
        }
        let src = noise(1.0, 0.3);
        let mut x = src.clone();
        h.process(&mut x);
        // mono sum is unchanged, the sides are not
        let mono_err: f32 = x.chunks(2).zip(src.chunks(2)).map(|(a, b)| ((a[0] + a[1]) - (b[0] + b[1])).abs()).fold(0.0, f32::max);
        let side: f32 = rms(&x.chunks(2).flat_map(|a| [a[0] - a[1], 0.0]).collect::<Vec<_>>());
        assert!(mono_err < 1e-3, "halo is mono safe: {mono_err}");
        assert!(side > 0.01, "and wide: {side}");
        // everything on, loud input: finite and bounded
        let mut h = VocalHalo::new(SR);
        for n in ["width", "halo", "double", "presence", "air", "deess", "punch", "bloom"] {
            h.set_param(n, 1.0);
        }
        let mut loud = noise(1.0, 0.95);
        h.process(&mut loud);
        assert!(loud.iter().all(|v| v.is_finite() && v.abs() <= 4.0));
    }

    #[test]
    fn the_deesser_and_the_double_do_their_jobs() {
        // a bright 7 kHz tone is turned down by the de-esser, a 300 Hz tone is not
        let level = |hz: f32, deess: f32| {
            let mut h = VocalHalo::new(SR);
            for n in ["halo", "double", "presence", "air", "punch", "bloom"] {
                h.set_param(n, 0.0);
            }
            h.set_param("width", 0.5);
            h.set_param("deess", deess);
            let mut x = sine(hz, 0.8, 0.5);
            h.process(&mut x);
            rms(tail(&x))
        };
        assert!(level(7000.0, 1.0) < level(7000.0, 0.0) * 0.75);
        assert!((level(300.0, 1.0) / level(300.0, 0.0) - 1.0).abs() < 0.1);
        // the double makes left and right differ for a mono source
        let mut h = VocalHalo::new(SR);
        for n in ["halo", "presence", "air", "deess", "punch", "bloom"] {
            h.set_param(n, 0.0);
        }
        h.set_param("double", 1.0);
        let mut x = noise(0.6, 0.3);
        h.process(&mut x);
        let side = rms(&x.chunks(2).flat_map(|a| [a[0] - a[1], 0.0]).collect::<Vec<_>>());
        assert!(side > 0.02, "{side}");
    }
}

← Back to index