灵符lingfu
L

ling-daw

↓ 0 downloads

Ling DAW engine: synth, physical-modeling drums, effects, mixer, sequencer, project I/O

lingfu install ling-daw
LicenseApache-2.0 OR MIT
AuthorsLing Lin <taellinglin@gmail.com>, Sanny Lin <SannyLing53@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (91)

src/dsp/drums.rs
//! Physically-modeled drum synth ("Ling Membrane").
//!
//! * Membranes (kick, toms, snare head): circular-membrane modal synthesis —
//!   Bessel-zero mode ratios, each mode a damped resonator, excited by a stick
//!   impulse whose brightness follows `hardness`; tension modulation gives the
//!   characteristic pitch drop. `purity` fades the upper modes away for analog-style
//!   sine drums, `damp` mutes the shell, `sweep` sets the pitch-drop time.
//! * Snare wires: noise band-passed and driven by the head's motion (sympathetic);
//!   `analog` adds the two tuned oscillators of electronic snares, `swell` fades the
//!   wires in (brush sweeps).
//! * Cymbals / hats: inharmonic plate modes + noise, or (`analog`) the six detuned
//!   square oscillators of classic drum machines; `gate` cuts any piece like a gated reverb.
//! * Clap: several staggered noise bursts through a band filter.
//! * Every GM key has its own voicing (tom pitches, side stick, cowbell, conga, ...).
use crate::dsp::fx::{FilterMode, Svf};
use crate::dsp::{Instrument, Rng, TAU};

/// Bessel-zero ratios of an ideal circular membrane (modes 01,11,21,02,31,12,41,22).
const MEMBRANE: [f32; 8] = [1.0, 1.593, 2.136, 2.296, 2.653, 2.918, 3.156, 3.500];
/// Inharmonic plate-ish ratios for metal.
const PLATE: [f32; 10] = [1.0, 1.4471, 1.6170, 1.9265, 2.5028, 2.6637, 3.1585, 3.6152, 4.2127, 5.0416];
/// The six square-wave oscillators of the classic analog hat circuit (Hz).
const SQUARES: [f32; 6] = [205.3, 304.4, 369.6, 522.7, 540.0, 800.0];

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Kind {
    Kick,
    Snare,
    Tom,
    ClosedHat,
    OpenHat,
    Crash,
    Ride,
    Clap,
}

/// What a particular key does with its piece: a voicing on top of the piece's parameters.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Flavor {
    Normal,
    SideStick,
    Cowbell,
    Bell,
}

#[derive(Clone, Copy, Debug)]
pub struct Var {
    pub semis: f32,
    pub decay: f32,
    pub noise: f32,
    pub click: f32,
    pub level: f32,
    pub hard: f32,
    pub flavor: Flavor,
}
impl Var {
    const N: Var = Var { semis: 0.0, decay: 1.0, noise: 1.0, click: 1.0, level: 1.0, hard: 0.0, flavor: Flavor::Normal };
}

#[derive(Clone)]
struct Mode {
    x: f32,
    y: f32,
    ratio: f32,
    amp: f32,
    decay: f32, // T60 seconds
    r: f32,
    c: f32,
    s: f32,
}

