灵符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/bassfx.rs
//! Sound-design effects for modern bass music (and anything else):
//!
//! * **OTT** - three-band upward + downward compression (88 Hz / 2.5 kHz Linkwitz-Riley crossovers): quiet detail comes up, loud
//!   peaks go down, every band glued to the same level. The "everything speaks at once" sheen of a modern growl.
//! * **Split Drive** - multiband distortion: the lows below the split stay clean (and optionally mono) while the mids and highs are
//!   driven (tube / fold / clip / crush), so a bass gets dirt without losing its sub.
//! * **Comb** - a stereo feedback comb tuned to a note: positive feedback rings at the note and its harmonics (metallic), negative at
//!   the odd harmonics an octave up (hollow); an LFO sweeps it. The spectral-comb bass sound.
//! * **Phaser** - 4 to 12 modulated all-pass stages with feedback.
//! * **Flanger** - a short modulated delay with feedback (jet sweeps, metallic swirls).
//! * **Freq Shifter** - a Hilbert-transform frequency shifter: every partial moves by the same number of Hz (inharmonic, metallic,
//!   the "talking robot" shimmer); the right channel can shift the other way.
//! * **Imager** - stereo width by mid / side, with everything below a frequency folded to mono (a bass's sub stays centred).
//! * **Transient** - transient shaper: more or less attack and sustain, from the difference of a fast and a slow envelope.
//!
//! All parameters are 0..1 like every other effect; every effect is finite for any input and settings (tests below).
use super::fx::Effect;
use crate::dsp::TAU;

// ── shared parts ────────────────────────────────────────────────────────────────

/// a TPT state-variable filter (Zavalishin): low, band and high pass from one structure, stable under modulation
#[derive(Clone, Copy, Default)]
pub struct Tsvf {
    ic1: f32,
    ic2: f32,
}
impl Tsvf {
    /// (low, band, high) for g = tan(pi fc / sr), k = 1 / Q
    #[inline]
    pub fn run(&mut self, x: f32, g: f32, k: f32) -> (f32, f32, f32) {
        let a1 = 1.0 / (1.0 + g * (g + k));
        let a2 = g * a1;
        let a3 = g * a2;
        let v3 = x - self.ic2;
        let v1 = a1 * self.ic1 + a2 * v3;
        let v2 = self.ic2 + a2 * self.ic1 + a3 * v3;
        self.ic1 = 2.0 * v1 - self.ic1;
        self.ic2 = 2.0 * v2 - self.ic2;
        if !(self.ic1.is_finite() && self.ic2.is_finite()) {
            self.ic1 = 0.0;
            self.ic2 = 0.0;
        }
        (v2, v1, x - k * v1 - v2)
    }
}

fn g_of(fc: f32, sr: f32) -> f32 {
    (std::f32::consts::PI * fc.clamp(10.0, sr * 0.45) / sr).tan()
}

/// a Linkwitz-Riley 4th-order split (two Butterworth sections per side): low + high sum to an all-pass
#[derive(Clone, Copy, Default)]
struct Lr4 {
    l1: Tsvf,
    l2: Tsvf,
    h1: Tsvf,
    h2: Tsvf,
}
impl Lr4 {
    #[inline]
    fn split(&mut self, x: f32, g: f32) -> (f32, f32) {
        const K: f32 = std::f32::consts::SQRT_2;
        let lo = self.l2.run(self.l1.run(x, g, K).0, g, K).0;
        let hi = self.h2.run(self.h1.run(x, g, K).2, g, K).2;
        (lo, hi)
    }
}

/// a 90-degree phase-difference network (two chains of 4 all-passes, Niemitalo's coefficients): (in-phase, quadrature)
#[derive(Clone, Copy, Default)]
pub struct Hilbert {
    a: [[f32; 2]; 4],
    ay: [[f32; 2]; 4],
    b: [[f32; 2]; 4],
    by: [[f32; 2]; 4],
    bd: f32,
}
const HA: [f32; 4] = [0.693_387_8 * 0.693_387_8, 0.936_065_4 * 0.936_065_4, 0.988_229_5 * 0.988_229_5, 0.998_748_8 * 0.998_748_8];
const HB: [f32; 4] = [0.402_192_1 * 0.402_192_1, 0.856_171_1 * 0.856_171_1, 0.972_290_9 * 0.972_290_9, 0.995_288_5 * 0.995_288_5];
impl Hilbert {
    #[inline]
    pub fn run(&mut self, x: f32) -> (f32, f32) {
        let mut u = x;
        for k in 0..4 {
            let y = HA[k] * (u + self.ay[k][1]) - self.a[k][1];
            self.a[k][1] = self.a[k][0];
            self.a[k][0] = u;
            self.ay[k][1] = self.ay[k][0];
            self.ay[k][0] = y;
            u = y;
        }
        let mut v = x;
        for k in 0..4 {
            let y = HB[k] * (v + self.by[k][1]) - self.b[k][1];
            self.b[k][1] = self.b[k][0];
            self.b[k][0] = v;
            self.by[k][1] = self.by[k][0];
            self.by[k][0] = y;
            v = y;
        }
        // the first path is one sample late
        let i = self.bd;
        self.bd = u;
        if !(i.is_finite() && v.is_finite()) {
            *self = Hilbert::default();
            return (0.0, 0.0);
        }
        (i, v)
    }
}

