From d842bd1aa51e715a1cbac94347fefa4ac0675065 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:33:59 +0000 Subject: [PATCH] feat: implement procedural drum synthesis based on DSP research - Implemented authentic TR-808 and TR-909 synthesis models for Kick, Snare, Hi-Hats, Clap, and Cowbell. - Added 15-bit LFSR noise and white noise generators in `DrumUtils.ts`. - Implemented two-stage amplitude envelopes and pitch sweeps as per research specs. - Added micro-randomization (Analog Drift) to oscillators and filters. - Integrated soft-clipping saturation (WaveShaper) for analog warmth. - Added a randomization (Dice) feature in `DrumsView.tsx` with Telegram haptic feedback. - Added downbeat haptic feedback in `SequencerLoop.tsx`. - Ensured robust node disposal and parameter synchronization. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/components/DrumsView.tsx | 39 +++++++++++++++++++++++++++++--- src/components/SequencerLoop.tsx | 5 ++++ src/logic/DrumMachine.ts | 11 +++++++++ src/logic/DrumUtils.ts | 39 ++++++++++++++++++++++++++++++++ src/logic/drums/TR808Clap.ts | 21 ++++++++++------- src/logic/drums/TR808Cowbell.ts | 22 ++++++++++-------- src/logic/drums/TR808HiHat.ts | 25 +++++++++++--------- src/logic/drums/TR808Kick.ts | 19 ++++++++++++---- src/logic/drums/TR808Snare.ts | 26 +++++++++------------ src/logic/drums/TR909Kick.ts | 33 ++++++++++++--------------- src/logic/drums/TR909Snare.ts | 24 ++++++++------------ src/store/audioStore.ts | 12 ++++++++++ src/store/instrumentStore.ts | 20 ++++++++++++---- 13 files changed, 210 insertions(+), 86 deletions(-) diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..133ada60 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -1,14 +1,12 @@ import { useDrumStore, DrumParams } from '../store/instrumentStore' import { Knob } from './Knob' -import { useBassStore, useHarmonyStore } from '../store/instrumentStore' -import { generateBassPattern } from '../logic/StingGenerator' import { Dices } from 'lucide-react' import { useAudioStore, AudioState } from '../store/audioStore' import { bjorklund, rotateArray } from '../logic/bjorklund' import { TransportControls } from './TransportControls' export function DrumsView() { - const { kick, snare, hihat, hihatOpen, clap, kit, drive, setParams, setKit, setDrive } = useDrumStore() + const { kick, snare, hihat, hihatOpen, clap, kit, drive, setParams, setKit, setDrive, randomizeDrums } = useDrumStore() const { drumMachine, volumes, setVolume } = useAudioStore() const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { @@ -30,6 +28,23 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const handleRandomize = () => { + // Haptic Feedback for Telegram + if ((window as any).Telegram?.WebApp?.HapticFeedback) { + (window as any).Telegram.WebApp.HapticFeedback.impactOccurred('medium'); + } + randomizeDrums(); + + // Sync the engine immediately + if (drumMachine) { + const drumIds: ('kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell')[] = ['kick', 'snare', 'hihat', 'hihatOpen', 'clap', 'cowbell']; + const state = useDrumStore.getState(); + drumIds.forEach(id => { + drumMachine.setDrumParams(id, state[id].pitch, state[id].decay); + }); + } + } + return (
@@ -38,6 +53,24 @@ export function DrumsView() {

Настройки

+ ) { + this.setKit(kit) + this.setSaturation(drive) + Object.entries(params).forEach(([id, p]) => { + this.setDrumParams(id, p.pitch, p.decay) + }) + } + triggerDrum(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', time: number, velocity: number = 0.8) { const p = this.params[drum] const kit808 = this.kit808 diff --git a/src/logic/DrumUtils.ts b/src/logic/DrumUtils.ts index b9917c79..104f7eea 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -1,3 +1,5 @@ +import * as Tone from 'tone' + /** * Shared DSP utilities for drum synthesis based on research specs. */ @@ -40,3 +42,40 @@ export function applyVariance(base: number, variance: number = 0.02): number { // Math.random() * 0.04 - 0.02 gives range [-0.02, 0.02] return base * (1 + (Math.random() * (variance * 2) - variance)); } + +/** + * Generates a white noise buffer. + * @param context - Tone.js Audio Context + * @param duration - Buffer duration in seconds + */ +export function generateWhiteNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer { + const sampleRate = context.sampleRate; + const buffer = context.createBuffer(1, sampleRate * duration, sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < data.length; i++) { + data[i] = Math.random() * 2 - 1; + } + return buffer; +} + +/** + * Generates a 15-bit LFSR noise buffer (TR-909 style). + * Uses polynomial x^15 + x^14 + 1. + * @param context - Tone.js Audio Context + * @param duration - Buffer duration in seconds + */ +export function generateLFSRNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer { + const sampleRate = context.sampleRate; + const buffer = context.createBuffer(1, sampleRate * duration, sampleRate); + const data = buffer.getChannelData(0); + let state = 0x7FFF; // Initial non-zero state + + for (let i = 0; i < data.length; i++) { + // x^15 + x^14 + 1 (15-bit) + const bit = ((state >> 14) ^ (state >> 13)) & 1; + state = ((state << 1) | bit) & 0x7FFF; + // Normalize to [-1, 1] + data[i] = (state / 16384) - 1.0; + } + return buffer; +} diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index 3334a6fe..1f6a493a 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -1,18 +1,21 @@ import * as Tone from 'tone' -import { applyVariance } from '../DrumUtils' +import { applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR808Clap { private noiseBuffer: AudioBuffer; constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; - this.noiseBuffer = Tone.getContext().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; + // Generate longer noise buffer once + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 1.0); } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration - 0.5); + const bpfFreq = (1000 + pitch * 1000); const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass"); const gain = new Tone.Gain(0).connect(this.destination); @@ -25,10 +28,12 @@ export class TR808Clap { const snapIntervalBase = 0.01; const snapInterval = applyVariance(snapIntervalBase, 0.02); + const safeVelocity = Math.max(0.001, velocity); + for (let i = 0; i < snapCount; i++) { const snapTime = time + i * snapInterval; - gain.gain.setValueAtTime(velocity, snapTime); - gain.gain.exponentialRampToValueAtTime(velocity * 0.1, snapTime + snapInterval * 0.8); + gain.gain.setValueAtTime(safeVelocity, snapTime); + gain.gain.exponentialRampToValueAtTime(safeVelocity * 0.1, snapTime + snapInterval * 0.8); } // Final decay @@ -36,10 +41,10 @@ export class TR808Clap { const decayTimeBase = 0.1 + decay * 0.5; const decayTime = applyVariance(decayTimeBase, 0.02); - gain.gain.setValueAtTime(velocity, finalDecayStart); + gain.gain.setValueAtTime(safeVelocity, finalDecayStart); gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime); - noiseSrc.start(time).stop(finalDecayStart + decayTime); + noiseSrc.start(time, randomStart).stop(finalDecayStart + decayTime); noiseSrc.onended = () => { noiseSrc.dispose(); diff --git a/src/logic/drums/TR808Cowbell.ts b/src/logic/drums/TR808Cowbell.ts index 3dd1d4fd..f4284e71 100644 --- a/src/logic/drums/TR808Cowbell.ts +++ b/src/logic/drums/TR808Cowbell.ts @@ -2,13 +2,14 @@ import * as Tone from 'tone' import { applyPitchDrift, applyVariance } from '../DrumUtils' export class TR808Cowbell { - private activeGains: Set = new Set(); + private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set(); constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // Core: Dual square wave oscillators from Schmitt trigger matrix - // Standard frequencies: 540Hz and 800Hz const pitchMultiplier = 0.5 + pitch; // Range approx 0.5x to 1.5x const freq1 = applyPitchDrift(540 * pitchMultiplier, 2.0); const freq2 = applyPitchDrift(800 * pitchMultiplier, 2.0); @@ -30,10 +31,12 @@ export class TR808Cowbell { const decayTime = applyVariance(0.1 + decay * 0.4, 0.02); - vca.gain.setValueAtTime(velocity, time); + const safeVelocity = Math.max(0.001, velocity); + vca.gain.setValueAtTime(safeVelocity, time); vca.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(vca); + const voice = { oscillators: [osc1, osc2], gain: vca }; + this.activeVoices.add(voice); osc1.start(time).stop(time + decayTime); osc2.start(time).stop(time + decayTime); @@ -45,15 +48,16 @@ export class TR808Cowbell { bpf.dispose(); hpf.dispose(); vca.dispose(); - this.activeGains.delete(vca); + this.activeVoices.delete(voice); }; } stop(time: number) { - this.activeGains.forEach(vca => { - vca.gain.cancelScheduledValues(time); - vca.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + this.activeVoices.forEach(voice => { + voice.gain.gain.cancelScheduledValues(time); + voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + voice.oscillators.forEach(osc => osc.stop(time + 0.02)); }); - this.activeGains.clear(); + this.activeVoices.clear(); } } diff --git a/src/logic/drums/TR808HiHat.ts b/src/logic/drums/TR808HiHat.ts index acd1216d..7327020f 100644 --- a/src/logic/drums/TR808HiHat.ts +++ b/src/logic/drums/TR808HiHat.ts @@ -3,11 +3,13 @@ import { applyPitchDrift, applyVariance } from '../DrumUtils' export class TR808HiHat { private frequencies = [205.3, 304.4, 369.6, 522.7, 800, 540]; - private activeGains: Set = new Set(); + private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set(); constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // Create nodes const mixGain = new Tone.Gain(0.15); const bpf1 = new Tone.Filter(3440, "bandpass"); @@ -28,7 +30,6 @@ export class TR808HiHat { }); // Routing Graph - // Oscillators -> MixGain -> [BPF1, BPF2] (Parallel) -> EnvGain -> HPF -> Destination mixGain.connect(bpf1); mixGain.connect(bpf2); bpf1.connect(envGain); @@ -48,18 +49,19 @@ export class TR808HiHat { const decayTime = applyVariance(decayBase, 0.02); // VCA Envelope - envGain.gain.setValueAtTime(velocity, time); + const safeVelocity = Math.max(0.001, velocity); + envGain.gain.setValueAtTime(safeVelocity, time); envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(envGain); + const voice = { oscillators, gain: envGain }; + this.activeVoices.add(voice); // Scheduling oscillators.forEach(osc => { osc.start(time).stop(time + decayTime); }); - // Disposal - Explicitly clean up all 11-12 nodes to prevent memory leaks - // We use the first oscillator's onstop event to trigger the cleanup + // Disposal oscillators[0].onstop = () => { oscillators.forEach(o => o.dispose()); mixGain.dispose(); @@ -67,15 +69,16 @@ export class TR808HiHat { bpf2.dispose(); envGain.dispose(); hpf.dispose(); - this.activeGains.delete(envGain); + this.activeVoices.delete(voice); }; } stop(time: number) { - this.activeGains.forEach(gain => { - gain.gain.cancelScheduledValues(time); - gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + this.activeVoices.forEach(voice => { + voice.gain.gain.cancelScheduledValues(time); + voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + voice.oscillators.forEach(osc => osc.stop(time + 0.02)); }); - this.activeGains.clear(); + this.activeVoices.clear(); } } diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..cc91632a 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -5,6 +5,8 @@ export class TR808Kick { constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // 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,11 +32,20 @@ export class TR808Kick { osc.frequency.setValueAtTime(startFreq, time); osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); - // VCA Amp Envelope: Instant attack, adjustable exponential decay - masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + // VCA Amp Envelope: Two-stage damping for authenticity + // Initial damping stage (diode-like behavior): fast decay to 50% volume in 20ms + const dampingTime = 0.02; + const midGain = Math.max(0.001, velocity * 0.5); + const safeVelocity = Math.max(0.001, velocity); + + masterGain.gain.setValueAtTime(safeVelocity, time); + masterGain.gain.exponentialRampToValueAtTime(midGain, time + dampingTime); + + // Final decay stage: from 50% to 0 + const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay); + masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay); - osc.start(time).stop(time + finalDecay); + osc.start(time).stop(time + safeFinalDecay); osc.onstop = () => { osc.dispose(); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 23f8f2ac..b2c7a4b0 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -1,20 +1,16 @@ import * as Tone from 'tone' -import { applyPitchDrift, applyVariance } from '../DrumUtils' +import { applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR808Snare { private noiseBuffer: AudioBuffer; constructor(private destination: Tone.ToneAudioNode) { - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.5; // 500ms - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - for (let i = 0; i < data.length; i++) { - data[i] = Math.random() * 2 - 1; - } + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 1.0); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // pitch maps to tone balance here (balance between low and high modes) const toneBalance = pitch; @@ -40,16 +36,16 @@ export class TR808Snare { // Independent envelopes for maximum authenticity as per research // High mode decays faster (approx 75% of low mode decay) - gainLow.gain.setValueAtTime(velocity * (1 - toneBalance), time); + const safeVelocity = Math.max(0.001, velocity); + gainLow.gain.setValueAtTime(safeVelocity * (1 - toneBalance), time); gainLow.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - gainHigh.gain.setValueAtTime(velocity * toneBalance, time); + gainHigh.gain.setValueAtTime(safeVelocity * toneBalance, time); gainHigh.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay * 0.75); - // Snappy Layer + // Snappy Layer - Using random start const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - // High-pass filter (>1800Hz) to prevent phase trap with tonal body - // Q = 0.707 (Butterworth) + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.1); const noiseFilter = new Tone.Filter({ frequency: noiseFilterFreq, type: "highpass", @@ -61,12 +57,12 @@ export class TR808Snare { noiseFilter.connect(snappyGain); snappyGain.connect(this.destination); - snappyGain.gain.setValueAtTime(velocity * 0.8, time); + snappyGain.gain.setValueAtTime(safeVelocity * 0.8, time); snappyGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay); oscLow.start(time).stop(time + vcaDecay); oscHigh.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + snappyDecay + 0.1); + noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1); // Cleanup oscLow.onstop = () => { diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index dd2582ed..6736b094 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -1,24 +1,19 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR909Kick { private noiseBuffer: AudioBuffer; private bodyCurve: Float32Array; constructor(private destination: Tone.ToneAudioNode) { - // Soft Clipping curve from research - this.bodyCurve = makeDistortionCurve(10); - - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.05; // 50ms click - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - for (let i = 0; i < bufferSize; i++) { - data[i] = Math.random() * 2 - 1; - } + // Soft Clipping curve from research - increased intensity for 909 punch + this.bodyCurve = makeDistortionCurve(30); + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 0.1); // 100ms click } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // 909 Kick base frequency is fixed around 50Hz const tune = 50; // The 'Pitch' parameter on 909 maps to the frequency sweep duration @@ -53,12 +48,14 @@ export class TR909Kick { bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + sweepDuration); // VCA Envelope - bodyGain.gain.setValueAtTime(velocity, time); + const safeVelocity = Math.max(0.001, velocity); + bodyGain.gain.setValueAtTime(safeVelocity, time); bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - // Click Layer (Noise) + // Click Layer (Noise) - Using random start to avoid machine-gun effect const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap + const randomStart = Math.random() * (this.noiseBuffer.duration - 0.05); + const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); const noiseGain = new Tone.Gain(0); noiseSrc.connect(noiseFilter); @@ -67,20 +64,20 @@ export class TR909Kick { // Ultra short envelope (10-20ms) for the click const clickDecay = applyVariance(0.02, 0.02); - noiseGain.gain.setValueAtTime(velocity * 0.7, time); + noiseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.7), time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay); // Rectangular Pulse Click: Short 5ms impulse for attack articulation - const pulseOsc = new Tone.Oscillator(100, "square"); // Research: 100Hz square wave pulse + const pulseOsc = new Tone.Oscillator(100, "square"); const pulseGain = new Tone.Gain(0); pulseOsc.connect(pulseGain); pulseGain.connect(this.destination); - pulseGain.gain.setValueAtTime(velocity * 0.5, time); + pulseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.5), time); pulseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.005); bodyOsc.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + clickDecay); + noiseSrc.start(time, randomStart).stop(time + clickDecay); pulseOsc.start(time).stop(time + 0.005); bodyOsc.onstop = () => { diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..2b86d67a 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -1,5 +1,5 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateLFSRNoise } from '../DrumUtils' export class TR909Snare { private noiseBuffer: AudioBuffer; @@ -8,18 +8,12 @@ export class TR909Snare { constructor(private destination: Tone.ToneAudioNode) { // Soft Clipping curve from research this.bodyCurve = makeDistortionCurve(15); - - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.5; - this.noiseBuffer = Tone.getContext().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++) { - data[i] = Math.random() * 2 - 1; - } + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 1.0); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz const freq1 = 160; const freq2 = 220; @@ -60,11 +54,13 @@ export class TR909Snare { osc2.frequency.setValueAtTime(startFreq2, time); osc2.frequency.exponentialRampToValueAtTime(toneDrift2, time + sweepTime); - tonalGain.gain.setValueAtTime(velocity, time); + const safeVelocity = Math.max(0.001, velocity); + tonalGain.gain.setValueAtTime(safeVelocity, time); tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - // Snappy Layer + // Snappy Layer - Using LFSR noise and random start const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.1); const hpf = new Tone.Filter(noiseHPFFreq, "highpass"); // HPF to protect fundamental // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz (research: toneCutoff) const toneCutoff = 4000 + pitch * 4000; @@ -76,12 +72,12 @@ export class TR909Snare { lpf.connect(noiseGain); noiseGain.connect(this.destination); - noiseGain.gain.setValueAtTime(velocity * 0.7, time); + noiseGain.gain.setValueAtTime(safeVelocity * 0.7, time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay); osc1.start(time).stop(time + vcaDecay); osc2.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + snappyDecay + 0.1); + noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1); osc1.onstop = () => { osc1.dispose(); diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index c90d17e8..8386593d 100644 --- a/src/store/audioStore.ts +++ b/src/store/audioStore.ts @@ -3,6 +3,7 @@ import * as Tone from 'tone' import { AcidSynth } from '../logic/AcidSynth' import { DrumMachine } from '../logic/DrumMachine' import { PadSynth } from '../logic/PadSynth' +import { useDrumStore } from './instrumentStore' export interface AudioState { isInitialized: boolean @@ -60,6 +61,17 @@ export const useAudioStore = create((set, get) => ({ const drums = new DrumMachine() const pads = new PadSynth() + // Synchronize initial state from stores + const drumStore = useDrumStore.getState() + drums.syncInternalParams(drumStore.kit, drumStore.drive, { + kick: { pitch: drumStore.kick.pitch, decay: drumStore.kick.decay }, + snare: { pitch: drumStore.snare.pitch, decay: drumStore.snare.decay }, + hihat: { pitch: drumStore.hihat.pitch, decay: drumStore.hihat.decay }, + hihatOpen: { pitch: drumStore.hihatOpen.pitch, decay: drumStore.hihatOpen.decay }, + clap: { pitch: drumStore.clap.pitch, decay: drumStore.clap.decay }, + cowbell: { pitch: drumStore.cowbell.pitch, decay: drumStore.cowbell.decay } + }) + Tone.Transport.bpm.value = get().bpm Tone.Transport.swing = get().swing diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..27a41800 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,22 +41,34 @@ interface DrumState { setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => void setKit: (kit: '808' | '909') => void setDrive: (drive: number) => void + randomizeDrums: () => void } export const useDrumStore = create((set) => ({ kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, - snare: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, + snare: { steps: 16, pulses: 4, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, hihatOpen: { steps: 16, pulses: 4, rotate: 2, decay: 0.5, pitch: 0.5, probability: 1.0 }, - clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, - cowbell: { steps: 16, pulses: 2, rotate: 2, decay: 0.5, pitch: 0.5, probability: 1.0 }, + clap: { steps: 16, pulses: 4, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, + cowbell: { steps: 16, pulses: 3, rotate: 2, decay: 0.5, pitch: 0.5, probability: 0.8 }, kit: '909', drive: 20, setParams: (drum, params) => set((state) => ({ [drum]: { ...state[drum], ...params } })), setKit: (kit) => set({ kit }), - setDrive: (drive) => set({ drive }) + setDrive: (drive) => set({ drive }), + randomizeDrums: () => set((state) => { + const randomPulses = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; + return { + kick: { ...state.kick, pulses: randomPulses(2, 6), rotate: Math.floor(Math.random() * 8) }, + snare: { ...state.snare, pulses: randomPulses(2, 6), rotate: Math.floor(Math.random() * 8) }, + hihat: { ...state.hihat, pulses: randomPulses(8, 14), rotate: Math.floor(Math.random() * 8) }, + hihatOpen: { ...state.hihatOpen, pulses: randomPulses(2, 6), rotate: Math.floor(Math.random() * 8) }, + clap: { ...state.clap, pulses: randomPulses(2, 4), rotate: Math.floor(Math.random() * 8) }, + cowbell: { ...state.cowbell, pulses: randomPulses(1, 6), rotate: Math.floor(Math.random() * 8), probability: 0.6 + Math.random() * 0.4 } + } + }) })) // Pad Store