灵符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/fx.rs
//! Effects: SVF filter, delay, Schroeder reverb, compressor, 3-band EQ, drive.
use crate::dsp::TAU;

pub trait Effect: Send {
    fn process(&mut self, buf: &mut [f32]); // interleaved stereo, in place
    fn set_param(&mut self, name: &str, v: f32);
    fn name(&self) -> &'static str;
    /// Level (linear) of an external sidechain key for this block; None = no key.
    fn set_sidechain(&mut self, _level: Option<f32>) {}
    /// Where the transport is for this block: the project tempo and the beat of its first frame (the same beat block after block
    /// when the transport is stopped). Effects that work in musical time (the Articulator's gate and LFO) read it.
    fn set_transport(&mut self, _bpm: f64, _beat: f64) {}
    /// Trigger events for this block: (frame in the block, velocity 0..1). The Ducker starts its envelope on each one.
    fn set_triggers(&mut self, _events: &[(u32, f32)]) {}
    /// Live values for the effect's display (0..1 each), empty when it has none.
    fn view(&self) -> Vec<f32> {
        vec![]
    }
    /// Load a file the effect works from (a WAV for the growl's spectral source); false when it has none or cannot read it.
    fn load_source(&mut self, _path: &str) -> bool {
        false
    }
    /// The audio of the tracks routed to this track's modulator input for this block (interleaved stereo, same length as the block); None = none.
    fn set_aux(&mut self, _aux: Option<&[f32]>) {}
    /// Effect-specific edit commands (the Ringstorm's pedals): `cmd` with numeric `args` and some `text`; the reply is a list of numbers.
    fn command(&mut self, _cmd: &str, _args: &[f64], _text: &str) -> Vec<f64> {
        vec![]
    }
    /// Effect-specific text ("config" = the whole state as JSON for saving).
    fn info(&self, _what: &str) -> String {
        String::new()
    }
    /// The file loaded with `load_source`, empty for none / the built-in.
    fn source(&self) -> String {
        String::new()
    }
}

#[derive(Clone, Copy, PartialEq)]
pub enum FilterMode {
    Low,
    High,
    Band,
}

/// Chamberlin-style state-variable filter (2x-oversampled step for stability).
#[derive(Clone)]
pub struct Svf {
    lp: f32,
    bp: f32,
    pub mode: FilterMode,
}
impl Svf {
    pub fn new(mode: FilterMode) -> Self {
        Svf { lp: 0.0, bp: 0.0, mode }
    }
    #[inline]
    pub fn tick(&mut self, x: f32, cutoff_hz: f32, reso: f32, sr: f32) -> f32 {
        // 4 sub-steps per sample => coefficient computed at 4*sr; clamp keeps
        // the Chamberlin form stable at high cutoff / low damping.
        let f = (2.0 * (std::f32::consts::PI * cutoff_hz.clamp(20.0, sr * 0.45) / (4.0 * sr)).sin()).min(0.6);
        let q = 1.0 - reso.clamp(0.0, 0.95);
        let mut out = 0.0;
        for _ in 0..4 {
            let hp = x - self.lp - q * 2.0 * self.bp;
            self.bp += f * hp;
            self.lp += f * self.bp;
            out = match self.mode {
                FilterMode::Low => self.lp,
                FilterMode::High => hp,
                FilterMode::Band => self.bp,
            };
        }
        out
    }
}