#[derive(Clone)]
pub struct DrumParams {
    pub tune: f32,     // 0..1 -> pitch scale
    pub decay: f32,    // 0..1 -> length scale
    pub tension: f32,  // 0..1 -> pitch-drop amount
    pub hardness: f32, // 0..1 -> exciter brightness
    pub body: f32,     // 0..1 -> balance shell vs noise/wires
    pub level: f32,
    pub click: f32,  // beater / stick transient
    pub tone: f32,   // brightness of the noise / cymbal filters
    pub damp: f32,   // shell damping (upper modes die faster)
    pub purity: f32, // 0 modal membrane .. 1 pure sine (808)
    pub sweep: f32,  // pitch-drop time
    pub drive: f32,  // saturation
    pub gate: f32,   // 0 off .. hard gate length (gated reverb / tight metal)
    pub analog: f32, // 0 physical .. 1 drum-machine oscillators
    pub swell: f32,  // noise fade-in (brush sweeps)
    pub pan: f32,    // 0..1, 0.5 centre
}
impl DrumParams {
    fn new(level: f32) -> Self {
        DrumParams { tune: 0.5, decay: 0.5, tension: 0.5, hardness: 0.5, body: 0.5, level, click: 0.5, tone: 0.5, damp: 0.0, purity: 0.0, sweep: 0.5, drive: 0.0, gate: 0.0, analog: 0.0, swell: 0.0, pan: 0.5 }
    }
    fn list(&self) -> [(&'static str, f32); 16] {
        [
            ("tune", self.tune),
            ("decay", self.decay),
            ("tension", self.tension),
            ("hardness", self.hardness),
            ("body", self.body),
            ("level", self.level),
            ("click", self.click),
            ("tone", self.tone),
            ("damp", self.damp),
            ("purity", self.purity),
            ("sweep", self.sweep),
            ("drive", self.drive),
            ("gate", self.gate),
            ("analog", self.analog),
            ("swell", self.swell),
            ("pan", self.pan),
        ]
    }
    fn set(&mut self, n: &str, v: f32) {
        match n {
            "tune" => self.tune = v,
            "decay" => self.decay = v,
            "tension" => self.tension = v,
            "hardness" => self.hardness = v,
            "body" => self.body = v,
            "level" => self.level = v,
            "click" => self.click = v,
            "tone" => self.tone = v,
            "damp" => self.damp = v,
            "purity" => self.purity = v,
            "sweep" => self.sweep = v,
            "drive" => self.drive = v,
            "gate" => self.gate = v,
            "analog" => self.analog = v,
            "swell" => self.swell = v,
            "pan" => self.pan = v,
            _ => {}
        }
    }
}

struct DrumVoice {
    kind: Kind,
    var: Var,
    active: bool,
    modes: Vec<Mode>,
    t: f32,
    vel: f32,
    base: f32,
    pitch_drop: f32,
    pitch_tau: f32,
    exc_lp: f32,
    exc_a: f32,
    exc_left: u32,
    wires: Svf,
    wire_env: f32,
    wire_t60: f32,
    noise_hp: Svf,
    noise_bp: Svf,
    noise_env: f32,
    noise_t60: f32,
    swell_g: f32,
    swell_t: f32,
    sq_ph: [f32; 6],
    tone_ph: [f32; 2],
    tick: u32,
    pan: f32,
    gate_t: f32,
    hard: f32,
    pure_gain: f32,
    p: DrumParams,
}

impl DrumVoice {
    fn new(kind: Kind) -> Self {
        DrumVoice {
            kind,
            var: Var::N,
            active: false,
            modes: vec![],
            t: 0.0,
            vel: 0.0,
            base: 100.0,
            pitch_drop: 0.0,
            pitch_tau: 0.03,
            exc_lp: 0.0,
            exc_a: 0.5,
            exc_left: 0,
            wires: Svf::new(FilterMode::Band),
            wire_env: 0.0,
            wire_t60: 0.3,
            noise_hp: Svf::new(FilterMode::High),
            noise_bp: Svf::new(FilterMode::Band),
            noise_env: 0.0,
            noise_t60: 0.1,
            swell_g: 1.0,
            swell_t: 0.0,
            sq_ph: [0.0; 6],
            tone_ph: [0.0; 2],
            tick: 0,
            pan: 0.0,
            gate_t: 0.0,
            hard: 0.5,
            pure_gain: 1.0,
            p: DrumParams::new(0.9),
        }
    }

