From 19fb9ae79d80f82f72942a2c3f0af660ebcb3d0b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:40:47 +0000 Subject: [PATCH] feat: implement procedural drum synthesis based on DSP research - Updated all TR-808 and TR-909 drum models to support velocity scaling and per-hit micro-randomization. - Enhanced TR-909 Kick with a smoothing low-pass filter on the triangle body. - Implemented master saturation (DRIVE) in DrumMachine using a soft-clipping tanh approximation curve. - Configured "Drum Party" default Euclidean patterns: Kick (16/4), Snare (16/2 rot 4), Hi-Hat (16/12), Open Hat (16/2 rot 2), and Clap (16/2 rot 4). - Added master DRIVE control to DrumsView and fixed rotated pattern visualization. - Fixed hihatOpen output routing in audioStore for independent volume control. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/components/DrumsView.tsx | 50 ++++++++++++++++++++------------ src/components/SequencerLoop.tsx | 13 +++++---- src/logic/DrumMachine.ts | 24 ++++++++------- src/logic/drums/TR808Clap.ts | 8 ++--- src/logic/drums/TR808HiHat.ts | 4 +-- src/logic/drums/TR808Kick.ts | 4 +-- src/logic/drums/TR808Snare.ts | 6 ++-- src/logic/drums/TR909Kick.ts | 13 +++++---- src/logic/drums/TR909Snare.ts | 6 ++-- src/store/audioStore.ts | 5 +--- src/store/instrumentStore.ts | 4 ++- 11 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 2f322b33..e9c2dab8 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -4,11 +4,11 @@ import { useBassStore, useHarmonyStore } from '../store/instrumentStore' import { generateBassPattern } from '../logic/StingGenerator' import { Dices } from 'lucide-react' import { useAudioStore, AudioState } from '../store/audioStore' -import { bjorklund } from '../logic/bjorklund' +import { bjorklund, rotateArray } from '../logic/bjorklund' import { TransportControls } from './TransportControls' export function DrumsView() { - const { kick, snare, hihat, hihatOpen, clap, kit, setParams, setKit } = useDrumStore() + const { kick, snare, hihat, hihatOpen, clap, kit, saturation, setParams, setKit } = useDrumStore() const { drumMachine, volumes, setVolume } = useAudioStore() const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', params: any) => { @@ -25,6 +25,11 @@ export function DrumsView() { if (drumMachine) drumMachine.setKit(newKit) } + const handleSaturationChange = (v: number) => { + useDrumStore.setState({ saturation: v }) + if (drumMachine) drumMachine.setSaturation(v) + } + return (
@@ -32,21 +37,30 @@ export function DrumsView() {

Настройки

-
- {(['808', '909'] as const).map(k => ( - - ))} +
+ +
+ {(['808', '909'] as const).map(k => ( + + ))} +
@@ -101,7 +115,7 @@ export function DrumsView() { { name: 'OPEN', data: hihatOpen }, { name: 'CLAP', data: clap } ].map((d, idx) => { - const pattern = bjorklund(d.data.steps, d.data.pulses) + const pattern = rotateArray(bjorklund(d.data.steps, d.data.pulses), d.data.rotate) return (
{d.name}
diff --git a/src/components/SequencerLoop.tsx b/src/components/SequencerLoop.tsx index bd7a08e7..e775c105 100644 --- a/src/components/SequencerLoop.tsx +++ b/src/components/SequencerLoop.tsx @@ -63,13 +63,14 @@ export function SequencerLoop() { const currentHarmony = useHarmonyStore.getState() const currentPads = usePadStore.getState() - // 1. Drums (Euclidean - using cached patterns) + // 1. Drums (Euclidean - using cached patterns) with velocity randomization const patterns = drumPatternsRef.current - if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time) - if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time) - if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time) - if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time) - if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time) + const getVel = () => 0.7 + Math.random() * 0.3 + if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time, getVel()) + if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time, getVel()) + if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time, getVel()) + if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time, getVel()) + if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time, getVel()) // 2. Bass (Sting logic) const bassStep = currentBass.pattern[step] diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index 47df2ded..8f29936d 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -93,6 +93,10 @@ export class DrumMachine { this.currentKit = kit } + setSaturation(amount: number) { + this.shaper.curve = this.makeDistortionCurve(amount) + } + setDrumParams(drum: string, pitch: number, decay: number) { this.params[drum] = { pitch, decay } } @@ -104,19 +108,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..aef1b106 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"); @@ -48,7 +48,7 @@ export class TR808HiHat { const decayTime = decayBase * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay // 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 fd15b475..450a3bf9 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 @@ -31,7 +31,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 + finalDecay); osc.start(time).stop(time + finalDecay); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 861e411a..42c62c3c 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; @@ -40,7 +40,7 @@ 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 + vcaDecay); @@ -54,7 +54,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..6acd44be 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -13,7 +13,7 @@ export class TR909Kick { } } - 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; // decay: 0.5 -> 0.45s, maps to 0.3-0.6s @@ -24,12 +24,14 @@ 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 + // 909 Kick Body: Triangle Oscillator with smoothing LPF const bodyOsc = new Tone.Oscillator(tune * 4.7 + drift, "triangle"); bodyOsc.phase = Math.random() * 360; + const bodyFilter = new Tone.Filter(1000, "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 @@ -40,7 +42,7 @@ export class TR909Kick { 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 + vcaDecay); // Click Layer (Noise) @@ -54,7 +56,7 @@ export class TR909Kick { // Ultra short envelope (10-20ms) for the click 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 +64,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..63694c1a 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; @@ -43,7 +43,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 +58,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); diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index 9e51b8bb..d9ee951c 100644 --- a/src/store/audioStore.ts +++ b/src/store/audioStore.ts @@ -93,10 +93,7 @@ export const useAudioStore = create((set, get) => ({ if (channel === 'kick') drumMachine.outputKick.gain.value = value if (channel === 'snare') drumMachine.outputSnare.gain.value = value if (channel === 'hihat') drumMachine.outputHihat.gain.value = value - // We use hihat output for both open and closed for now in TR808HiHat as it shares a node, - // but we can scale the volume parameter independently here if we had separate synths. - // For now, we'll map openHat to hihat channel, and clap to clap. - if (channel === 'hihatOpen') drumMachine.outputHihat.gain.value = value // Shared + if (channel === 'hihatOpen') drumMachine.outputOpenHat.gain.value = value if (channel === 'clap') drumMachine.outputClap.gain.value = value } diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 07c1c23a..e700c4c1 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -27,6 +27,7 @@ interface DrumState { hihatOpen: { steps: number, pulses: number, rotate: number, decay: number, pitch: number } clap: { steps: number, pulses: number, rotate: number, decay: number, pitch: number } kit: '808' | '909' + saturation: number setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', params: Partial<{ steps: number, pulses: number, rotate: number, decay: number, pitch: number }>) => void setKit: (kit: '808' | '909') => void } @@ -36,8 +37,9 @@ export const useDrumStore = create((set) => ({ 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 }, + clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 }, kit: '909', + saturation: 15, setParams: (drum, params) => set((state) => ({ [drum]: { ...state[drum], ...params } })),