From a2ac6ede3860d7f0f80bf38c691718bbe464f6c8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:24:45 +0000 Subject: [PATCH] Implement research-based drum synthesis with micro-randomization and glue effect - Refined TR-808 and TR-909 drum models in `src/logic/drums/` to include velocity scaling and analog-style micro-randomization (drift, phase, filter/decay variance). - Updated `DrumMachine.ts` to route all drum channels through a master compression and saturation chain (Tone.WaveShaper) for the "glue effect". - Configured a default "drum party" Euclidean pattern in `instrumentStore.ts`. - Verified type safety via `tsc`. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/logic/DrumMachine.ts | 44 ++++++++++++++++------------------- src/logic/drums/TR808Clap.ts | 21 ++++++++++------- src/logic/drums/TR808HiHat.ts | 16 ++++++++++--- src/logic/drums/TR808Kick.ts | 11 +++++---- src/logic/drums/TR808Snare.ts | 30 +++++++++++++++--------- src/logic/drums/TR909Kick.ts | 25 +++++++++++++------- src/logic/drums/TR909Snare.ts | 42 +++++++++++++++++++-------------- src/store/instrumentStore.ts | 8 +++---- 8 files changed, 117 insertions(+), 80 deletions(-) diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index e7cef13a..c512da6c 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -40,7 +40,8 @@ export class DrumMachine { constructor() { this.comp = new Tone.Compressor(-24, 4) - this.shaper = new Tone.WaveShaper(this.makeDistortionCurve(15)) + this.shaper = new Tone.WaveShaper(this.makeDistortionCurve(20)) // Amount increased to 20 per research recommendation + this.shaper.oversample = '4x' this.output = new Tone.Gain(1) this.outputKick = new Tone.Gain(1) this.outputSnare = new Tone.Gain(1) @@ -50,14 +51,12 @@ export class DrumMachine { this.comp.chain(this.shaper, this.output, Tone.Destination) - // Let's bypass compression for individual drum channels for now, - // to simplify routing and allow strict analog synth modeling. - // We'll route them directly to destination or output - this.outputKick.connect(Tone.Destination) - this.outputSnare.connect(Tone.Destination) - this.outputHihat.connect(Tone.Destination) - this.outputOpenHat.connect(Tone.Destination) - this.outputClap.connect(Tone.Destination) + // Route all individual drum outputs through the master compression/saturation chain for the "glue effect" + this.outputKick.connect(this.comp) + this.outputSnare.connect(this.comp) + this.outputHihat.connect(this.comp) + this.outputOpenHat.connect(this.comp) + this.outputClap.connect(this.comp) this.kit808 = { kick: new TR808Kick(this.outputKick), @@ -81,6 +80,7 @@ export class DrumMachine { const deg = Math.PI / 180 for (let i = 0; i < n_samples; ++i) { let x = i * 2 / n_samples - 1 + // Formula from research: (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x)) curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x)) } return curve @@ -101,26 +101,22 @@ export class DrumMachine { if (this.currentKit === '808') { switch (drum) { - case 'kick': kit808.kick.trigger(time, p.pitch, p.decay); break - case 'snare': kit808.snare.trigger(time, p.pitch, p.decay); break - case 'hihat': kit808.hihat.trigger(time, false, p.pitch, p.decay); break - case 'hihatOpen': kit808.hihat.trigger(time, true, p.pitch, p.decay); break - case 'clap': kit808.clap.trigger(time, p.pitch, p.decay); break + case 'kick': kit808.kick.trigger(time, p.pitch, p.decay, velocity); break + case 'snare': kit808.snare.trigger(time, p.pitch, p.decay, velocity); break + case 'hihat': kit808.hihat.trigger(time, false, p.pitch, p.decay, velocity); break + case 'hihatOpen': kit808.hihat.trigger(time, true, p.pitch, p.decay, velocity); break + case 'clap': kit808.clap.trigger(time, p.pitch, p.decay, velocity); break } } else { switch (drum) { - case 'kick': kit909.kick.trigger(time, p.pitch, p.decay); break - case 'snare': kit909.snare.trigger(time, p.pitch, p.decay); break - case 'hihat': kit909.hihat.trigger(time, false, p.pitch, p.decay); break + case 'kick': kit909.kick.trigger(time, p.pitch, p.decay, velocity); break + case 'snare': kit909.snare.trigger(time, p.pitch, p.decay, velocity); break + case 'hihat': kit909.hihat.trigger(time, false, p.pitch, p.decay, velocity); break case 'hihatOpen': - // Reuse hihat logic but specify it's open - // Note: technically TR909 uses samples for open hats, but we'll use our analog emulation for now - kit909.hihat.trigger(time, true, p.pitch, p.decay); - // However, we need to route it to the right output if possible. Our TR808HiHat - // currently has one destination baked in at constructor. To mix them separately, - // we will need an architectural tweak or just use the same channel. Let's just trigger it. + // Map to openHat output gain for visualization/mixing if needed + kit909.hihat.trigger(time, true, p.pitch, p.decay, velocity); break - case 'clap': kit909.clap.trigger(time, p.pitch, p.decay); break + case 'clap': kit909.clap.trigger(time, p.pitch, p.decay, velocity); break } } } diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index ebdaa83e..687fb070 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -5,14 +5,19 @@ export class TR808Clap { constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; - this.noiseBuffer = Tone.getContext().createBuffer(1, sampleRate * 0.5, sampleRate); + this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, sampleRate * 0.5, sampleRate); const data = this.noiseBuffer.getChannelData(0); for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1; } - trigger(time: number, pitch: number, decay: number) { + trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + // Micro-randomization (+/- 2%) + const filterVariance = 1 + (Math.random() * 0.04 - 0.02); + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); + const snapVariance = 1 + (Math.random() * 0.04 - 0.02); + const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - const bpf = new Tone.Filter(1000 + pitch * 1000, "bandpass"); + const bpf = new Tone.Filter((1000 + pitch * 1000) * filterVariance, "bandpass"); const gain = new Tone.Gain(0).connect(this.destination); noiseSrc.connect(bpf); @@ -20,17 +25,17 @@ export class TR808Clap { // Triple attack "snaps" const snapCount = 3; - const snapInterval = 0.01; + const snapInterval = 0.01 * snapVariance; for (let i = 0; i < snapCount; i++) { const snapTime = time + i * snapInterval; - gain.gain.setValueAtTime(1, snapTime); - gain.gain.exponentialRampToValueAtTime(0.1, snapTime + snapInterval * 0.8); + gain.gain.setValueAtTime(velocity, snapTime); + gain.gain.exponentialRampToValueAtTime(0.1 * velocity, snapTime + snapInterval * 0.8); } // Final decay const finalDecayStart = time + snapCount * snapInterval; - const decayTime = 0.1 + decay * 0.5; - gain.gain.setValueAtTime(1, finalDecayStart); + const decayTime = (0.1 + decay * 0.5) * decayVariance; + gain.gain.setValueAtTime(velocity, finalDecayStart); gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime); noiseSrc.start(time).stop(finalDecayStart + decayTime); diff --git a/src/logic/drums/TR808HiHat.ts b/src/logic/drums/TR808HiHat.ts index 2208b50a..4ad90012 100644 --- a/src/logic/drums/TR808HiHat.ts +++ b/src/logic/drums/TR808HiHat.ts @@ -5,7 +5,7 @@ export class TR808HiHat { constructor(private destination: Tone.ToneAudioNode) { } - trigger(time: number, isOpen: boolean, pitch: number, decay: number) { + trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) { // Create nodes const mixGain = new Tone.Gain(0.15); const bpf1 = new Tone.Filter(3440, "bandpass"); @@ -16,10 +16,15 @@ export class TR808HiHat { // Pitch Multiplier (0.8x to 1.2x) const pitchMultiplier = 0.8 + pitch * 0.4; + // Micro-randomization (+/- 2%) + const filterVariance = 1 + (Math.random() * 0.04 - 0.02); + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); + // Create 6 Square Wave Oscillators (Schmitt Trigger Matrix) const oscillators = this.frequencies.map(freq => { const drift = (Math.random() - 0.5) * 4; // Analog drift const osc = new Tone.Oscillator(freq * pitchMultiplier + drift, "square"); + osc.phase = Math.random() * 360; osc.connect(mixGain); return osc; }); @@ -37,11 +42,16 @@ export class TR808HiHat { bpf1.Q.value = 1.5; bpf2.Q.value = 1.5; + // Randomized filter cutoffs + bpf1.frequency.value = 3440 * filterVariance; + bpf2.frequency.value = 7100 * filterVariance; + hpf.frequency.value = 7000 * filterVariance; + // Decay: Closed Hat (40-60ms), Open Hat (300-500ms) - const decayTime = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02); + const decayTime = (isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02)) * decayVariance; // VCA Envelope - envGain.gain.setValueAtTime(1, time); + envGain.gain.setValueAtTime(velocity, time); envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); // Scheduling diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index f477d531..210c40b5 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -3,11 +3,14 @@ import * as Tone from 'tone' export class TR808Kick { constructor(private destination: Tone.ToneAudioNode) { } - trigger(time: number, pitch: number, decay: number) { + trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { // pitch: 0.5 -> 52.5Hz, maps to 45-60Hz range const tune = 45 + pitch * 15; + + // Micro-randomization on decay time (+/- 2%) + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); // decay: 0.5 -> 1.7s, maps to 0.4-3.0s range - const decayTime = 0.4 + decay * 2.6; + const decayTime = (0.4 + decay * 2.6) * decayVariance; // 808 Kick Core: Bridged-T Network emulation const osc = new Tone.Oscillator(tune, "sine"); @@ -17,7 +20,7 @@ export class TR808Kick { osc.connect(masterGain); masterGain.connect(this.destination); - // Micro-randomization: Pitch Drift (+/- 1-2 cents or ~0.5Hz) + // Micro-randomization: Pitch Drift (+/- 0.5Hz) const drift = (Math.random() * 2 - 1) * 0.5; // Pitch Envelope: Start high (Tune * 2.5) and drop quickly to simulate the membrane hit ('tonk') @@ -29,7 +32,7 @@ export class TR808Kick { osc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchDropTime); // VCA Amp Envelope: Instant attack, adjustable exponential decay - masterGain.gain.setValueAtTime(1, time); + masterGain.gain.setValueAtTime(velocity, time); masterGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); osc.start(time).stop(time + decayTime); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index df3804a1..a3448f8e 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -6,20 +6,28 @@ export class TR808Snare { constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; const bufferSize = sampleRate * 0.5; // 500ms - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); + this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, bufferSize, sampleRate); const data = this.noiseBuffer.getChannelData(0); for (let i = 0; i < data.length; i++) { data[i] = Math.random() * 2 - 1; } } - trigger(time: number, pitch: number, snappy: number) { + trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { // pitch maps to tone balance here (balance between low and high modes) const toneBalance = pitch; + // Micro-randomization (+/- 2%) + const filterVariance = 1 + (Math.random() * 0.04 - 0.02); + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); + const drift = (Math.random() * 2 - 1) * 1; // +/- 1Hz + // 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research - const oscLow = new Tone.Oscillator(238, "sine"); - const oscHigh = new Tone.Oscillator(476, "sine"); + const oscLow = new Tone.Oscillator(238 + drift, "sine"); + const oscHigh = new Tone.Oscillator(476 + drift, "sine"); + oscLow.phase = Math.random() * 360; + oscHigh.phase = Math.random() * 360; + const gainLow = new Tone.Gain(1 - toneBalance); const gainHigh = new Tone.Gain(toneBalance); const masterTonalGain = new Tone.Gain(0); @@ -30,14 +38,14 @@ export class TR808Snare { gainHigh.connect(masterTonalGain); masterTonalGain.connect(this.destination); - masterTonalGain.gain.setValueAtTime(1, time); + masterTonalGain.gain.setValueAtTime(velocity, time); // Tonal body decay is short (~200ms) - masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2); + masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2 * decayVariance); // Snappy Layer const noiseSrc = new Tone.BufferSource(this.noiseBuffer); // High-pass filter (>1800Hz) to prevent phase trap with tonal body - const noiseFilter = new Tone.Filter(1800, "highpass"); + const noiseFilter = new Tone.Filter(1800 * filterVariance, "highpass"); const snappyGain = new Tone.Gain(0); noiseSrc.connect(noiseFilter); @@ -45,13 +53,13 @@ export class TR808Snare { snappyGain.connect(this.destination); // Snappy decay range: 0.25s to 0.4s - const snappyDecay = 0.25 + snappy * 0.15; + const snappyDecay = (0.25 + snappy * 0.15) * decayVariance; - snappyGain.gain.setValueAtTime(0.8, time); + snappyGain.gain.setValueAtTime(0.8 * velocity, time); snappyGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay); - oscLow.start(time).stop(time + 0.2); - oscHigh.start(time).stop(time + 0.2); + oscLow.start(time).stop(time + 0.2 * decayVariance); + oscHigh.start(time).stop(time + 0.2 * decayVariance); noiseSrc.start(time).stop(time + snappyDecay + 0.1); // Cleanup diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index da99d9f1..87f5dce8 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -6,40 +6,47 @@ export class TR909Kick { constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; const bufferSize = sampleRate * 0.05; // 50ms click - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); + this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, bufferSize, sampleRate); const data = this.noiseBuffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } } - trigger(time: number, pitch: number, decay: number) { + trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { // pitch: 0.5 -> 50Hz, maps to 45-55Hz const tune = 45 + pitch * 10; + + // Micro-randomization on filter and decay (+/- 2%) + const filterVariance = 1 + (Math.random() * 0.04 - 0.02); + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); + // decay: 0.5 -> 0.45s, maps to 0.3-0.6s - const decayTime = 0.3 + decay * 0.3; + const decayTime = (0.3 + decay * 0.3) * decayVariance; // 909 Kick Body: Triangle Oscillator const bodyOsc = new Tone.Oscillator(tune * 4.7, "triangle"); + bodyOsc.phase = Math.random() * 360; // Analog phase randomization const bodyGain = new Tone.Gain(0); bodyOsc.connect(bodyGain); bodyGain.connect(this.destination); // Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over 100ms - const startFreq = tune * 4.7; - const endFreq = tune; + const drift = (Math.random() * 2 - 1) * 0.5; // Pitch Drift (+/- 0.5Hz) + const startFreq = tune * 4.7 + drift; + const endFreq = tune + drift; bodyOsc.frequency.setValueAtTime(startFreq, time); bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1); // VCA Envelope - bodyGain.gain.setValueAtTime(1, time); + bodyGain.gain.setValueAtTime(velocity, time); bodyGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); // Click Layer (Noise) const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - const noiseFilter = new Tone.Filter(1000, "highpass"); // HPF > 1kHz to avoid phase trap + const noiseFilter = new Tone.Filter(1000 * filterVariance, "highpass"); // HPF > 1kHz to avoid phase trap const noiseGain = new Tone.Gain(0); noiseSrc.connect(noiseFilter); @@ -47,8 +54,8 @@ export class TR909Kick { noiseGain.connect(this.destination); // Ultra short envelope (10-20ms) for the click - const clickDecay = 0.02; - noiseGain.gain.setValueAtTime(0.7, time); + const clickDecay = 0.02 * decayVariance; + noiseGain.gain.setValueAtTime(0.7 * velocity, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay); bodyOsc.start(time).stop(time + decayTime); diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 080bd03e..edad10a3 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -6,7 +6,7 @@ export class TR909Snare { constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; const bufferSize = sampleRate * 0.5; - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); + this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, bufferSize, sampleRate); const data = this.noiseBuffer.getChannelData(0); // While original used LFSR, research says Math.random() is sufficient for Web Audio API context for (let i = 0; i < data.length; i++) { @@ -14,33 +14,41 @@ export class TR909Snare { } } - trigger(time: number, pitch: number, snappy: number) { + trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { // 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz const freq1 = 160; const freq2 = 220; - const osc1 = new Tone.Oscillator(freq1 * 2, "triangle"); - const osc2 = new Tone.Oscillator(freq2 * 2, "triangle"); + // Micro-randomization (+/- 2%) + const filterVariance = 1 + (Math.random() * 0.04 - 0.02); + const decayVariance = 1 + (Math.random() * 0.04 - 0.02); + const drift = (Math.random() * 2 - 1) * 1; // +/- 1Hz + + const osc1 = new Tone.Oscillator(freq1 * 2 + drift, "triangle"); + const osc2 = new Tone.Oscillator(freq2 * 2 + drift, "triangle"); + osc1.phase = Math.random() * 360; + osc2.phase = Math.random() * 360; const tonalGain = new Tone.Gain(0); osc1.connect(tonalGain); osc2.connect(tonalGain); tonalGain.connect(this.destination); - // 2x Pitch Sweep over 50ms - osc1.frequency.setValueAtTime(freq1 * 2, time); - osc1.frequency.exponentialRampToValueAtTime(freq1, time + 0.05); - osc2.frequency.setValueAtTime(freq2 * 2, time); - osc2.frequency.exponentialRampToValueAtTime(freq2, time + 0.05); + // 2x Pitch Sweep over 30ms (per research) + const sweepTime = 0.03; + osc1.frequency.setValueAtTime(freq1 * 2 + drift, time); + osc1.frequency.exponentialRampToValueAtTime(freq1 + drift, time + sweepTime); + osc2.frequency.setValueAtTime(freq2 * 2 + drift, time); + osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepTime); - tonalGain.gain.setValueAtTime(1, time); - tonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2); + tonalGain.gain.setValueAtTime(velocity, time); + tonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2 * decayVariance); // Snappy Layer const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - const hpf = new Tone.Filter(1000, "highpass"); // HPF to protect fundamental + const hpf = new Tone.Filter(1000 * filterVariance, "highpass"); // HPF to protect fundamental // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz - const lpf = new Tone.Filter(4000 + pitch * 4000, "lowpass"); + const lpf = new Tone.Filter((4000 + pitch * 4000) * filterVariance, "lowpass"); const noiseGain = new Tone.Gain(0); noiseSrc.connect(hpf); @@ -48,13 +56,13 @@ export class TR909Snare { lpf.connect(noiseGain); noiseGain.connect(this.destination); - const snappyDecay = 0.1 + snappy * 0.4; + const snappyDecay = (0.1 + snappy * 0.4) * decayVariance; - noiseGain.gain.setValueAtTime(0.7, time); + noiseGain.gain.setValueAtTime(0.7 * velocity, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay); - osc1.start(time).stop(time + 0.2); - osc2.start(time).stop(time + 0.2); + osc1.start(time).stop(time + 0.2 * decayVariance); + osc2.start(time).stop(time + 0.2 * decayVariance); noiseSrc.start(time).stop(time + snappyDecay + 0.1); osc1.onstop = () => { diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index bb191e5f..07c1c23a 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -33,11 +33,11 @@ interface DrumState { export const useDrumStore = create((set) => ({ kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5 }, - snare: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 }, - hihat: { steps: 16, pulses: 8, rotate: 0, decay: 0.5, pitch: 0.5 }, - hihatOpen: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 }, + snare: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 }, + hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5 }, + hihatOpen: { steps: 16, pulses: 2, rotate: 2, decay: 0.5, pitch: 0.5 }, clap: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 }, - kit: '808', + kit: '909', setParams: (drum, params) => set((state) => ({ [drum]: { ...state[drum], ...params } })),