/// a fractional delay line
struct Line {
    buf: Vec<f32>,
    w: usize,
}
impl Line {
    fn new(n: usize) -> Line {
        Line { buf: vec![0.0; n.max(16)], w: 0 }
    }
    #[inline]
    fn push(&mut self, x: f32) {
        self.buf[self.w] = if x.is_finite() { x } else { 0.0 };
        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 - 3) as f32);
        let p = self.w as f32 - d + n as f32;
        let i = p as usize;
        let f = p - p.floor();
        // cubic (Hermite) read: a comb tuned to a high note stays bright and in tune
        let y0 = self.buf[(i + n - 1) % n];
        let y1 = self.buf[i % n];
        let y2 = self.buf[(i + 1) % n];
        let y3 = self.buf[(i + 2) % n];
        let c1 = 0.5 * (y2 - y0);
        let c2 = y0 - 2.5 * y1 + 2.0 * y2 - 0.5 * y3;
        let c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
        ((c3 * f + c2) * f + c1) * f + y1
    }
}

#[inline]
fn db2lin(d: f32) -> f32 {
    10f32.powf(d / 20.0)
}
#[inline]
fn lin2db(x: f32) -> f32 {
    20.0 * x.max(1e-9).log10()
}

/// a quick sanity net: a non-finite sample becomes silence
#[inline]
fn fin(x: f32) -> f32 {
    if x.is_finite() {
        x.clamp(-8.0, 8.0)
    } else {
        0.0
    }
}

// ── OTT ─────────────────────────────────────────────────────────────────────────

pub const OTT_PARAMS: [(&str, f32); 9] = [("depth", 0.6), ("time", 0.3), ("upward", 0.6), ("downward", 0.8), ("low", 0.5), ("mid", 0.5), ("high", 0.5), ("in", 0.5), ("out", 0.5)];

