灵符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/articulator.rs
//! **Articulator** - a filter, a gate and an articulation shaper in one box.
//!
//! * the **filter**: a state-variable filter (low / high / band / notch / 24 dB low-pass) with drive in front of it. Its cutoff is moved by
//!   an envelope follower (auto-wah up or down), a tempo-synced LFO, and the gate steps themselves (each step "plucks" the filter open).
//! * the **gate**: a 16-step pattern (trance gate, offbeats, gallop, stutter ...) read in steps of 1/4 .. 1/32 or triplets, with swing,
//!   per-step accents, a length (staccato to legato) and a smoothing time so it never clicks. It follows the transport, so it stays
//!   locked to the song; stopped, it keeps its own time at the project tempo.
//! * the **articulation**: every step opens with a short "punch" (a quick gain and brightness pop that decays) - the pick, the bow,
//!   the tongue - so pads and chords can be played staccato, pumped or plucked without touching the notes.
//!
//! Parameters are 0..1 like every effect's. The effect-level dry/wet that every effect has (`fx<i>.wet`) is the mix, so there is no knob
//! for it here. Output is finite for any input and settings (tests below).
use super::bassfx::Tsvf;
use super::fx::Effect;
use crate::dsp::TAU;

pub const PARAMS: [(&str, f32); 18] = [
    ("mode", 0.0),
    ("cutoff", 0.62),
    ("res", 0.3),
    ("drive", 0.12),
    ("env", 0.5),
    ("follow", 0.4),
    ("lfo", 0.0),
    ("rate", 0.55),
    ("shape", 0.0),
    ("gate", 0.8),
    ("pattern", 0.2727),
    ("steps", 0.75),
    ("length", 0.6),
    ("smooth", 0.25),
    ("swing", 0.0),
    ("accent", 0.5),
    ("punch", 0.3),
    ("pluck", 0.0),
];

