From 3eeb8ec21101fd6c9a05110807fd5e2d6aefd071 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:42:29 +0000 Subject: [PATCH] Implement TR-808/909 drum synthesis refinements based on DSP research - Refined TR-909 Kick with LowPass smoothing filter and variable pitch envelope decay - Updated TR-808 Snare with average resonance frequencies (180Hz/330Hz) - Added saturation (tanh) to TR-909 Snare tonal body - Implemented velocity scaling across all TR-808 and TR-909 drum models - Configured professional default Euclidean patterns for a 'drum party' experience - Unified drum machine trigger logic to handle dynamic velocity - Fixed redundant scaling and initialization issues identified in code review - Verified via TypeScript compilation and frontend visual checks Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/logic/DrumMachine.ts | 20 ++++++++++---------- src/logic/drums/TR808Clap.ts | 8 ++++---- src/logic/drums/TR808HiHat.ts | 6 +++--- src/logic/drums/TR808Kick.ts | 6 +++--- src/logic/drums/TR808Snare.ts | 13 +++++++------ src/logic/drums/TR909Kick.ts | 29 +++++++++++++++++------------ src/logic/drums/TR909Snare.ts | 15 ++++++++++----- src/store/instrumentStore.ts | 6 +++++- 8 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index 47df2ded..4bbe4d31 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -104,19 +104,19 @@ 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.hihatOpen.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.hihatOpen.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 'hihatOpen': kit909.hihatOpen.trigger(time, true, p.pitch, p.decay); break - case 'clap': kit909.clap.trigger(time, 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': kit909.hihatOpen.trigger(time, true, p.pitch, p.decay, velocity); 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 989f3c88..1c8fcedc 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -10,7 +10,7 @@ export class TR808Clap { 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) { const noiseSrc = new Tone.BufferSource(this.noiseBuffer); const filterVariance = 1 + (Math.random() * 0.04 - 0.02); // +/- 2% filter const bpf = new Tone.Filter((1000 + pitch * 1000) * filterVariance, "bandpass"); @@ -26,8 +26,8 @@ export class TR808Clap { 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 @@ -35,7 +35,7 @@ export class TR808Clap { const decayTimeBase = 0.1 + decay * 0.5; const decayTime = decayTimeBase * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay - gain.gain.setValueAtTime(1, finalDecayStart); + 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 01f3854c..efeaa1db 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"); @@ -47,8 +47,8 @@ export class TR808HiHat { const decayBase = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02); const decayTime = decayBase * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay - // VCA Envelope - envGain.gain.setValueAtTime(1, time); + // VCA Envelope with velocity scaling + 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 fd15b475..1f243dce 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -3,7 +3,7 @@ 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; // decay: 0.5 -> 1.7s, maps to 0.4-3.0s range @@ -30,8 +30,8 @@ export class TR808Kick { osc.frequency.setValueAtTime(startFreq, time); osc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchDropTime); - // VCA Amp Envelope: Instant attack, adjustable exponential decay - masterGain.gain.setValueAtTime(1, time); + // VCA Amp Envelope: Instant attack, adjustable exponential decay, scaled by velocity + masterGain.gain.setValueAtTime(velocity, time); masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); osc.start(time).stop(time + finalDecay); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 861e411a..0883b6e9 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -13,7 +13,7 @@ export class TR808Snare { } } - 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; @@ -24,9 +24,9 @@ export class TR808Snare { const snappyDecay = snappyDecayBase * (1 + (Math.random() * 0.04 - 0.02)); const filterVariance = 1 + (Math.random() * 0.04 - 0.02); - // 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research - const oscLow = new Tone.Oscillator(238 + drift, "sine"); - const oscHigh = new Tone.Oscillator(476 + drift, "sine"); + // 808 Membrane modes: 180Hz and 330Hz as per research average + const oscLow = new Tone.Oscillator(180 + drift, "sine"); + const oscHigh = new Tone.Oscillator(330 + drift, "sine"); oscLow.phase = Math.random() * 360; oscHigh.phase = Math.random() * 360; @@ -40,7 +40,8 @@ export class TR808Snare { gainHigh.connect(masterTonalGain); masterTonalGain.connect(this.destination); - masterTonalGain.gain.setValueAtTime(1, time); + // Apply velocity once at master gain level + masterTonalGain.gain.setValueAtTime(velocity, time); // Tonal body decay is short (~200ms) masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); @@ -54,7 +55,7 @@ export class TR808Snare { noiseFilter.connect(snappyGain); snappyGain.connect(this.destination); - 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 + vcaDecay); diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index 0a44f1ad..1c7c0376 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -13,10 +13,12 @@ export class TR909Kick { } } - trigger(time: number, pitch: number, decay: number) { - // pitch: 0.5 -> 50Hz, maps to 45-55Hz + trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + // Research: Tune controls the pitch envelope decay time (0.05s - 0.15s) + const pitchEnvDecay = 0.05 + pitch * 0.1; + // Final frequency is relatively stable for 909 (45Hz - 55Hz) const tune = 45 + pitch * 10; - // decay: 0.5 -> 0.45s, maps to 0.3-0.6s + // Amp decay: 0.5 -> 0.45s, maps to 0.3-0.6s const decayTime = 0.3 + decay * 0.3; // Micro-randomization @@ -24,23 +26,25 @@ export class TR909Kick { const vcaDecay = decayTime * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay const filterVariance = 1 + (Math.random() * 0.04 - 0.02); // +/- 2% filter - // 909 Kick Body: Triangle Oscillator - const bodyOsc = new Tone.Oscillator(tune * 4.7 + drift, "triangle"); + // 909 Kick Body: Triangle Oscillator + Smoothing LowPass Filter + const bodyOsc = new Tone.Oscillator(0, "triangle"); // Freq set via envelope bodyOsc.phase = Math.random() * 360; + const bodyFilter = new Tone.Filter(1000 * filterVariance, "lowpass"); const bodyGain = new Tone.Gain(0); - bodyOsc.connect(bodyGain); + bodyOsc.connect(bodyFilter); + bodyFilter.connect(bodyGain); bodyGain.connect(this.destination); - // Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over 100ms + // Aggressive Pitch Envelope: Start at Tune * 4.7 and drop over variable duration (from Tune knob) const startFreq = tune * 4.7 + drift; const endFreq = tune + drift; bodyOsc.frequency.setValueAtTime(startFreq, time); - bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1); + bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchEnvDecay); - // VCA Envelope - bodyGain.gain.setValueAtTime(1, time); + // VCA Envelope with velocity scaling + bodyGain.gain.setValueAtTime(velocity, time); bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); // Click Layer (Noise) @@ -52,9 +56,9 @@ export class TR909Kick { noiseFilter.connect(noiseGain); noiseGain.connect(this.destination); - // Ultra short envelope (10-20ms) for the click + // Ultra short envelope (10-20ms) for the click, also velocity scaled const clickDecay = 0.02 * (1 + (Math.random() * 0.04 - 0.02)); - noiseGain.gain.setValueAtTime(0.7, time); + noiseGain.gain.setValueAtTime(0.7 * velocity, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay); bodyOsc.start(time).stop(time + vcaDecay); @@ -62,6 +66,7 @@ export class TR909Kick { bodyOsc.onstop = () => { bodyOsc.dispose(); + bodyFilter.dispose(); bodyGain.dispose(); }; noiseSrc.onended = () => { diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 8f77f539..01dd69ef 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -14,7 +14,7 @@ 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; @@ -30,10 +30,14 @@ export class TR909Snare { const osc2 = new Tone.Oscillator(freq2 * 2 + drift, "triangle"); osc1.phase = Math.random() * 360; osc2.phase = Math.random() * 360; + + // Slight saturation/waveshaping for tonal body + const shaper = new Tone.WaveShaper((x) => Math.tanh(x * 1.5)); const tonalGain = new Tone.Gain(0); - osc1.connect(tonalGain); - osc2.connect(tonalGain); + osc1.connect(shaper); + osc2.connect(shaper); + shaper.connect(tonalGain); tonalGain.connect(this.destination); // 2x Pitch Sweep over 30ms (research says ~30ms for 909 Snare) @@ -43,7 +47,7 @@ export class TR909Snare { osc2.frequency.setValueAtTime(freq2 * 2 + drift, time); osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepTime); - tonalGain.gain.setValueAtTime(1, time); + tonalGain.gain.setValueAtTime(velocity, time); tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); // Snappy Layer @@ -58,7 +62,7 @@ export class TR909Snare { lpf.connect(noiseGain); noiseGain.connect(this.destination); - 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 + vcaDecay); @@ -68,6 +72,7 @@ export class TR909Snare { osc1.onstop = () => { osc1.dispose(); osc2.dispose(); + shaper.dispose(); tonalGain.dispose(); }; noiseSrc.onended = () => { diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 07c1c23a..252219b3 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -32,11 +32,15 @@ interface DrumState { } export const useDrumStore = create((set) => ({ + // Kick: 4-on-the-floor kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5 }, + // Snare/Clap: Backbeat (2 and 4), rotated by 4 steps (16/2 rot 4) snare: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 }, + clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 }, + // Hi-hat: 16th note pattern hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5 }, + // Open Hi-hat: Off-beat (rotated by 2) 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: '909', setParams: (drum, params) => set((state) => ({ [drum]: { ...state[drum], ...params }