pub struct Delay {
    buf: [Vec<f32>; 2],
    pos: usize,
    pub time: f32,
    pub feedback: f32,
    pub mix: f32,
    sr: f32,
}
impl Delay {
    pub fn new(sr: f32) -> Self {
        let n = (sr * 2.0) as usize;
        Delay { buf: [vec![0.0; n], vec![0.0; n]], pos: 0, time: 0.3, feedback: 0.4, mix: 0.3, sr }
    }
}
impl Effect for Delay {
    fn process(&mut self, b: &mut [f32]) {
        let n = self.buf[0].len();
        let d = ((self.time * self.sr) as usize).clamp(1, n - 1);
        for fr in b.chunks_exact_mut(2) {
            let rp = (self.pos + n - d) % n;
            for c in 0..2 {
                let wet = self.buf[c][rp];
                self.buf[c][self.pos] = fr[c] + wet * self.feedback;
                fr[c] += wet * self.mix;
            }
            self.pos = (self.pos + 1) % n;
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        match n {
            "time" => self.time = 0.02 + v * 1.5,
            "feedback" => self.feedback = v * 0.95,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Delay"
    }
}

struct Comb {
    buf: Vec<f32>,
    i: usize,
    lp: f32,
}
struct Allpass {
    buf: Vec<f32>,
    i: usize,
}
pub struct Reverb {
    combs: [Vec<Comb>; 2],
    aps: [Vec<Allpass>; 2],
    pub mix: f32,
    pub size: f32,
    pub damp: f32,
}
impl Reverb {
    pub fn new(sr: f32) -> Self {
        let k = sr / 44100.0;
        let ct = [1116usize, 1188, 1277, 1356, 1422, 1491];
        let at = [556usize, 441, 341];
        let mk = |off: usize| -> (Vec<Comb>, Vec<Allpass>) {
            (
                ct.iter().map(|&t| Comb { buf: vec![0.0; ((t + off) as f32 * k) as usize], i: 0, lp: 0.0 }).collect(),
                at.iter().map(|&t| Allpass { buf: vec![0.0; ((t + off) as f32 * k) as usize], i: 0 }).collect(),
            )
        };
        let (c0, a0) = mk(0);
        let (c1, a1) = mk(23);
        Reverb { combs: [c0, c1], aps: [a0, a1], mix: 0.25, size: 0.8, damp: 0.4 }
    }
}
impl Effect for Reverb {
    fn process(&mut self, b: &mut [f32]) {
        for fr in b.chunks_exact_mut(2) {
            let inp = (fr[0] + fr[1]) * 0.5 * 0.15;
            for c in 0..2 {
                let mut acc = 0.0;
                for cb in self.combs[c].iter_mut() {
                    let y = cb.buf[cb.i];
                    cb.lp = y * (1.0 - self.damp) + cb.lp * self.damp;
                    cb.buf[cb.i] = inp + cb.lp * self.size;
                    cb.i = (cb.i + 1) % cb.buf.len();
                    acc += y;
                }
                for ap in self.aps[c].iter_mut() {
                    let y = ap.buf[ap.i];
                    let z = acc + y * 0.5;
                    ap.buf[ap.i] = z;
                    acc = y - z * 0.5;
                    ap.i = (ap.i + 1) % ap.buf.len();
                }
                fr[c] += acc * self.mix;
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        match n {
            "mix" => self.mix = v,
            "size" => self.size = 0.5 + v * 0.48,
            "damp" => self.damp = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Reverb"
    }
}

pub struct Compressor {
    env: f32,
    pub threshold_db: f32,
    pub ratio: f32,
    pub attack: f32,
    pub release: f32,
    pub makeup_db: f32,
    sr: f32,
    sc: Option<f32>,
}
impl Compressor {
    pub fn new(sr: f32) -> Self {
        Compressor { env: 0.0, threshold_db: -18.0, ratio: 4.0, attack: 0.005, release: 0.12, makeup_db: 3.0, sr, sc: None }
    }
}
impl Effect for Compressor {
    fn process(&mut self, b: &mut [f32]) {
        let ka = (-1.0 / (self.attack * self.sr)).exp();
        let kr = (-1.0 / (self.release * self.sr)).exp();
        let mk = 10f32.powf(self.makeup_db / 20.0);
        for fr in b.chunks_exact_mut(2) {
            let lvl = self.sc.unwrap_or_else(|| fr[0].abs().max(fr[1].abs()));
            let k = if lvl > self.env { ka } else { kr };
            self.env = lvl + (self.env - lvl) * k;
            let db = 20.0 * self.env.max(1e-6).log10();
            let over = (db - self.threshold_db).max(0.0);
            let g = 10f32.powf(-over * (1.0 - 1.0 / self.ratio) / 20.0) * mk;
            fr[0] *= g;
            fr[1] *= g;
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        match n {
            "threshold" => self.threshold_db = -60.0 + v * 60.0,
            "ratio" => self.ratio = 1.0 + v * 19.0,
            "attack" => self.attack = (1.0 + v * 199.0) / 1000.0,
            "release" => self.release = (10.0 + v * 990.0) / 1000.0,
            "makeup" => self.makeup_db = -12.0 + v * 24.0,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Compressor"
    }
    fn set_sidechain(&mut self, level: Option<f32>) {
        self.sc = level;
    }
}

// ── Ducker: a sidechain-style volume dip triggered by the notes of another track (or by a sidechain audio key) ─────────────────────

/// The volume dips by `depth` dB the moment a trigger arrives, holds, and comes back along a curve. Triggers are the notes of the tracks routed
/// into it with a Duck route (sample-accurate, and only the notes the route lets through, such as the kick of a drum kit) or the rising edges
/// of an audio sidechain key. A new trigger in the middle of a recovery starts from where the volume is, so retriggers never jump.
pub struct Ducker {
    sr: f32,
    pub depth_db: f32,
    pub attack: f32,
    pub hold: f32,
    pub release: f32,
    pub curve: f32,
    pub thresh: f32,
    pub mix: f32,
    /// seconds since the last trigger
    t: f32,
    g_from: f32,
    g_min: f32,
    active: bool,
    events: Vec<(u32, f32)>,
    sc_over: bool,
    sc_edge: bool,
    gain: f32,
}

impl Ducker {
    pub fn new(sr: f32) -> Self {
        Ducker { sr, depth_db: 13.5, attack: 0.004, hold: 0.03, release: 0.26, curve: 1.6, thresh: 0.1, mix: 1.0, t: 0.0, g_from: 1.0, g_min: 1.0, active: false, events: vec![], sc_over: false, sc_edge: false, gain: 1.0 }
    }

    fn trigger(&mut self, vel: f32) {
        // a harder note ducks a little deeper
        let d = self.depth_db * (0.55 + 0.45 * vel.clamp(0.0, 1.0));
        self.g_from = self.gain;
        self.g_min = 10f32.powf(-d / 20.0).min(self.g_from);
        self.t = 0.0;
        self.active = true;
    }

    /// the gain `t` seconds after a trigger
    fn gain_at(&self, t: f32) -> f32 {
        if t < self.attack {
            let u = (t / self.attack.max(1e-4)).clamp(0.0, 1.0);
            let e = u * u * (3.0 - 2.0 * u);
            return self.g_from + (self.g_min - self.g_from) * e;
        }
        let t2 = t - self.attack;
        if t2 < self.hold {
            return self.g_min;
        }
        let u = ((t2 - self.hold) / self.release.max(1e-3)).clamp(0.0, 1.0);
        self.g_min + (1.0 - self.g_min) * u.powf(self.curve)
    }

    /// the whole envelope of one trigger from silence: (seconds, gain) points for the display
    pub fn shape(&self, n: usize) -> Vec<f32> {
        let span = self.attack + self.hold + self.release;
        let mut d = Ducker { gain: 1.0, g_from: 1.0, ..Ducker::new(self.sr) };
        d.depth_db = self.depth_db;
        d.attack = self.attack;
        d.hold = self.hold;
        d.release = self.release;
        d.curve = self.curve;
        d.g_min = 10f32.powf(-self.depth_db / 20.0);
        (0..n.max(2)).map(|i| d.gain_at(span * 1.15 * i as f32 / (n.max(2) - 1) as f32)).collect()
    }
}

impl Effect for Ducker {
    fn process(&mut self, b: &mut [f32]) {
        let frames = b.len() / 2;
        if self.events.is_empty() && !self.sc_edge && !self.active {
            self.gain = 1.0;
            return;
        }
        let mut events = std::mem::take(&mut self.events);
        events.sort_by_key(|e| e.0);
        let mut ei = 0;
        let dt = 1.0 / self.sr;
        if self.sc_edge {
            self.trigger(1.0);
            self.sc_edge = false;
        }
        for f in 0..frames {
            while ei < events.len() && events[ei].0 as usize <= f {
                self.trigger(events[ei].1);
                ei += 1;
            }
            let g = if self.active {
                let g = self.gain_at(self.t);
                self.t += dt;
                if self.t > self.attack + self.hold + self.release {
                    self.active = false;
                }
                g
            } else {
                1.0
            };
            self.gain = g;
            let gm = 1.0 + (g - 1.0) * self.mix;
            b[f * 2] *= gm;
            b[f * 2 + 1] *= gm;
        }
        events.clear();
        self.events = events;
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "depth" => self.depth_db = v * 30.0,
            "attack" => self.attack = (0.0005 + v * v * 0.04).max(0.0005),
            "hold" => self.hold = v * 0.2,
            "release" => self.release = 0.03 + v * v * 1.17,
            "curve" => self.curve = 0.4 + v * 3.6,
            "thresh" => self.thresh = 10f32.powf((-50.0 + v * 44.0) / 20.0),
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Ducker"
    }
    fn set_sidechain(&mut self, level: Option<f32>) {
        if let Some(l) = level {
            let over = l > self.thresh;
            if over && !self.sc_over {
                self.sc_edge = true;
            }
            self.sc_over = over;
        } else {
            self.sc_over = false;
        }
    }
    fn set_triggers(&mut self, events: &[(u32, f32)]) {
        self.events.clear();
        self.events.extend_from_slice(events);
    }
    fn view(&self) -> Vec<f32> {
        // the dip, drawn as 0..1 (1 = full volume)
        self.shape(64)
    }
}

/// 3-band EQ via one-pole crossovers at 250 Hz / 3.5 kHz.
pub struct Eq3 {
    lo: [f32; 2],
    hi: [f32; 2],
    pub low_db: f32,
    pub mid_db: f32,
    pub high_db: f32,
    a_lo: f32,
    a_hi: f32,
}
impl Eq3 {
    pub fn new(sr: f32) -> Self {
        let a = |f: f32| 1.0 - (-TAU * f / sr).exp();
        Eq3 { lo: [0.0; 2], hi: [0.0; 2], low_db: 0.0, mid_db: 0.0, high_db: 0.0, a_lo: a(250.0), a_hi: a(3500.0) }
    }
}
impl Effect for Eq3 {
    fn process(&mut self, b: &mut [f32]) {
        let g = |db: f32| 10f32.powf(db / 20.0);
        let (gl, gm, gh) = (g(self.low_db), g(self.mid_db), g(self.high_db));
        for fr in b.chunks_exact_mut(2) {
            for c in 0..2 {
                let x = fr[c];
                self.lo[c] += self.a_lo * (x - self.lo[c]);
                self.hi[c] += self.a_hi * (x - self.hi[c]);
                let low = self.lo[c];
                let high = x - self.hi[c];
                let mid = x - low - high;
                fr[c] = low * gl + mid * gm + high * gh;
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let db = (v - 0.5) * 24.0;
        match n {
            "low" => self.low_db = db,
            "mid" => self.mid_db = db,
            "high" => self.high_db = db,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "EQ"
    }
}

pub struct Drive {
    pub amount: f32,
    pub mix: f32,
}
impl Drive {
    pub fn new() -> Self {
        Drive { amount: 0.3, mix: 1.0 }
    }
}
impl Default for Drive {
    fn default() -> Self {
        Self::new()
    }
}
impl Effect for Drive {
    fn process(&mut self, b: &mut [f32]) {
        let g = 1.0 + self.amount * 20.0;
        for s in b.iter_mut() {
            *s = *s * (1.0 - self.mix) + (*s * g).tanh() / g.sqrt() * self.mix;
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        match n {
            "amount" => self.amount = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Drive"
    }
}

/// Two-voice modulated-delay chorus (ensemble strings, organs, clean guitars, pads).
pub struct Chorus {
    buf: [Vec<f32>; 2],
    pos: usize,
    ph: f32,
    rate: f32,
    depth: f32,
    mix: f32,
    sr: f32,
}
impl Chorus {
    pub fn new(sr: f32) -> Self {
        let n = (sr * 0.06) as usize + 8;
        Chorus { buf: [vec![0.0; n], vec![0.0; n]], pos: 0, ph: 0.0, rate: 0.8, depth: 0.5, mix: 0.5, sr }
    }
}
impl Effect for Chorus {
    fn process(&mut self, b: &mut [f32]) {
        let n = self.buf[0].len();
        let dt = self.rate / self.sr;
        for fr in b.chunks_exact_mut(2) {
            self.ph = (self.ph + dt).fract();
            for c in 0..2 {
                self.buf[c][self.pos] = fr[c];
                // the two channels use opposite LFO phases for stereo width
                let ph = if c == 0 { self.ph } else { (self.ph + 0.5).fract() };
                let lfo = (ph * TAU).sin();
                let d = (0.012 + lfo * self.depth * 0.006) * self.sr; // 12 ms +- 6 ms
                let rp = self.pos as f32 + n as f32 - d;
                let i0 = rp.floor();
                let f = rp - i0;
                let i0 = i0 as usize % n;
                let i1 = (i0 + 1) % n;
                let wet = self.buf[c][i0] * (1.0 - f) + self.buf[c][i1] * f;
                fr[c] = fr[c] * (1.0 - self.mix * 0.5) + wet * self.mix * 0.8;
            }
            self.pos = (self.pos + 1) % n;
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        match n {
            "rate" => self.rate = 0.1 + v * 4.0,
            "depth" => self.depth = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Chorus"
    }
}

pub const EFFECT_NAMES: [&str; 22] = ["EQ", "Compressor", "Drive", "Delay", "Reverb", "Chorus", "Vox Filter", "Vox Reverb", "Vocal Halo", "Rainbow Hypergrowl", "Ducker", "Vocoder", "Ringstorm", "OTT", "Split Drive", "Comb", "Phaser", "Flanger", "Freq Shifter", "Imager", "Transient", "Articulator"];

/// Parameter names + default normalised values shown in the UI.
pub fn defaults(name: &str) -> &'static [(&'static str, f32)] {
    match name {
        "EQ" => &[("low", 0.5), ("mid", 0.5), ("high", 0.5)],
        "Compressor" => &[("threshold", 0.7), ("ratio", 0.2), ("attack", 0.02), ("release", 0.11), ("makeup", 0.625)],
        "Drive" => &[("amount", 0.3), ("mix", 1.0)],
        "Delay" => &[("time", 0.2), ("feedback", 0.4), ("mix", 0.3)],
        "Reverb" => &[("mix", 0.25), ("size", 0.6), ("damp", 0.4)],
        "Chorus" => &[("rate", 0.25), ("depth", 0.5), ("mix", 0.5)],
        "Vox Filter" => &[("hp", 0.4), ("slope", 0.5), ("lp", 1.0), ("res", 0.1), ("tilt", 0.5), ("dyn", 0.3), ("mix", 1.0)],
        "Vox Reverb" => &[("mix", 0.25), ("decay", 0.4), ("predelay", 0.1), ("size", 0.5), ("damp", 0.5), ("lowcut", 0.45), ("highcut", 0.75), ("duck", 0.4), ("width", 0.8), ("early", 0.4), ("mod", 0.3)],
        "Vocal Halo" => &[("width", 0.55), ("halo", 0.4), ("double", 0.3), ("presence", 0.35), ("air", 0.3), ("deess", 0.3), ("punch", 0.25), ("bloom", 0.2), ("focus", 0.5), ("mix", 1.0)],
        "Rainbow Hypergrowl" => &super::hypergrowl::PARAMS,
        "Vocoder" => &super::vocoder::PARAMS,
        "Ringstorm" => &super::guitar::PARAMS,
        "OTT" => &super::bassfx::OTT_PARAMS,
        "Split Drive" => &super::bassfx::SPLIT_PARAMS,
        "Comb" => &super::bassfx::COMB_PARAMS,
        "Phaser" => &super::bassfx::PHASER_PARAMS,
        "Flanger" => &super::bassfx::FLANGER_PARAMS,
        "Freq Shifter" => &super::bassfx::FSHIFT_PARAMS,
        "Imager" => &super::bassfx::IMAGER_PARAMS,
        "Transient" => &super::bassfx::TRANSIENT_PARAMS,
        "Articulator" => &super::articulator::PARAMS,
        "Ducker" => &[("depth", 0.45), ("attack", 0.08), ("hold", 0.15), ("release", 0.2), ("curve", 0.35), ("thresh", 0.5), ("mix", 1.0)],
        _ => &[],
    }
}

pub fn make(name: &str, sr: f32) -> Option<Box<dyn Effect>> {
    let mut e: Box<dyn Effect> = match name {
        "Delay" => Box::new(Delay::new(sr)),
        "Reverb" => Box::new(Reverb::new(sr)),
        "Compressor" => Box::new(Compressor::new(sr)),
        "EQ" => Box::new(Eq3::new(sr)),
        "Drive" => Box::new(Drive::new()),
        "Chorus" => Box::new(Chorus::new(sr)),
        "Vox Filter" => Box::new(super::vox::VoxFilter::new(sr)),
        "Vox Reverb" => Box::new(super::vox::VoxReverb::new(sr)),
        "Vocal Halo" => Box::new(super::vox::VocalHalo::new(sr)),
        "Rainbow Hypergrowl" => Box::new(super::hypergrowl::Hypergrowl::new(sr)),
        "Ducker" => Box::new(Ducker::new(sr)),
        "Vocoder" => Box::new(super::vocoder::Vocoder::new(sr)),
        "Ringstorm" => Box::new(super::guitar::Rig::new(sr)),
        "OTT" => Box::new(super::bassfx::Ott::new(sr)),
        "Split Drive" => Box::new(super::bassfx::SplitDrive::new(sr)),
        "Comb" => Box::new(super::bassfx::Comb::new(sr)),
        "Phaser" => Box::new(super::bassfx::Phaser::new(sr)),
        "Flanger" => Box::new(super::bassfx::Flanger::new(sr)),
        "Freq Shifter" => Box::new(super::bassfx::FreqShifter::new(sr)),
        "Imager" => Box::new(super::bassfx::Imager::new(sr)),
        "Transient" => Box::new(super::bassfx::Transient::new(sr)),
        "Articulator" => Box::new(super::articulator::Articulator::new(sr)),
        _ => return None,
    };
    for (n, v) in defaults(name) {
        e.set_param(n, *v);
    }
    Some(e)
}

/// Ready-made settings of an effect: (name, overrides on top of the defaults). Only some effects have any.
pub fn presets(name: &str) -> &'static [(&'static str, &'static [(&'static str, f32)])] {
    match name {
        "Rainbow Hypergrowl" => &super::hypergrowl::PRESETS,
        "Vocoder" => &super::vocoder::PRESETS,
        "Ringstorm" => &super::guitar::PRESETS,
        "OTT" => &super::bassfx::OTT_PRESETS,
        "Split Drive" => &super::bassfx::SPLIT_PRESETS,
        "Comb" => &super::bassfx::COMB_PRESETS,
        "Phaser" => &super::bassfx::PHASER_PRESETS,
        "Flanger" => &super::bassfx::FLANGER_PRESETS,
        "Freq Shifter" => &super::bassfx::FSHIFT_PRESETS,
        "Imager" => &super::bassfx::IMAGER_PRESETS,
        "Transient" => &super::bassfx::TRANSIENT_PRESETS,
        "Articulator" => &super::articulator::PRESETS,
        "Ducker" => &[
            ("Kick duck", &[("depth", 0.5), ("attack", 0.06), ("hold", 0.12), ("release", 0.2), ("curve", 0.35)]),
            ("Hard pump", &[("depth", 0.85), ("attack", 0.04), ("hold", 0.2), ("release", 0.32), ("curve", 0.2)]),
            ("Gentle breath", &[("depth", 0.25), ("attack", 0.2), ("hold", 0.0), ("release", 0.45), ("curve", 0.5)]),
            ("Tight gate", &[("depth", 1.0), ("attack", 0.02), ("hold", 0.3), ("release", 0.12), ("curve", 0.7)]),
        ],
        _ => &[],
    }
}

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

    const SR: f32 = 48000.0;

    fn run(d: &mut Ducker, frames: usize, events: &[(u32, f32)]) -> Vec<f32> {
        let mut b = vec![1.0f32; frames * 2];
        d.set_triggers(events);
        d.process(&mut b);
        b.chunks(2).map(|f| f[0]).collect()
    }

    #[test]
    fn without_triggers_the_signal_is_untouched() {
        let mut d = Ducker::new(SR);
        let mut b: Vec<f32> = (0..2000).map(|i| (i as f32 * 0.01).sin() * 0.5).collect();
        let orig = b.clone();
        d.process(&mut b);
        assert_eq!(b, orig);
    }

    #[test]
    fn a_trigger_dips_holds_and_comes_back() {
        let mut d = Ducker::new(SR);
        for (n, v) in defaults("Ducker") {
            d.set_param(n, *v);
        }
        d.set_param("depth", 0.5); // 15 dB
        let g = run(&mut d, SR as usize, &[(1000, 1.0)]);
        assert!((g[999] - 1.0).abs() < 1e-6, "nothing before the trigger");
        let (a, h, r) = (d.attack, d.hold, d.release);
        let at = |t: f32| g[1000 + (t * SR) as usize];
        let floor = 10f32.powf(-15.0 / 20.0);
        assert!((at(a + 0.002) - floor).abs() < 0.01, "the dip reaches 15 dB: {} vs {}", at(a + 0.002), floor);
        assert!((at(a + h * 0.5) - floor).abs() < 0.01, "and holds");
        assert!(at(a + h + r * 0.5) > floor + 0.1 && at(a + h + r * 0.5) < 0.95, "mid-recovery is between");
        assert!(at(a + h + r + 0.01) > 0.999, "and it is fully back");
        // the recovery only rises
        let mut last = 0.0;
        for k in 0..200 {
            let v = at(a + h + r * k as f32 / 200.0);
            assert!(v >= last - 1e-5);
            last = v;
        }
    }

    #[test]
    fn a_harder_note_ducks_deeper_and_depth_runs_to_30_db() {
        let dip = |vel: f32, depth: f32| {
            let mut d = Ducker::new(SR);
            d.set_param("depth", depth);
            let g = run(&mut d, 4800, &[(10, vel)]);
            g.iter().cloned().fold(1.0f32, f32::min)
        };
        assert!(dip(1.0, 0.5) < dip(0.3, 0.5));
        let deepest = dip(1.0, 1.0);
        assert!((deepest - 10f32.powf(-30.0 / 20.0)).abs() < 0.005, "{deepest}");
    }

    #[test]
    fn a_retrigger_during_the_recovery_starts_from_where_the_volume_is() {
        let mut d = Ducker::new(SR);
        let g = run(&mut d, 20000, &[(0, 1.0), (7000, 1.0)]);
        for w in g.windows(2) {
            assert!((w[1] - w[0]).abs() < 0.02, "a jump of {} in the volume", (w[1] - w[0]).abs());
        }
        // the second dip starts from a partly recovered volume, not from 1.0
        assert!(g[7000] < 0.95);
    }

    #[test]
    fn the_dip_carries_over_into_the_next_block() {
        let mut one = Ducker::new(SR);
        let whole = run(&mut one, 6000, &[(100, 1.0)]);
        let mut two = Ducker::new(SR);
        let mut parts = run(&mut two, 2000, &[(100, 1.0)]);
        parts.extend(run(&mut two, 4000, &[]));
        for (a, b) in whole.iter().zip(&parts) {
            assert!((a - b).abs() < 1e-5);
        }
    }

    #[test]
    fn an_audio_key_triggers_on_the_rising_edge_only() {
        let mut d = Ducker::new(SR);
        d.set_param("thresh", 0.5);
        d.set_sidechain(Some(0.5));
        let g1 = run(&mut d, 3000, &[]);
        assert!(g1[2000] < 0.9, "the key above the threshold ducks");
        // still above: no second trigger, the volume keeps recovering
        d.set_sidechain(Some(0.5));
        let g2 = run(&mut d, 20000, &[]);
        assert!(g2[19999] > 0.99);
        // below, then above again: ducks again
        d.set_sidechain(Some(0.0));
        run(&mut d, 100, &[]);
        d.set_sidechain(Some(0.5));
        let g3 = run(&mut d, 3000, &[]);
        assert!(g3[2000] < 0.9);
    }

    #[test]
    fn the_shape_for_the_display_matches_the_envelope() {
        let mut d = Ducker::new(SR);
        d.set_param("depth", 0.4);
        let s = d.shape(64);
        assert_eq!(s.len(), 64);
        assert!(s[0] > 0.99 && s.iter().cloned().fold(1.0, f32::min) < 0.3 && s[63] > 0.999);
        assert_eq!(d.view().len(), 64);
        assert_eq!(super::defaults("Ducker").len(), 7);
        assert!(super::make("Ducker", SR).is_some());
    }
}

← Back to index