L
ling-daw
↓ 0 downloadsLing DAW engine: synth, physical-modeling drums, effects, mixer, sequencer, project I/O
lingfu install ling-dawCode / 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);
}
}