    fn trigger(&mut self, vel: f32, sr: f32, p: &DrumParams, var: Var, human: f32) {
        self.p = p.clone();
        self.var = var;
        self.vel = vel;
        self.t = 0.0;
        self.active = true;
        self.exc_lp = 0.0;
        self.tick = 0;
        self.sq_ph = [0.0; 6];
        self.tone_ph = [0.0; 2];
        // harder playing = brighter; the variant can push it further
        let hard = (p.hardness * 0.75 + vel * 0.25 + var.hard).clamp(0.0, 1.0);
        self.hard = hard;
        // stick impulse: harder = shorter/brighter (less lowpass smoothing)
        self.exc_a = 0.98 - hard * 0.93;
        self.exc_left = 2 + ((1.0 - hard) * 24.0) as u32;
        let tune = 2f32.powf((p.tune - 0.5) * 1.6) * 2f32.powf(var.semis / 12.0) * human;
        let dscale = (0.35 + p.decay * 1.9) * var.decay;
        self.modes.clear();
        let (n_modes, base, decay0, drop, tau) = match self.kind {
            Kind::Kick => (5, 52.0 * tune, 0.45 * dscale, 2.4 * p.tension + 0.3, 0.035),
            Kind::Tom => (7, 120.0 * tune, 0.5 * dscale, 0.7 * p.tension + 0.1, 0.06),
            Kind::Snare => (8, 185.0 * tune, 0.16 * dscale, 0.35 * p.tension, 0.02),
            Kind::ClosedHat => (10, 320.0 * tune, 0.05 * dscale, 0.0, 0.01),
            Kind::OpenHat => (10, 320.0 * tune, 0.5 * dscale, 0.0, 0.01),
            Kind::Crash => (10, 280.0 * tune, 1.4 * dscale, 0.0, 0.01),
            Kind::Ride => (10, 400.0 * tune, 1.0 * dscale, 0.0, 0.01),
            Kind::Clap => (0, 1000.0, 0.0, 0.0, 0.01),
        };
        self.base = base;
        self.pitch_drop = drop;
        self.pitch_tau = tau * (0.2 + p.sweep * 1.6);
        let metal = matches!(self.kind, Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride);
        // purity: fade the membrane's upper modes out for analog-style sine drums
        let keep = 1.0 - p.purity;
        self.pure_gain = 1.0 + p.purity * 1.3;
        for i in 0..n_modes {
            let ratio = if metal { PLATE[i] * 2.0 } else { MEMBRANE[i] };
            // higher modes die faster; shell partials on kick fade quickest; damp shortens them further
            let mut amp = if metal { 0.6 / (1.0 + i as f32 * 0.25) } else { 1.0 / (1.0 + i as f32 * 0.9) };
            if !metal && i > 0 {
                amp *= keep;
            }
            let decay = decay0 / (1.0 + i as f32 * if metal { 0.15 } else { 0.7 + p.damp * 2.5 });
            self.modes.push(Mode { x: 0.0, y: 0.0, ratio, amp, decay, r: 0.0, c: 1.0, s: 0.0 });
        }
        self.update_coeffs(sr, 0.0);
        // wire / noise tails are T60 times (time to -60 dB)
        self.wire_env = 1.0;
        self.wire_t60 = (0.10 + 0.55 * p.decay) * var.decay;
        self.noise_env = 1.0;
        self.noise_t60 = match self.kind {
            Kind::ClosedHat => 0.07 * dscale,
            Kind::OpenHat => 0.45 * dscale,
            Kind::Crash => 1.4 * dscale,
            Kind::Ride => 1.0 * dscale,
            Kind::Clap => 0.14 + 0.2 * p.decay,
            _ => 0.05,
        };
        self.swell_t = p.swell * p.swell * 0.12;
        self.swell_g = if self.swell_t > 0.0 { 0.0 } else { 1.0 };
        // gate: seconds until the piece is cut
        self.gate_t = if p.gate > 0.001 { 0.03 + p.gate * 0.4 } else { 0.0 };
        self.pan = (p.pan * 2.0 - 1.0).clamp(-1.0, 1.0);
        self.wires = Svf::new(FilterMode::Band);
        self.noise_hp = Svf::new(FilterMode::High);
        self.noise_bp = Svf::new(FilterMode::Band);
    }

    fn update_coeffs(&mut self, sr: f32, t: f32) {
        let bend = 1.0 + self.pitch_drop * (-t / self.pitch_tau).exp();
        for m in self.modes.iter_mut() {
            let w = TAU * (self.base * m.ratio * bend).min(sr * 0.45) / sr;
            m.c = w.cos();
            m.s = w.sin();
            m.r = (-6.9 / (m.decay.max(0.005) * sr)).exp();
        }
    }