pub struct Ott {
    sr: f32,
    x1: [Lr4; 2],
    x2: [Lr4; 2],
    ap_low: [Lr4; 2],
    env: [f32; 3],
    /// a fast peak follower per band: the upward gain drops the moment a note starts, so there is no spike out of silence
    fast: [f32; 3],
    gain_db: [f32; 3],
    depth: f32,
    time: f32,
    up: f32,
    down: f32,
    band: [f32; 3],
    pre: f32,
    post: f32,
}
impl Ott {
    pub fn new(sr: f32) -> Ott {
        Ott { sr, x1: Default::default(), x2: Default::default(), ap_low: Default::default(), env: [0.0; 3], fast: [0.0; 3], gain_db: [0.0; 3], depth: 0.6, time: 0.3, up: 0.6, down: 0.8, band: [0.0; 3], pre: 0.0, post: 0.0 }
    }
}
/// per band: downward threshold (dBFS), attack and release at time = 1 (seconds)
const OTT_BAND: [(f32, f32, f32); 3] = [(-28.0, 0.048, 0.28), (-30.0, 0.022, 0.28), (-34.0, 0.0135, 0.13)];
impl Effect for Ott {
    fn process(&mut self, buf: &mut [f32]) {
        let (g1, g2) = (g_of(88.0, self.sr), g_of(2500.0, self.sr));
        let pre = db2lin(self.pre);
        let post = db2lin(self.post);
        let tscale = 0.05 + self.time * 1.95;
        let coef: Vec<(f32, f32)> = OTT_BAND.iter().map(|b| ((-1.0 / (b.1 * tscale * self.sr)).exp(), (-1.0 / (b.2 * tscale * self.sr)).exp())).collect();
        let bgain = [db2lin(self.band[0]), db2lin(self.band[1]), db2lin(self.band[2])];
        let kfa = (-1.0 / (0.0005 * self.sr)).exp();
        for fr in buf.chunks_exact_mut(2) {
            let mut bands = [[0.0f32; 2]; 3];
            for c in 0..2 {
                let x = fin(fr[c]) * pre;
                let (lo, rest) = self.x1[c].split(x, g1);
                let (mid, hi) = self.x2[c].split(rest, g2);
                // the low band through the same all-pass the others went through, so the bands sum flat
                let (a, b) = self.ap_low[c].split(lo, g2);
                bands[0][c] = a + b;
                bands[1][c] = mid;
                bands[2][c] = hi;
            }
            for b in 0..3 {
                let lvl = bands[b][0].abs().max(bands[b][1].abs());
                let (ka, kr) = coef[b];
                let k = if lvl > self.env[b] { ka } else { kr };
                self.env[b] = lvl + (self.env[b] - lvl) * k;
                self.fast[b] = lvl + (self.fast[b] - lvl) * if lvl > self.fast[b] { kfa } else { kr };
                let d = lin2db(self.env[b]);
                let du = lin2db(self.env[b].max(self.fast[b]));
                let t = OTT_BAND[b].0;
                let mut g = 0.0;
                if d > t {
                    g -= (d - t) * (1.0 - 1.0 / 66.0) * self.down;
                } else {
                    // upward, faded out near silence so the noise floor is not pulled up
                    let floor = ((du + 78.0) / 18.0).clamp(0.0, 1.0);
                    g += ((t - du).max(0.0) * (1.0 - 1.0 / 4.0) * self.up).min(36.0) * floor;
                }
                self.gain_db[b] = g * self.depth;
            }
            for c in 0..2 {
                let mut y = 0.0;
                for b in 0..3 {
                    y += bands[b][c] * db2lin(self.gain_db[b]) * bgain[b];
                }
                fr[c] = fin(y * post);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "depth" => self.depth = v,
            "time" => self.time = v,
            "upward" => self.up = v,
            "downward" => self.down = v,
            "low" => self.band[0] = (v - 0.5) * 24.0,
            "mid" => self.band[1] = (v - 0.5) * 24.0,
            "high" => self.band[2] = (v - 0.5) * 24.0,
            "in" => self.pre = (v - 0.5) * 24.0,
            "out" => self.post = (v - 0.5) * 24.0,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "OTT"
    }
    fn view(&self) -> Vec<f32> {
        self.gain_db.iter().map(|g| ((g + 24.0) / 48.0).clamp(0.0, 1.0)).collect()
    }
}

// ── Split Drive ────────────────────────────────────────────────────────────────

pub const SPLIT_PARAMS: [(&str, f32); 8] = [("split", 0.3), ("drive", 0.5), ("mode", 0.0), ("tone", 0.7), ("mix", 1.0), ("low", 0.5), ("mono low", 1.0), ("out", 0.5)];

pub struct SplitDrive {
    sr: f32,
    x: [Lr4; 2],
    tone: [Tsvf; 2],
    os: [(f32, f32); 2],
    crush: [(f32, f32); 2],
    dc: [(f32, f32); 2],
    split: f32,
    drive: f32,
    mode: usize,
    tone_v: f32,
    mix: f32,
    low: f32,
    mono_low: bool,
    out: f32,
}
impl SplitDrive {
    pub fn new(sr: f32) -> SplitDrive {
        SplitDrive { sr, x: Default::default(), tone: Default::default(), os: [(0.0, 0.0); 2], crush: [(0.0, 0.0); 2], dc: [(0.0, 0.0); 2], split: 150.0, drive: 0.5, mode: 0, tone_v: 0.7, mix: 1.0, low: 0.0, mono_low: true, out: 0.0 }
    }
    #[inline]
    fn shape(mode: usize, y: f32) -> f32 {
        match mode {
            // tube: asymmetric soft clip (even harmonics)
            0 => {
                let z = y + 0.25;
                (z.tanh() - 0.25f32.tanh()) * 0.9
            }
            // fold: sine wavefolder
            1 => (y * std::f32::consts::FRAC_PI_2).sin(),
            // hard clip
            2 => y.clamp(-1.0, 1.0),
            _ => (y * 1.2).tanh(),
        }
    }
}
impl Effect for SplitDrive {
    fn process(&mut self, buf: &mut [f32]) {
        let g = g_of(self.split, self.sr);
        let gt = g_of(800.0 * 2f32.powf(self.tone_v * 4.6), self.sr);
        let pre = 1.0 + self.drive * self.drive * 40.0;
        let comp = 1.0 / (1.0 + self.drive * 1.5);
        let low_g = db2lin(self.low);
        let outg = db2lin(self.out);
        // crush: bits and sample-hold from the drive in mode 3
        let bits = 2f32.powf(12.0 - self.drive * 9.0);
        let hold = 1.0 + (self.drive * self.drive * 24.0).floor();
        let mut lows = [0.0f32; 2];
        for fr in buf.chunks_exact_mut(2) {
            let mut highs = [0.0f32; 2];
            for c in 0..2 {
                let (lo, hi) = self.x[c].split(fin(fr[c]), g);
                lows[c] = lo;
                let y = hi * pre;
                let wet = if self.mode == 3 {
                    // bit / rate crusher
                    let (h, cnt) = &mut self.crush[c];
                    *cnt += 1.0;
                    if *cnt >= hold {
                        *cnt = 0.0;
                        *h = (Self::shape(3, y) * bits).round() / bits;
                    }
                    *h
                } else {
                    // twice the rate: a midpoint, then a 1-2-1 average back down (less aliasing from the new harmonics)
                    let (py, ps) = self.os[c];
                    let cur = Self::shape(self.mode, y);
                    let mid = Self::shape(self.mode, 0.5 * (py + y));
                    self.os[c] = (y, cur);
                    0.25 * ps + 0.5 * mid + 0.25 * cur
                };
                // DC from the asymmetric curve out, then the tone low-pass
                let (px, py) = self.dc[c];
                let d = wet - px + 0.995 * py;
                self.dc[c] = (wet, d);
                let toned = self.tone[c].run(d, gt, 0.9).0 * comp;
                highs[c] = hi * (1.0 - self.mix) + toned * self.mix;
            }
            if self.mono_low {
                let m = 0.5 * (lows[0] + lows[1]);
                lows = [m, m];
            }
            fr[0] = fin((lows[0] * low_g + highs[0]) * outg);
            fr[1] = fin((lows[1] * low_g + highs[1]) * outg);
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "split" => self.split = 40.0 * 2f32.powf(v * 4.3),
            "drive" => self.drive = v,
            "mode" => self.mode = (v * 3.0).round() as usize,
            "tone" => self.tone_v = v,
            "mix" => self.mix = v,
            "low" => self.low = (v - 0.5) * 24.0,
            "mono low" => self.mono_low = v > 0.5,
            "out" => self.out = (v - 0.5) * 24.0,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Split Drive"
    }
}

// ── Comb ────────────────────────────────────────────────────────────────────────

pub const COMB_PARAMS: [(&str, f32); 8] = [("note", 0.375), ("fine", 0.5), ("feedback", 0.85), ("damp", 0.3), ("rate", 0.2), ("sweep", 0.0), ("stereo", 0.2), ("mix", 0.5)];

pub struct Comb {
    sr: f32,
    line: [Line; 2],
    lp: [f32; 2],
    ph: f32,
    note: f32,
    fine: f32,
    fb: f32,
    damp: f32,
    rate: f32,
    sweep: f32,
    stereo: f32,
    mix: f32,
}
impl Comb {
    pub fn new(sr: f32) -> Comb {
        let n = (sr / 15.0) as usize + 8;
        Comb { sr, line: [Line::new(n), Line::new(n)], lp: [0.0; 2], ph: 0.0, note: 48.0, fine: 0.0, fb: 0.7, damp: 0.3, rate: 0.5, sweep: 0.0, stereo: 0.0, mix: 0.5 }
    }
}
impl Effect for Comb {
    fn process(&mut self, buf: &mut [f32]) {
        let dl = 1.0 - self.damp * 0.92;
        // a comb's gain grows with its feedback: scaled back so the level stays even across the knob
        let norm = 1.0 - self.fb.abs() * 0.85;
        for fr in buf.chunks_exact_mut(2) {
            self.ph = (self.ph + self.rate / self.sr).fract();
            let lfo = (TAU * self.ph).sin() * self.sweep * 12.0;
            for c in 0..2 {
                let st = if c == 0 { -self.stereo } else { self.stereo } * 0.35;
                let semis = self.note + self.fine + lfo + st;
                let f = 440.0 * 2f32.powf((semis - 69.0) / 12.0);
                let d = self.sr / f.max(15.0);
                let x = fin(fr[c]);
                let y = self.line[c].read(d);
                self.lp[c] += (y - self.lp[c]) * dl;
                self.line[c].push(x + self.lp[c] * self.fb);
                fr[c] = fin(x * (1.0 - self.mix) + y * norm * self.mix * 1.4);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "note" => self.note = 12.0 + v * 96.0,
            "fine" => self.fine = (v - 0.5) * 2.0,
            "feedback" => self.fb = (v * 2.0 - 1.0) * 0.97,
            "damp" => self.damp = v,
            "rate" => self.rate = 0.02 * 2f32.powf(v * 9.0),
            "sweep" => self.sweep = v,
            "stereo" => self.stereo = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Comb"
    }
}

// ── Phaser ──────────────────────────────────────────────────────────────────────

pub const PHASER_PARAMS: [(&str, f32); 7] = [("stages", 0.5), ("rate", 0.25), ("depth", 0.7), ("center", 0.45), ("feedback", 0.65), ("stereo", 0.5), ("mix", 0.5)];

pub struct Phaser {
    sr: f32,
    ap: [[f32; 12]; 2],
    fbs: [f32; 2],
    ph: f32,
    stages: usize,
    rate: f32,
    depth: f32,
    center: f32,
    fb: f32,
    stereo: f32,
    mix: f32,
}
impl Phaser {
    pub fn new(sr: f32) -> Phaser {
        Phaser { sr, ap: [[0.0; 12]; 2], fbs: [0.0; 2], ph: 0.0, stages: 8, rate: 0.3, depth: 0.7, center: 800.0, fb: 0.4, stereo: 0.5, mix: 0.5 }
    }
}
impl Effect for Phaser {
    fn process(&mut self, buf: &mut [f32]) {
        for fr in buf.chunks_exact_mut(2) {
            self.ph = (self.ph + self.rate / self.sr).fract();
            for c in 0..2 {
                let p = self.ph + if c == 1 { self.stereo * 0.25 } else { 0.0 };
                let l = 0.5 - 0.5 * (TAU * p).cos();
                // the notches sweep over +-3 octaves around the centre
                let f = (self.center * 2f32.powf((l - 0.5) * 6.0 * self.depth)).clamp(30.0, self.sr * 0.45);
                let t = (std::f32::consts::PI * f / self.sr).tan();
                let a = (t - 1.0) / (t + 1.0);
                let x = fin(fr[c]);
                let mut y = x + self.fbs[c] * self.fb;
                for k in 0..self.stages {
                    let s = self.ap[c][k];
                    let o = a * y + s;
                    self.ap[c][k] = y - a * o;
                    y = o;
                }
                self.fbs[c] = fin(y).clamp(-2.0, 2.0);
                fr[c] = fin(x * (1.0 - self.mix * 0.5) + y * self.mix * 0.5);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "stages" => self.stages = [4, 6, 8, 10, 12][(v * 4.0).round() as usize],
            "rate" => self.rate = 0.02 * 2f32.powf(v * 9.0),
            "depth" => self.depth = v,
            "center" => self.center = 100.0 * 2f32.powf(v * 6.5),
            "feedback" => self.fb = (v * 2.0 - 1.0) * 0.9,
            "stereo" => self.stereo = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Phaser"
    }
}

// ── Flanger ─────────────────────────────────────────────────────────────────────

pub const FLANGER_PARAMS: [(&str, f32); 6] = [("delay", 0.3), ("depth", 0.6), ("rate", 0.2), ("feedback", 0.75), ("stereo", 0.5), ("mix", 0.5)];

pub struct Flanger {
    sr: f32,
    line: [Line; 2],
    ph: f32,
    delay: f32,
    depth: f32,
    rate: f32,
    fb: f32,
    stereo: f32,
    mix: f32,
    last: [f32; 2],
}
impl Flanger {
    pub fn new(sr: f32) -> Flanger {
        let n = (sr * 0.03) as usize;
        Flanger { sr, line: [Line::new(n), Line::new(n)], ph: 0.0, delay: 3.0, depth: 0.6, rate: 0.2, fb: 0.5, stereo: 0.5, mix: 0.5, last: [0.0; 2] }
    }
}
impl Effect for Flanger {
    fn process(&mut self, buf: &mut [f32]) {
        for fr in buf.chunks_exact_mut(2) {
            self.ph = (self.ph + self.rate / self.sr).fract();
            for c in 0..2 {
                let p = self.ph + if c == 1 { self.stereo * 0.25 } else { 0.0 };
                let l = 0.5 - 0.5 * (TAU * p).cos();
                let ms = 0.1 + self.delay * (1.0 - self.depth * 0.95) + self.delay * self.depth * 0.95 * l * 2.0;
                let d = (ms * 0.001 * self.sr).max(1.0);
                let x = fin(fr[c]);
                let y = self.line[c].read(d);
                self.line[c].push(x + y * self.fb);
                self.last[c] = y;
                fr[c] = fin(x * (1.0 - self.mix * 0.5) + y * self.mix * 0.5 * (1.0 - self.fb.abs() * 0.5));
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "delay" => self.delay = 0.3 + v * 9.7,
            "depth" => self.depth = v,
            "rate" => self.rate = 0.02 * 2f32.powf(v * 9.0),
            "feedback" => self.fb = (v * 2.0 - 1.0) * 0.95,
            "stereo" => self.stereo = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Flanger"
    }
}

// ── Freq Shifter ────────────────────────────────────────────────────────────────

pub const FSHIFT_PARAMS: [(&str, f32); 6] = [("shift", 0.6), ("fine", 0.5), ("rate", 0.2), ("sweep", 0.0), ("stereo", 0.0), ("mix", 0.5)];

/// the shift in Hz of a 0..1 knob: centre = 0, a square curve out to +-2 kHz (fine control near zero)
pub fn shift_hz(v: f32) -> f32 {
    let x = (v - 0.5) * 2.0;
    x.signum() * x * x * 2000.0
}

pub struct FreqShifter {
    sr: f32,
    h: [Hilbert; 2],
    ph: [f64; 2],
    lfo: f32,
    shift: f32,
    fine: f32,
    rate: f32,
    sweep: f32,
    stereo: f32,
    mix: f32,
}
impl FreqShifter {
    pub fn new(sr: f32) -> FreqShifter {
        FreqShifter { sr, h: Default::default(), ph: [0.0; 2], lfo: 0.0, shift: 20.0, fine: 0.0, rate: 0.2, sweep: 0.0, stereo: 0.0, mix: 0.5 }
    }
}
impl Effect for FreqShifter {
    fn process(&mut self, buf: &mut [f32]) {
        for fr in buf.chunks_exact_mut(2) {
            self.lfo = (self.lfo + self.rate / self.sr).fract();
            let base = self.shift + self.fine + (TAU * self.lfo).sin() * self.sweep * 400.0;
            for c in 0..2 {
                // stereo: the right channel shifts by a blend towards the opposite direction
                let hz = if c == 1 { base * (1.0 - 2.0 * self.stereo) } else { base };
                self.ph[c] = (self.ph[c] + hz as f64 / self.sr as f64).rem_euclid(1.0);
                let x = fin(fr[c]);
                let (i, q) = self.h[c].run(x);
                let a = (std::f64::consts::TAU * self.ph[c]) as f32;
                let y = i * a.cos() + q * a.sin();
                fr[c] = fin(x * (1.0 - self.mix) + y * self.mix);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "shift" => self.shift = shift_hz(v),
            "fine" => self.fine = (v - 0.5) * 20.0,
            "rate" => self.rate = 0.02 * 2f32.powf(v * 9.0),
            "sweep" => self.sweep = v,
            "stereo" => self.stereo = v,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Freq Shifter"
    }
}

// ── Imager ──────────────────────────────────────────────────────────────────────

pub const IMAGER_PARAMS: [(&str, f32); 3] = [("width", 0.5), ("mono below", 0.35), ("out", 0.5)];

pub struct Imager {
    sr: f32,
    x: Lr4,
    xs: Lr4,
    width: f32,
    mono_hz: f32,
    out: f32,
}
impl Imager {
    pub fn new(sr: f32) -> Imager {
        Imager { sr, x: Lr4::default(), xs: Lr4::default(), width: 1.0, mono_hz: 120.0, out: 0.0 }
    }
}
impl Effect for Imager {
    fn process(&mut self, buf: &mut [f32]) {
        let g = g_of(self.mono_hz.max(20.0), self.sr);
        let outg = db2lin(self.out);
        for fr in buf.chunks_exact_mut(2) {
            let (l, r) = (fin(fr[0]), fin(fr[1]));
            let m = 0.5 * (l + r);
            let mut s = 0.5 * (l - r);
            if self.mono_hz > 21.0 {
                // the side's lows removed (the mid keeps them): below the split everything is mono; the mid takes the same all-pass
                let (_, hi) = self.xs.split(s, g);
                s = hi;
                let (a, b) = self.x.split(m, g);
                let m2 = a + b;
                fr[0] = fin((m2 + s * self.width) * outg);
                fr[1] = fin((m2 - s * self.width) * outg);
            } else {
                fr[0] = fin((m + s * self.width) * outg);
                fr[1] = fin((m - s * self.width) * outg);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "width" => self.width = v * 2.0,
            "mono below" => self.mono_hz = if v < 0.02 { 0.0 } else { 20.0 * 2f32.powf(v * 5.0) },
            "out" => self.out = (v - 0.5) * 24.0,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Imager"
    }
}

// ── Transient ───────────────────────────────────────────────────────────────────

pub const TRANSIENT_PARAMS: [(&str, f32); 5] = [("attack", 0.7), ("sustain", 0.4), ("speed", 0.5), ("out", 0.5), ("mix", 1.0)];

pub struct Transient {
    sr: f32,
    fast: f32,
    slow: f32,
    att: f32,
    sus: f32,
    speed: f32,
    out: f32,
    mix: f32,
}
impl Transient {
    pub fn new(sr: f32) -> Transient {
        Transient { sr, fast: 0.0, slow: 0.0, att: 0.0, sus: 0.0, speed: 0.5, out: 0.0, mix: 1.0 }
    }
}
impl Effect for Transient {
    fn process(&mut self, buf: &mut [f32]) {
        let tf = 0.0008 + (1.0 - self.speed) * 0.004;
        let ts = 0.02 + (1.0 - self.speed) * 0.15;
        let kf = (-1.0 / (tf * self.sr)).exp();
        let ks = (-1.0 / (ts * self.sr)).exp();
        let outg = db2lin(self.out);
        for fr in buf.chunks_exact_mut(2) {
            let lvl = fin(fr[0]).abs().max(fin(fr[1]).abs());
            self.fast = lvl + (self.fast - lvl) * kf;
            // the slow one rises slowly and falls slowly: fast > slow means an attack, fast < slow the tail
            self.slow = lvl + (self.slow - lvl) * ks;
            let diff = lin2db(self.fast + 1e-6) - lin2db(self.slow + 1e-6);
            // boosts stop at +9 dB (a note out of silence would otherwise jump by the whole difference), cuts at -12
            let g_db = if diff > 0.0 { (diff.min(24.0) * self.att).clamp(-12.0, 9.0) } else { (-diff.max(-24.0) * self.sus).clamp(-12.0, 9.0) };
            let g = db2lin(g_db) * outg;
            for c in 0..2 {
                let x = fin(fr[c]);
                fr[c] = fin(x * (1.0 - self.mix) + x * g * self.mix);
            }
        }
    }
    fn set_param(&mut self, n: &str, v: f32) {
        let v = v.clamp(0.0, 1.0);
        match n {
            "attack" => self.att = (v - 0.5) * 1.5,
            "sustain" => self.sus = (v - 0.5) * 1.5,
            "speed" => self.speed = v,
            "out" => self.out = (v - 0.5) * 24.0,
            "mix" => self.mix = v,
            _ => {}
        }
    }
    fn name(&self) -> &'static str {
        "Transient"
    }
}

// ── presets ─────────────────────────────────────────────────────────────────────

type Pre = (&'static str, &'static [(&'static str, f32)]);
pub const OTT_PRESETS: [Pre; 4] = [
    ("Default 60%", &[]),
    ("Bass sheen (full)", &[("depth", 1.0), ("upward", 0.75), ("low", 0.45), ("high", 0.55), ("out", 0.42)]),
    ("Downward only (glue)", &[("depth", 0.5), ("upward", 0.0), ("downward", 1.0), ("out", 0.6)]),
    ("Upward only (detail)", &[("depth", 0.7), ("upward", 1.0), ("downward", 0.0), ("out", 0.38)]),
];
pub const SPLIT_PRESETS: [Pre; 5] = [
    ("Tube mids", &[("drive", 0.45), ("mode", 0.0), ("tone", 0.7)]),
    ("Folded growl", &[("drive", 0.6), ("mode", 0.333), ("tone", 0.65), ("split", 0.35)]),
    ("Hard clip", &[("drive", 0.55), ("mode", 0.667), ("tone", 0.6)]),
    ("Crushed", &[("drive", 0.5), ("mode", 1.0), ("tone", 0.75), ("mix", 0.7)]),
    ("Parallel grit", &[("drive", 0.7), ("mode", 0.0), ("mix", 0.4)]),
];
pub const COMB_PRESETS: [Pre; 4] = [
    ("Metal ring", &[("feedback", 0.92), ("damp", 0.15), ("mix", 0.45)]),
    ("Hollow (negative)", &[("feedback", 0.1), ("damp", 0.25), ("mix", 0.5)]),
    ("Swept comb", &[("feedback", 0.88), ("sweep", 0.35), ("rate", 0.3), ("mix", 0.5)]),
    ("Wide shimmer", &[("feedback", 0.85), ("stereo", 0.8), ("mix", 0.35)]),
];
pub const PHASER_PRESETS: [Pre; 3] = [("Slow swirl", &[("rate", 0.15), ("feedback", 0.7)]), ("Jet", &[("stages", 1.0), ("rate", 0.35), ("feedback", 0.85), ("mix", 0.6)]), ("Notch growl", &[("stages", 0.75), ("rate", 0.55), ("depth", 0.9), ("feedback", 0.25)])];
pub const FLANGER_PRESETS: [Pre; 3] = [("Jet sweep", &[("rate", 0.15), ("feedback", 0.88)]), ("Metallic", &[("delay", 0.1), ("feedback", 0.95), ("rate", 0.3)]), ("Negative hollow", &[("feedback", 0.1), ("mix", 0.6)])];
pub const FSHIFT_PRESETS: [Pre; 3] = [("Slight shimmer", &[("shift", 0.53), ("stereo", 0.5), ("mix", 0.4)]), ("Robot", &[("shift", 0.72), ("mix", 0.8)]), ("Barber pole", &[("shift", 0.52), ("sweep", 0.3), ("rate", 0.25), ("mix", 0.5)])];
pub const IMAGER_PRESETS: [Pre; 3] = [("Bass: mono below 120 Hz", &[]), ("Wide top", &[("width", 0.75), ("mono below", 0.45)]), ("Mono", &[("width", 0.0)])];
pub const TRANSIENT_PRESETS: [Pre; 3] = [("Punch", &[]), ("Tight (short tails)", &[("attack", 0.6), ("sustain", 0.2)]), ("Soften", &[("attack", 0.3), ("sustain", 0.6)])];

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

    fn tone(sr: f32, f: f32, n: usize, amp: f32) -> Vec<f32> {
        (0..n).flat_map(|i| {
            let x = (TAU * f * i as f32 / sr).sin() * amp;
            [x, x]
        }).collect()
    }
    fn rms(v: &[f32]) -> f32 {
        (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
    }

    #[test]
    fn every_effect_stays_finite_for_any_settings() {
        let sr = 48000.0;
        for name in ["OTT", "Split Drive", "Comb", "Phaser", "Flanger", "Freq Shifter", "Imager", "Transient"] {
            for v in [0.0f32, 0.5, 1.0] {
                let mut e = super::super::fx::make(name, sr).unwrap();
                for (p, _) in super::super::fx::defaults(name) {
                    e.set_param(p, v);
                }
                let mut b: Vec<f32> = (0..9600).map(|i| ((i * 7919) % 2000) as f32 / 1000.0 - 1.0).collect();
                b[100] = f32::NAN;
                b[200] = 40.0;
                e.process(&mut b);
                assert!(b.iter().all(|x| x.is_finite() && x.abs() <= 8.0), "{name} at {v}");
            }
        }
    }

    #[test]
    fn ott_brings_quiet_up_and_loud_down() {
        let sr = 48000.0;
        let run = |amp: f32| {
            let mut o = Ott::new(sr);
            for (p, v) in OTT_PARAMS {
                o.set_param(p, v);
            }
            o.set_param("depth", 1.0);
            o.set_param("upward", 1.0);
            let mut b = tone(sr, 1000.0, 48000, amp);
            o.process(&mut b);
            rms(&b[48000..]) / rms(&tone(sr, 1000.0, 48000, amp)[48000..])
        };
        assert!(run(0.01) > 2.0, "quiet came up only {}", run(0.01));
        assert!(run(0.9) < 0.5, "loud went down only to {}", run(0.9));
    }

    #[test]
    fn crossovers_sum_flat_and_split_drive_keeps_the_sub_clean() {
        let sr = 48000.0;
        // the OTT at depth 0 is transparent in level across the spectrum
        for f in [50.0, 300.0, 2500.0, 8000.0] {
            let mut o = Ott::new(sr);
            o.set_param("depth", 0.0);
            let mut b = tone(sr, f, 24000, 0.5);
            o.process(&mut b);
            let r = rms(&b[12000..]) / rms(&tone(sr, f, 24000, 0.5)[12000..]);
            assert!((r - 1.0).abs() < 0.06, "{f} Hz: {r}");
        }
        // a 40 Hz sine through heavy split drive comes out a sine: (almost) nothing at its 3rd harmonic
        let mut d = SplitDrive::new(sr);
        for (p, v) in SPLIT_PARAMS {
            d.set_param(p, v);
        }
        d.set_param("drive", 1.0);
        let mut b = tone(sr, 40.0, 48000, 0.8);
        d.process(&mut b);
        let mono: Vec<f32> = b.chunks(2).map(|c| c[0]).skip(24000).collect();
        let mag = |f: f32| {
            let (mut re, mut im) = (0.0f32, 0.0f32);
            for (i, x) in mono.iter().enumerate() {
                let a = TAU * f * i as f32 / sr;
                re += x * a.cos();
                im += x * a.sin();
            }
            (re * re + im * im).sqrt()
        };
        assert!(mag(120.0) < mag(40.0) * 0.05, "the sub distorted: {} vs {}", mag(120.0), mag(40.0));
    }

    #[test]
    fn the_comb_rings_at_its_note_and_the_shifter_moves_partials() {
        let sr = 48000.0;
        // white-ish noise through a comb at A2 (110 Hz): energy piles up at 110 Hz and its multiples
        let mut c = Comb::new(sr);
        for (p, v) in COMB_PARAMS {
            c.set_param(p, v);
        }
        c.set_param("note", (45.0 - 12.0) / 96.0);
        c.set_param("feedback", 0.97);
        c.set_param("mix", 1.0);
        let mut seed = 1u32;
        let mut b: Vec<f32> = (0..96000).map(|_| {
            seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
            (seed >> 9) as f32 / 8388608.0 - 1.0
        }).flat_map(|x| [x * 0.2, x * 0.2]).collect();
        c.process(&mut b);
        let mono: Vec<f32> = b.chunks(2).map(|x| x[0]).skip(48000).collect();
        let mag = |f: f32| {
            let (mut re, mut im) = (0.0f32, 0.0f32);
            for (i, x) in mono.iter().enumerate() {
                let a = TAU * f * i as f32 / sr;
                re += x * a.cos();
                im += x * a.sin();
            }
            (re * re + im * im).sqrt()
        };
        assert!(mag(220.0) > 3.0 * mag(165.0), "no peak at the 2nd harmonic: {} vs {}", mag(220.0), mag(165.0));
        // 1 kHz shifted by +100 Hz: the energy moves to 1.1 kHz
        let mut s = FreqShifter::new(sr);
        s.set_param("mix", 1.0);
        s.shift = 100.0;
        let mut b = tone(sr, 1000.0, 48000, 0.5);
        s.process(&mut b);
        let mono: Vec<f32> = b.chunks(2).map(|x| x[0]).skip(24000).collect();
        let mag = |f: f32| {
            let (mut re, mut im) = (0.0f32, 0.0f32);
            for (i, x) in mono.iter().enumerate() {
                let a = TAU * f * i as f32 / sr;
                re += x * a.cos();
                im += x * a.sin();
            }
            (re * re + im * im).sqrt()
        };
        assert!(mag(1100.0) > 10.0 * mag(1000.0) && mag(1100.0) > 10.0 * mag(900.0), "{} {} {}", mag(900.0), mag(1000.0), mag(1100.0));
    }

    #[test]
    fn the_imager_keeps_the_lows_mono() {
        let sr = 48000.0;
        let mut im = Imager::new(sr);
        im.set_param("width", 0.5);
        im.set_param("mono below", 0.45);
        // opposite-polarity 50 Hz (all side): mostly removed; opposite-polarity 3 kHz: widened
        let side = |f: f32, im: &mut Imager| {
            let mut b: Vec<f32> = (0..48000).flat_map(|i| {
                let x = (TAU * f * i as f32 / sr).sin() * 0.3;
                [x, -x]
            }).collect();
            im.process(&mut b);
            rms(&b[48000..])
        };
        let low = side(50.0, &mut im);
        assert!(low < 0.06, "{low}");
        let mut im2 = Imager::new(sr);
        im2.set_param("width", 1.0);
        assert!(side(3000.0, &mut im2) > 0.35);
    }
}

← Back to index