/// the 16-step patterns: 0 = closed, 1 = open at full level, in between = an accented step
pub const PATTERNS: [(&str, [f32; 16]); 12] = [
    ("Chop 16ths", [1.0; 16]),
    ("Eighths", [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]),
    ("Offbeat pump", [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
    ("Trance 3-3-2", [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0]),
    ("Gallop", [1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0]),
    ("Stutter", [1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0]),
    ("Syncopated", [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]),
    ("Accents", [1.0, 0.45, 0.7, 0.45, 1.0, 0.45, 0.7, 0.45, 1.0, 0.45, 0.7, 0.45, 1.0, 0.45, 0.7, 0.45]),
    ("Triplet feel", [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]),
    ("Build", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0]),
    ("Sparse", [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]),
    ("Half time", [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
];

/// LFO / step lengths in beats: 1/4, 1/8, 1/8 triplet, 1/16, 1/32
const STEP_BEATS: [f64; 5] = [1.0, 0.5, 1.0 / 3.0, 0.25, 0.125];
/// LFO periods in beats: 4 bars, 2 bars, 1 bar, 1/2 bar, 1/4, 1/8, 1/16, 1/32 note
const LFO_BEATS: [f64; 8] = [16.0, 8.0, 4.0, 2.0, 1.0, 0.5, 0.25, 0.125];

#[inline]
fn fin(x: f32) -> f32 {
    if x.is_finite() {
        x
    } else {
        0.0
    }
}

pub struct Articulator {
    sr: f32,
    // parameters (already mapped)
    mode: usize,
    cutoff: f32,
    res: f32,
    drive: f32,
    env_amt: f32,
    follow: f32,
    lfo_amt: f32,
    lfo_idx: usize,
    shape: usize,
    gate: f32,
    pattern: usize,
    steps: usize,
    length: f32,
    smooth: f32,
    swing: f32,
    accent: f32,
    punch: f32,
    pluck: f32,
    // state
    f1: [Tsvf; 2],
    f2: [Tsvf; 2],
    env: f32,
    gate_env: f32,
    pop: f32,
    pluck_env: f32,
    last_step: i64,
    beat: f64,
    last_base: f64,
    bpm: f64,
    rng: u32,
    sh: f32,
    last_lfo_cycle: i64,
    // what the display shows
    shown: [f32; 5],
    shown_step: usize,
}

impl Articulator {
    pub fn new(sr: f32) -> Articulator {
        Articulator {
            sr,
            mode: 0,
            cutoff: 0.62,
            res: 0.3,
            drive: 0.12,
            env_amt: 0.0,
            follow: 0.4,
            lfo_amt: 0.0,
            lfo_idx: 4,
            shape: 0,
            gate: 0.8,
            pattern: 3,
            steps: 3,
            length: 0.6,
            smooth: 0.25,
            swing: 0.0,
            accent: 0.5,
            punch: 0.3,
            pluck: 0.0,
            f1: [Tsvf::default(); 2],
            f2: [Tsvf::default(); 2],
            env: 0.0,
            gate_env: 1.0,
            pop: 0.0,
            pluck_env: 0.0,
            last_step: i64::MIN,
            beat: 0.0,
            last_base: f64::NAN,
            bpm: 120.0,
            rng: 0x1234_5678,
            sh: 0.0,
            last_lfo_cycle: i64::MIN,
            shown: [0.0; 5],
            shown_step: 0,
        }
    }

    fn rand(&mut self) -> f32 {
        self.rng ^= self.rng << 13;
        self.rng ^= self.rng >> 17;
        self.rng ^= self.rng << 5;
        (self.rng as f32 / u32::MAX as f32) * 2.0 - 1.0
    }

    /// the LFO at a phase 0..1
    fn lfo(&mut self, ph: f64, cycle: i64) -> f32 {
        let p = ph as f32;
        match self.shape {
            0 => (p * TAU).sin(),
            1 => 1.0 - 4.0 * (p - 0.5).abs(),
            2 => 1.0 - 2.0 * p,
            3 => {
                if p < 0.5 {
                    1.0
                } else {
                    -1.0
                }
            }
            _ => {
                if cycle != self.last_lfo_cycle {
                    self.last_lfo_cycle = cycle;
                    self.sh = self.rand();
                }
                self.sh
            }
        }
    }
}

impl Effect for Articulator {
    fn set_transport(&mut self, bpm: f64, beat: f64) {
        if bpm.is_finite() && bpm > 20.0 {
            self.bpm = bpm;
        }
        if !beat.is_finite() {
            return;
        }
        // stopped (the position does not move between blocks): keep time on our own so the effect still works on a live input
        let moved = self.last_base.is_nan() || (beat - self.last_base).abs() > 1e-9;
        self.last_base = beat;
        if moved {
            self.beat = beat;
        }
    }

    fn process(&mut self, buf: &mut [f32]) {
        let sr = self.sr;
        let bps = self.bpm / 60.0 / sr as f64;
        let step_beats = STEP_BEATS[self.steps.min(4)];
        let pat = &PATTERNS[self.pattern.min(PATTERNS.len() - 1)].1;
        let len = 0.08 + self.length * 0.92;
        // gate smoothing: opening a little quicker than closing
        let t_open = 0.0008 + self.smooth * 0.03;
        let t_close = 0.0015 + self.smooth * 0.09;
        let k_open = (-1.0 / (t_open * sr)).exp();
        let k_close = (-1.0 / (t_close * sr)).exp();
        let k_pop = (-1.0 / (0.06 * sr)).exp();
        let k_pluck = (-1.0 / ((0.04 + 0.4 * len) * sr)).exp();
        let t_env = 0.002 + (1.0 - self.follow) * 0.25;
        let k_env = (-1.0 / (t_env * sr)).exp();
        let outg = 1.0;
        let drive = 1.0 + self.drive * 9.0;
        let dcomp = 1.0 / drive.tanh().max(0.2);
        let q = 1.0 - self.res * 0.97;
        let k = (2.0 * q).max(0.04);
        let lfo_period = LFO_BEATS[self.lfo_idx.min(7)];
        for fr in buf.chunks_exact_mut(2) {
            let b = self.beat;
            self.beat += bps;
            // which step, and where in it (with swing: the odd steps start later)
            let sf = b / step_beats;
            let pair = sf / 2.0;
            let pk = pair.floor();
            let pf = pair - pk;
            let boundary = 0.5 + 0.25 * self.swing as f64;
            let (step, frac) = if pf < boundary { ((pk as i64) * 2, pf / boundary) } else { ((pk as i64) * 2 + 1, (pf - boundary) / (1.0 - boundary)) };
            let lvl = pat[(step.rem_euclid(16)) as usize];
            let open = lvl > 0.0 && (frac as f32) < len;
            // a new step that opens: the pick (punch + pluck) fires
            if step != self.last_step {
                self.last_step = step;
                if lvl > 0.0 {
                    self.pop = lvl;
                    self.pluck_env = lvl;
                }
                self.shown_step = (step.rem_euclid(16)) as usize;
            }
            let accent_gain = (1.0 - self.accent) + self.accent * lvl;
            let target = if open { accent_gain } else { 0.0 };
            let kk = if target > self.gate_env { k_open } else { k_close };
            self.gate_env = target + (self.gate_env - target) * kk;
            // the gate's depth: 0 = open all the time
            let g = 1.0 - self.gate * (1.0 - self.gate_env.clamp(0.0, 1.0));
            self.pop *= k_pop;
            self.pluck_env *= k_pluck;
            let punch_gain = 1.0 + self.punch * 0.9 * self.pop;

            let l = fin(fr[0]);
            let r = fin(fr[1]);
            // envelope follower of the input
            let lvl_in = l.abs().max(r.abs());
            self.env = lvl_in + (self.env - lvl_in) * k_env;
            // cutoff in octaves around the knob: 30 Hz .. ~21 kHz
            let base = 30.0 * (2.0f32).powf(self.cutoff * 9.5);
            let cyc = (b / lfo_period).floor();
            let ph = b / lfo_period - cyc;
            let lfo = self.lfo(ph, cyc as i64);
            let mut oct = self.env_amt * (self.env * 2.5).min(1.0) * 5.0 + self.lfo_amt * lfo * 3.0 + self.pluck * self.pluck_env * 4.0 + self.punch * self.pop * 1.5;
            if !oct.is_finite() {
                oct = 0.0;
            }
            let fc = (base * (2.0f32).powf(oct)).clamp(20.0, sr * 0.45);
            let gg = (std::f32::consts::PI * fc / sr).tan();
            let mut out = [0.0f32; 2];
            for (c, x0) in [l, r].into_iter().enumerate() {
                let x = (x0 * drive).tanh() * dcomp;
                let (lo, bp, hi) = self.f1[c].run(x, gg, k);
                let y = match self.mode {
                    0 => lo,
                    1 => hi,
                    2 => bp * (k.max(0.3)),
                    3 => lo + hi,
                    _ => {
                        let (lo2, _, _) = self.f2[c].run(lo, gg, k);
                        lo2
                    }
                };
                out[c] = fin(y) * g * punch_gain * outg;
            }
            fr[0] = fin(out[0]);
            fr[1] = fin(out[1]);
            self.shown = [fc.max(20.0).log2().min(15.0) / 15.0, self.gate_env.clamp(0.0, 1.0), (lfo * 0.5 + 0.5).clamp(0.0, 1.0), (self.env * 2.0).min(1.0), self.pop.min(1.0)];
        }
    }

    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "mode" => self.mode = ((v * 4.0).round() as usize).min(4),
            "cutoff" => self.cutoff = v,
            "res" => self.res = v,
            "drive" => self.drive = v,
            "env" => self.env_amt = (v - 0.5) * 2.0,
            "follow" => self.follow = v,
            "lfo" => self.lfo_amt = v,
            "rate" => self.lfo_idx = ((v * 7.0).round() as usize).min(7),
            "shape" => self.shape = ((v * 4.0).round() as usize).min(4),
            "gate" => self.gate = v,
            "pattern" => self.pattern = ((v * (PATTERNS.len() - 1) as f32).round() as usize).min(PATTERNS.len() - 1),
            "steps" => self.steps = ((v * 4.0).round() as usize).min(4),
            "length" => self.length = v,
            "smooth" => self.smooth = v,
            "swing" => self.swing = v,
            "accent" => self.accent = v,
            "punch" => self.punch = v,
            "pluck" => self.pluck = v,
            _ => {}
        }
    }

    fn name(&self) -> &'static str {
        "Articulator"
    }

    /// [cutoff 0..1 (log), gate open 0..1, LFO 0..1, input envelope 0..1, punch 0..1, current step 0..15 / 16, then the 16 steps of the pattern]
    fn view(&self) -> Vec<f32> {
        let mut v = self.shown.to_vec();
        v.push(self.shown_step as f32 / 16.0);
        v.extend(PATTERNS[self.pattern.min(PATTERNS.len() - 1)].1.iter().copied());
        v
    }

    fn info(&self, what: &str) -> String {
        match what {
            "pattern" => PATTERNS[self.pattern.min(PATTERNS.len() - 1)].0.to_string(),
            "patterns" => PATTERNS.iter().map(|p| p.0).collect::<Vec<_>>().join("|"),
            _ => String::new(),
        }
    }
}