    #[inline]
    fn sample(&mut self, sr: f32, rng: &mut Rng) -> f32 {
        if !self.active {
            return 0.0;
        }
        let dt = 1.0 / sr;
        let p = &self.p;
        let var = self.var;
        // exciter: short noise/impulse burst, one-pole smoothed by hardness
        let mut exc = 0.0;
        if self.exc_left > 0 {
            self.exc_left -= 1;
            let raw = if matches!(self.kind, Kind::Kick | Kind::Tom | Kind::Snare) { 1.0 } else { rng.next() };
            self.exc_lp += (raw - self.exc_lp) * (1.0 - self.exc_a);
            exc = self.exc_lp;
        } else {
            self.exc_lp *= 0.9;
        }
        self.tick += 1;
        if self.tick % 16 == 0 && self.pitch_drop > 0.0 {
            let t = self.t;
            self.update_coeffs(sr, t);
        }
        let mut head = 0.0;
        for m in self.modes.iter_mut() {
            let nx = m.r * (m.c * m.x - m.s * m.y) + exc * m.amp * 0.02;
            let ny = m.r * (m.s * m.x + m.c * m.y);
            m.x = nx;
            m.y = ny;
            head += ny * m.amp * 8.0;
        }
        head *= self.pure_gain;
        let p = &self.p;
        let (body_w, noise_w) = (p.body, 1.0 - p.body);
        let tone_k = 0.6 + p.tone * 0.8;
        // noise fade-in (brush sweeps)
        if self.swell_g < 1.0 {
            self.swell_g = (self.swell_g + dt / self.swell_t.max(1e-4)).min(1.0);
        }
        let click_g = p.click * var.click;
        let out = match self.kind {
            Kind::Kick | Kind::Tom => {
                let mut o = head * (0.6 + body_w * 0.6);
                // beater click
                let ct = 0.004;
                if self.t < ct {
                    o += rng.next() * 0.5 * click_g * (0.4 + self.hard) * (1.0 - self.t / ct);
                }
                o
            }
            Kind::Snare => {
                self.wire_env *= (-6.9 * dt / self.wire_t60.max(0.01)).exp();
                let drive = head.abs().min(1.0) * 0.6 + 0.4;
                let side = var.flavor == Flavor::SideStick;
                let w = self.wires.tick(rng.next() * drive, (4200.0 + self.hard * 3000.0) * tone_k, 0.35, sr);
                let mut o = head * (0.5 + body_w * 0.5) + w * self.wire_env * self.swell_g * (0.4 + noise_w * 1.2) * var.noise;
                // stick crack
                if self.t < 0.003 {
                    o += rng.next() * 0.55 * click_g * (1.0 - self.t / 0.003);
                }
                if side {
                    // side stick: a short wooden tick, no wires
                    o = head * 0.6 + if self.t < 0.004 { rng.next() * 0.9 * click_g * (1.0 - self.t / 0.004) } else { 0.0 };
                }
                // analog (drum machine) snare: two tuned oscillators under the noise
                if p.analog > 0.0 {
                    let e1 = (-self.t * 6.9 / 0.11).exp();
                    let e2 = (-self.t * 6.9 / 0.07).exp();
                    self.tone_ph[0] = (self.tone_ph[0] + self.base * dt * (1.0 + 0.6 * (-self.t * 60.0).exp())).fract();
                    self.tone_ph[1] = (self.tone_ph[1] + self.base * 1.83 * dt).fract();
                    let t1 = (self.tone_ph[0] * TAU).sin() * e1 + (self.tone_ph[1] * TAU).sin() * e2 * 0.6;
                    o = o * (1.0 - p.analog * 0.35) + t1 * p.analog * 0.9;
                }
                o
            }
            Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride => {
                self.noise_env *= (-6.9 * dt / self.noise_t60.max(0.01)).exp();
                let n = self.noise_hp.tick(rng.next(), (6000.0 + self.hard * 3000.0) * tone_k, 0.2, sr);
                let plate = head * 0.9 * body_w.max(0.3) + n * (1.0 + noise_w * 1.4) * var.noise * self.swell_g;
                let mut o = plate;
                let tk = 2f32.powf((p.tune - 0.5) * 1.6) * 2f32.powf(var.semis / 12.0);
                if var.flavor == Flavor::Cowbell {
                    // two square waves through a band filter (808 cowbell)
                    self.sq_ph[0] = (self.sq_ph[0] + 540.0 * tk * dt).fract();
                    self.sq_ph[1] = (self.sq_ph[1] + 800.0 * tk * dt).fract();
                    let s = (if self.sq_ph[0] < 0.5 { 1.0 } else { -1.0 }) + (if self.sq_ph[1] < 0.5 { 1.0 } else { -1.0 });
                    let f = self.noise_bp.tick(s * 0.5, 1500.0 * tk * tone_k, 0.55, sr);
                    let fast = (-self.t * 6.9 / 0.04).exp();
                    o = f * (0.35 + 0.65 * fast) * 2.4 + head * 0.15;
                } else if p.analog > 0.0 {
                    let mut s = 0.0;
                    for (i, f) in SQUARES.iter().enumerate() {
                        self.sq_ph[i] = (self.sq_ph[i] + f * tk * 2.0 * dt).fract();
                        s += if self.sq_ph[i] < 0.5 { 1.0 } else { -1.0 };
                    }
                    let a = self.noise_hp.tick(s * 0.16, 7000.0 * tone_k, 0.3, sr) * 1.5;
                    o = plate * (1.0 - p.analog) + a * p.analog * 1.6 * self.swell_g;
                }
                if var.flavor == Flavor::Bell {
                    o = head * 1.6 + n * 0.25;
                }
                o * self.noise_env
            }
            Kind::Clap => {
                // three staggered bursts then a tail
                let t = self.t;
                let burst = |t0: f32| if t >= t0 && t < t0 + 0.012 { 1.0 - (t - t0) / 0.012 } else { 0.0 };
                let tail = (-((t - 0.03).max(0.0)) * 6.9 / self.noise_t60.max(0.05)).exp() * if t > 0.03 { 0.6 } else { 0.0 };
                let env = burst(0.0).max(burst(0.011)).max(burst(0.023)) * 0.8 + tail;
                self.wires.tick(rng.next(), (1100.0 + p.tune * 1800.0) * tone_k, 0.5, sr) * env * 2.0 * self.swell_g
            }
        };
        self.t += dt;
        // a hard gate (gated-reverb snares, tight metal kicks)
        let mut gate_g = 1.0;
        if self.gate_t > 0.0 {
            if self.t > self.gate_t + 0.015 {
                self.active = false;
                return 0.0;
            }
            if self.t > self.gate_t {
                gate_g = 1.0 - (self.t - self.gate_t) / 0.015;
            }
        }
        let limit = match self.kind {
            Kind::Crash => 4.0,
            Kind::Ride | Kind::OpenHat => 2.5,
            _ => 1.5,
        };
        if self.t > limit || (self.t > 0.05 && out.abs() < 1e-5 && self.modes.iter().all(|m| m.x.abs() + m.y.abs() < 1e-5) && self.noise_env < 1e-3 && self.wire_env < 1e-3) {
            self.active = false;
        }
        let d = self.p.drive;
        let y = out * self.vel * self.p.level * var.level * gate_g;
        if d > 0.0 {
            (y * (1.0 + d * 5.0)).tanh() / (1.0 + d * 1.5)
        } else {
            y.tanh()
        }
    }
}

/// Map GM drum keys to a piece + voicing.
pub fn variant_for(key: u8) -> Option<(Kind, Var)> {
    let n = Var::N;
    Some(match key {
        35 => (Kind::Kick, Var { semis: -1.0, decay: 0.85, level: 0.9, ..n }),
        36 => (Kind::Kick, n),
        37 => (Kind::Snare, Var { semis: 5.0, decay: 0.3, noise: 0.0, click: 2.0, level: 0.9, flavor: Flavor::SideStick, ..n }),
        38 => (Kind::Snare, n),
        40 => (Kind::Snare, Var { semis: 1.0, noise: 1.15, decay: 0.9, hard: 0.1, ..n }),
        39 => (Kind::Clap, n),
        41 | 43 | 45 | 47 | 48 | 50 => (Kind::Tom, Var { semis: (key as f32 - 45.0) * 1.3, ..n }),
        42 => (Kind::ClosedHat, n),
        44 => (Kind::ClosedHat, Var { decay: 0.6, level: 0.75, noise: 0.85, ..n }),
        46 => (Kind::OpenHat, n),
        49 => (Kind::Crash, n),
        57 => (Kind::Crash, Var { semis: 2.0, decay: 0.85, ..n }),
        55 => (Kind::Crash, Var { semis: 6.0, decay: 0.3, level: 0.9, ..n }),
        52 => (Kind::Crash, Var { semis: -1.0, decay: 0.55, noise: 1.4, hard: 0.15, ..n }),
        51 => (Kind::Ride, n),
        59 => (Kind::Ride, Var { semis: 1.0, decay: 0.9, ..n }),
        53 => (Kind::Ride, Var { semis: 5.0, decay: 0.5, flavor: Flavor::Bell, ..n }),
        54 => (Kind::ClosedHat, Var { semis: 4.0, decay: 1.8, noise: 1.3, hard: 0.1, ..n }), // tambourine
        69 => (Kind::ClosedHat, Var { semis: 6.0, decay: 0.8, noise: 1.2, level: 0.8, ..n }), // cabasa
        70 => (Kind::ClosedHat, Var { semis: 8.0, decay: 0.7, noise: 1.2, level: 0.7, ..n }), // maracas
        82 => (Kind::ClosedHat, Var { semis: 3.0, decay: 1.0, noise: 1.2, level: 0.8, ..n }), // shaker
        56 => (Kind::ClosedHat, Var { decay: 4.0, flavor: Flavor::Cowbell, level: 0.8, ..n }),
        58 => (Kind::OpenHat, Var { semis: -3.0, decay: 1.6, noise: 1.2, ..n }), // vibraslap
        60 => (Kind::Tom, Var { semis: 19.0, decay: 0.3, hard: 0.2, ..n }),
        61 => (Kind::Tom, Var { semis: 15.0, decay: 0.3, hard: 0.2, ..n }),
        62 => (Kind::Tom, Var { semis: 13.0, decay: 0.25, hard: 0.2, ..n }),
        63 => (Kind::Tom, Var { semis: 12.0, decay: 0.4, hard: 0.15, ..n }),
        64 => (Kind::Tom, Var { semis: 7.0, decay: 0.45, hard: 0.15, ..n }),
        65 => (Kind::Tom, Var { semis: 17.0, decay: 0.4, hard: 0.35, ..n }),
        66 => (Kind::Tom, Var { semis: 13.0, decay: 0.4, hard: 0.35, ..n }),
        67 => (Kind::ClosedHat, Var { semis: 5.0, decay: 3.0, flavor: Flavor::Cowbell, level: 0.7, ..n }),
        68 => (Kind::ClosedHat, Var { semis: 1.0, decay: 3.0, flavor: Flavor::Cowbell, level: 0.7, ..n }),
        71 | 72 => (Kind::OpenHat, Var { semis: 14.0, decay: 0.25, noise: 0.6, level: 0.6, ..n }),
        73 | 74 => (Kind::OpenHat, Var { semis: 4.0, decay: 0.3, noise: 1.1, level: 0.7, ..n }),
        75 => (Kind::Tom, Var { semis: 30.0, decay: 0.07, click: 1.5, hard: 0.4, level: 0.8, ..n }),
        76 | 77 => (Kind::Tom, Var { semis: 21.0, decay: 0.1, click: 1.5, hard: 0.3, level: 0.8, ..n }),
        78 | 79 => (Kind::Tom, Var { semis: 10.0, decay: 0.3, ..n }),
        80 | 81 => (Kind::Ride, Var { semis: 16.0, decay: 0.4, noise: 0.15, flavor: Flavor::Bell, level: 0.6, ..n }),
        _ => return None,
    })
}

pub struct DrumSynth {
    voices: Vec<DrumVoice>,
    pub params: [DrumParams; 8],
    sr: f32,
    rng: Rng,
    selected: usize,
    steal: usize,
}

pub const KIND_NAMES: [&str; 8] = ["Kick", "Snare", "Tom", "Closed Hat", "Open Hat", "Crash", "Ride", "Clap"];
const KINDS: [Kind; 8] = [Kind::Kick, Kind::Snare, Kind::Tom, Kind::ClosedHat, Kind::OpenHat, Kind::Crash, Kind::Ride, Kind::Clap];

impl DrumSynth {
    pub fn new(sr: f32) -> Self {
        let mut params: [DrumParams; 8] = std::array::from_fn(|_| DrumParams::new(0.9));
        params[0].tune = 0.45;
        params[1].body = 0.55;
        params[3].hardness = 0.8;
        // a little stage width
        params[2].pan = 0.42;
        params[3].pan = 0.62;
        params[5].pan = 0.35;
        params[6].pan = 0.66;
        DrumSynth { voices: (0..24).map(|_| DrumVoice::new(Kind::Kick)).collect(), params, sr, rng: Rng::new(99), selected: 0, steal: 0 }
    }
    pub fn select(&mut self, i: usize) {
        self.selected = i.min(7);
    }
    pub fn selected(&self) -> usize {
        self.selected
    }
    pub fn trigger(&mut self, kind: Kind, vel: f32) {
        self.trigger_var(kind, vel, Var::N);
    }
    fn trigger_var(&mut self, kind: Kind, vel: f32, var: Var) {
        let idx = KINDS.iter().position(|k| *k == kind).unwrap();
        // choke: closed hat cuts open hat
        if kind == Kind::ClosedHat && var.flavor == Flavor::Normal {
            for v in self.voices.iter_mut().filter(|v| v.kind == Kind::OpenHat && v.var.flavor == Flavor::Normal) {
                v.active = false;
            }
        }
        let slot = match self.voices.iter().position(|v| !v.active) {
            Some(s) => s,
            None => {
                self.steal = (self.steal + 1) % self.voices.len();
                self.steal
            }
        };
        let p = self.params[idx].clone();
        let sr = self.sr;
        // every hit is a little different (+-1.5 % pitch)
        let human = 1.0 + (self.rng.next() * 0.015);
        let v = &mut self.voices[slot];
        v.kind = kind;
        v.trigger(vel, sr, &p, var, human);
    }
}

impl Instrument for DrumSynth {
    fn note_on(&mut self, key: u8, vel: f32) {
        if let Some((k, var)) = variant_for(key) {
            self.trigger_var(k, vel, var);
        }
    }
    fn note_off(&mut self, _key: u8) {}
    fn all_off(&mut self) {
        for v in self.voices.iter_mut() {
            v.active = false;
        }
    }
    fn render(&mut self, out: &mut [f32]) {
        let sr = self.sr;
        for fr in out.chunks_exact_mut(2) {
            let (mut l, mut r) = (0.0, 0.0);
            for v in self.voices.iter_mut() {
                if !v.active {
                    continue;
                }
                let s = v.sample(sr, &mut self.rng);
                let ang = (v.pan + 1.0) * std::f32::consts::FRAC_PI_4;
                l += s * ang.cos();
                r += s * ang.sin();
            }
            // equal-power pan: centre = 0.707 per side, scaled so centred material matches the old mono mix
            fr[0] += l * 0.7;
            fr[1] += r * 0.7;
        }
    }
    fn params(&self) -> Vec<(&'static str, f32)> {
        self.params[self.selected].list().to_vec()
    }
    fn set_param(&mut self, n: &str, v: f32) {
        if !v.is_finite() {
            return;
        }
        let v = v.clamp(0.0, 1.0);
        self.params[self.selected].set(n, v);
    }
    fn name(&self) -> &'static str {
        "Ling Membrane"
    }
    fn state(&self) -> Vec<(String, f32)> {
        let mut out = vec![];
        for (i, name) in KIND_NAMES.iter().enumerate() {
            let key = name.to_lowercase().replace(' ', "_");
            for (n, v) in self.params[i].list() {
                out.push((format!("{key}.{n}"), v));
            }
        }
        out
    }
    fn load_state(&mut self, st: &[(String, f32)]) {
        let keep = self.selected;
        for (k, v) in st {
            if let Some((part, n)) = k.split_once('.') {
                if let Some(i) = KIND_NAMES.iter().position(|x| x.to_lowercase().replace(' ', "_") == part) {
                    self.selected = i;
                    self.set_param(n, *v);
                }
            }
        }
        self.selected = keep;
    }
    fn parts(&self) -> Vec<&'static str> {
        KIND_NAMES.to_vec()
    }
    fn part(&self) -> usize {
        self.selected
    }
    fn select_part(&mut self, i: usize) {
        self.select(i);
    }
}

← Back to index