type Pre = (&'static str, &'static [(&'static str, f32)]);
pub const PRESETS: [Pre; 8] = [
    ("Trance gate", &[("mode", 0.0), ("cutoff", 0.8), ("gate", 0.95), ("pattern", 0.2727), ("steps", 0.75), ("length", 0.55), ("smooth", 0.15), ("punch", 0.4)]),
    ("Pluck every step", &[("mode", 0.0), ("cutoff", 0.35), ("res", 0.45), ("gate", 0.0), ("pattern", 0.0), ("steps", 0.25), ("pluck", 0.8), ("punch", 0.2)]),
    ("Offbeat pump", &[("mode", 0.0), ("cutoff", 0.9), ("gate", 0.85), ("pattern", 0.1818), ("steps", 0.75), ("length", 0.45), ("smooth", 0.3), ("punch", 0.5)]),
    ("Auto-wah", &[("mode", 0.5), ("cutoff", 0.4), ("res", 0.55), ("env", 0.9), ("follow", 0.55), ("gate", 0.0), ("drive", 0.25)]),
    ("Slow sweep", &[("mode", 0.25), ("cutoff", 0.55), ("res", 0.4), ("lfo", 0.8), ("rate", 0.0), ("shape", 0.25), ("gate", 0.0)]),
    ("Staccato pads", &[("mode", 0.0), ("cutoff", 0.9), ("gate", 1.0), ("pattern", 0.0), ("steps", 0.25), ("length", 0.28), ("smooth", 0.2), ("punch", 0.5)]),
    ("Gallop gate", &[("mode", 0.0), ("cutoff", 0.85), ("gate", 0.9), ("pattern", 0.3636), ("steps", 0.75), ("length", 0.6), ("swing", 0.15)]),
    ("Telephone", &[("mode", 0.5), ("cutoff", 0.58), ("res", 0.5), ("drive", 0.6), ("gate", 0.0)]),
];

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

    const SR: f32 = 48000.0;

    fn run(a: &mut Articulator, input: impl Fn(usize) -> f32, frames: usize, beat0: f64) -> Vec<f32> {
        let mut buf = vec![0.0f32; frames * 2];
        for i in 0..frames {
            let x = input(i);
            buf[i * 2] = x;
            buf[i * 2 + 1] = x;
        }
        a.set_transport(120.0, beat0);
        a.process(&mut buf);
        buf
    }

    fn set(a: &mut Articulator, kv: &[(&str, f32)]) {
        for (n, v) in kv {
            a.set_param(n, *v);
        }
    }

    #[test]
    fn finite_and_bounded_for_any_setting() {
        for seed in 0..40u32 {
            let mut a = Articulator::new(SR);
            let mut x = seed.wrapping_mul(2654435761) | 1;
            for (n, _) in PARAMS {
                x ^= x << 13;
                x ^= x >> 17;
                x ^= x << 5;
                a.set_param(n, (x % 1001) as f32 / 1000.0);
            }
            let y = run(&mut a, |i| ((i as f32 * 0.05).sin() * 0.9) + if i % 997 == 0 { 3.0 } else { 0.0 }, 20000, seed as f64 * 0.37);
            assert!(y.iter().all(|v| v.is_finite() && v.abs() < 60.0), "seed {seed}");
        }
        let mut a = Articulator::new(SR);
        let y = run(&mut a, |_| f32::NAN, 2000, 0.0);
        assert!(y.iter().all(|v| v.is_finite()));
    }

    #[test]
    fn the_gate_follows_its_pattern() {
        // quarter-note steps at 120 bpm = 24000 samples each; "Half time" opens step 0 and 8 of 16 only
        let mut a = Articulator::new(SR);
        set(&mut a, &[("mode", 0.25), ("cutoff", 0.0), ("gate", 1.0), ("pattern", 1.0), ("steps", 0.0), ("length", 1.0), ("smooth", 0.0), ("drive", 0.0), ("punch", 0.0), ("res", 0.0)]);
        // a high-pass at 30 Hz lets a 1 kHz tone through; on a closed step the gate shuts it
        let tone = |i: usize| (i as f32 * 2.0 * std::f32::consts::PI * 1000.0 / SR).sin() * 0.5;
        let y = run(&mut a, tone, 24000 * 3, 0.0);
        let rms = |lo: usize, hi: usize| (y[lo * 2..hi * 2].iter().map(|v| v * v).sum::<f32>() / (2 * (hi - lo)) as f32).sqrt();
        assert!(rms(3000, 20000) > 0.2, "step 0 is open: {}", rms(3000, 20000));
        assert!(rms(30000, 46000) < 0.01, "step 1 is closed: {}", rms(30000, 46000));
    }

    #[test]
    fn the_filter_removes_the_highs() {
        let mut a = Articulator::new(SR);
        set(&mut a, &[("mode", 0.0), ("cutoff", 0.25), ("gate", 0.0), ("drive", 0.0), ("punch", 0.0), ("res", 0.0)]);
        let hi = |i: usize| (i as f32 * 2.0 * std::f32::consts::PI * 8000.0 / SR).sin() * 0.5;
        let lo = |i: usize| (i as f32 * 2.0 * std::f32::consts::PI * 80.0 / SR).sin() * 0.5;
        let yh = run(&mut a, hi, 9600, 0.0);
        let mut b = Articulator::new(SR);
        set(&mut b, &[("mode", 0.0), ("cutoff", 0.25), ("gate", 0.0), ("drive", 0.0), ("punch", 0.0), ("res", 0.0)]);
        let yl = run(&mut b, lo, 9600, 0.0);
        let rms = |y: &[f32]| (y[9600..].iter().map(|v| v * v).sum::<f32>() / (y.len() - 9600) as f32).sqrt();
        assert!(rms(&yh) < rms(&yl) * 0.2, "8 kHz {} vs 80 Hz {}", rms(&yh), rms(&yl));
    }

    #[test]
    fn it_keeps_time_when_stopped() {
        let mut a = Articulator::new(SR);
        set(&mut a, &[("gate", 1.0), ("pattern", 0.0909), ("steps", 0.5), ("length", 0.5)]);
        let tone = |i: usize| (i as f32 * 0.07).sin() * 0.5;
        // the same beat position block after block (a stopped transport): the gate still opens and closes
        let mut energy = vec![];
        for _ in 0..8 {
            let y = run(&mut a, tone, 6000, 4.0);
            energy.push(y.iter().map(|v| v * v).sum::<f32>());
        }
        let (mn, mx) = (energy.iter().cloned().fold(f32::MAX, f32::min), energy.iter().cloned().fold(0.0f32, f32::max));
        assert!(mx > mn * 3.0 || mn < 1e-3, "{energy:?}");
    }

    #[test]
    fn presets_and_names_are_valid() {
        let names: Vec<&str> = PARAMS.iter().map(|p| p.0).collect();
        for (pn, kv) in PRESETS {
            for (n, v) in kv {
                assert!(names.contains(n), "{pn}: {n}");
                assert!((0.0..=1.0).contains(v));
            }
        }
    }

    #[test]
    fn the_slot_blends_dry_and_wet() {
        use crate::model::EffectSlot;
        // a gate that is shut on the second step: wet 1 = silence, wet 0.5 = half the input, wet 0 = the input
        let mut slot = EffectSlot::new("Articulator", SR).unwrap();
        for (n, v) in [("mode", 0.25f32), ("cutoff", 0.0), ("gate", 1.0), ("pattern", 1.0), ("steps", 0.0), ("length", 1.0), ("smooth", 0.0), ("drive", 0.0), ("punch", 0.0), ("res", 0.0)] {
            let i = slot.params.iter().position(|p| p.0 == n).unwrap();
            slot.set(i, v);
        }
        let mk = || (0..48000usize * 2).map(|i| ((i / 2) as f32 * 0.14).sin() * 0.5).collect::<Vec<f32>>();
        let rms = |y: &[f32]| (y[60000..90000].iter().map(|v| v * v).sum::<f32>() / 30000.0).sqrt();
        let input = rms(&mk());
        slot.fx.set_transport(120.0, 0.0);
        let mut a = mk();
        slot.wet = 1.0;
        slot.process(&mut a);
        assert!(rms(&a) < 0.01, "wet 1: {}", rms(&a));
        let mut slot2 = EffectSlot::new("Articulator", SR).unwrap();
        for (n, v) in [("mode", 0.25f32), ("cutoff", 0.0), ("gate", 1.0), ("pattern", 1.0), ("steps", 0.0), ("length", 1.0), ("smooth", 0.0), ("drive", 0.0), ("punch", 0.0), ("res", 0.0)] {
            let i = slot2.params.iter().position(|p| p.0 == n).unwrap();
            slot2.set(i, v);
        }
        slot2.fx.set_transport(120.0, 0.0);
        slot2.wet = 0.5;
        let mut b = mk();
        slot2.process(&mut b);
        assert!((rms(&b) - input * 0.5).abs() < input * 0.05, "wet 0.5: {} of {}", rms(&b), input);
        let mut slot3 = EffectSlot::new("Articulator", SR).unwrap();
        slot3.wet = 0.0;
        let mut c = mk();
        slot3.process(&mut c);
        assert_eq!(c, mk());
    }
}

← Back to index