From c67ec734d94ec112ce3caeba6be72a377731792d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:43:53 +0000 Subject: [PATCH] feat: implement procedural analog drum synthesis based on deep DSP research - Enhanced DrumUtils with 15-bit LFSR noise and cents-based pitch drift. - Refined TR-808 Kick with diode damping emulation and dual-stage envelopes. - Refined TR-909 Kick with triangle body, click layers, and saturation. - Refined TR-808/909 Snares and Clap with randomized noise start offsets. - Implemented authentic 15-bit LFSR noise for TR-909 Snappy. - Re-architected Hi-Hat and Cowbell with active voice tracking and proper disposal. - Added parameter synchronization between UI and audio engine. - Established techno-focused default Euclidean patterns. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/logic/DrumMachine.ts | 8 ++++++ src/logic/DrumUtils.ts | 49 ++++++++++++++++++++++++++++----- src/logic/drums/TR808Clap.ts | 12 ++++---- src/logic/drums/TR808Cowbell.ts | 19 +++++++------ src/logic/drums/TR808HiHat.ts | 28 +++++++++++-------- src/logic/drums/TR808Kick.ts | 25 +++++++++++------ src/logic/drums/TR808Snare.ts | 24 ++++++---------- src/logic/drums/TR909Kick.ts | 33 ++++++++-------------- src/logic/drums/TR909Snare.ts | 36 +++++++++--------------- src/store/audioStore.ts | 12 ++++++++ src/store/instrumentStore.ts | 4 +-- 11 files changed, 147 insertions(+), 103 deletions(-) diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index cdf1d2e1..9eaa437b 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -87,6 +87,14 @@ export class DrumMachine { } } + syncInternalParams(kit: '808' | '909', drive: number, drumParams: Record) { + this.setKit(kit) + this.setSaturation(drive) + for (const [drum, p] of Object.entries(drumParams)) { + this.setDrumParams(drum, p.pitch, p.decay) + } + } + setSaturation(amount: number) { this.shaper.curve = makeDistortionCurve(amount) } diff --git a/src/logic/DrumUtils.ts b/src/logic/DrumUtils.ts index b9917c79..a5ef6e78 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. */ @@ -21,13 +23,14 @@ export function makeDistortionCurve(amount: number = 20): Float32Array { } /** - * Applies micro-randomization to a base frequency (Pitch Drift). - * Typically +/- 1Hz as per research. + * Applies micro-randomization to a base frequency (Pitch Drift) using cents. + * Formula: f_new = f_base * Math.pow(2, cents / 1200) * @param base - Base frequency in Hz - * @param range - Drift range in Hz (default 1.0) + * @param centsRange - Drift range in cents (default 1.0) */ -export function applyPitchDrift(base: number, range: number = 1.0): number { - return base + (Math.random() * 2 - 1) * range; +export function applyPitchDrift(base: number, centsRange: number = 1.0): number { + const cents = (Math.random() * 2 - 1) * centsRange; + return base * Math.pow(2, cents / 1200); } /** @@ -37,6 +40,38 @@ export function applyPitchDrift(base: number, range: number = 1.0): number { * @param variance - Variance percentage (e.g. 0.02 for 2%) */ 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)); + // Math.random() * 2 - 1 gives range [-1, 1] + return base * (1 + (Math.random() * 2 - 1) * variance); +} + +/** + * Generates a 2.0s white noise buffer. + */ +export function generateWhiteNoise(context: Tone.BaseContext): AudioBuffer { + const bufferSize = context.sampleRate * 2.0; + const buffer = context.createBuffer(1, bufferSize, context.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + return buffer; +} + +/** + * Generates an authentic 15-bit LFSR pseudo-random noise sequence. + * Uses characteristic polynomial x^15 + x^14 + 1. + */ +export function generateLFSRNoise(context: Tone.BaseContext): AudioBuffer { + const bufferSize = context.sampleRate * 2.0; + const buffer = context.createBuffer(1, bufferSize, context.sampleRate); + const data = buffer.getChannelData(0); + let reg = 0x7FFF; // 15-bit register initialized to all ones + + for (let i = 0; i < bufferSize; i++) { + // Feedback bit is bit 14 XOR bit 13 (zero-indexed) + const bit = ((reg >> 14) ^ (reg >> 13)) & 1; + reg = ((reg << 1) | bit) & 0x7FFF; + data[i] = (reg / 16384.0) - 1.0; // Normalize to [-1.0, 1.0] + } + return buffer; } diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index 3334a6fe..e1d3735a 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -1,17 +1,17 @@ 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; + this.noiseBuffer = generateWhiteNoise(Tone.getContext()); } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + + const randomStart = Math.random() * (this.noiseBuffer.duration - 0.6); const noiseSrc = new Tone.BufferSource(this.noiseBuffer); const bpfFreq = (1000 + pitch * 1000); const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass"); @@ -39,7 +39,7 @@ export class TR808Clap { gain.gain.setValueAtTime(velocity, 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..9fe7a4ac 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[], vca: 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); @@ -33,7 +34,8 @@ export class TR808Cowbell { vca.gain.setValueAtTime(velocity, time); vca.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(vca); + const voice = { oscillators: [osc1, osc2], vca }; + this.activeVoices.add(voice); osc1.start(time).stop(time + decayTime); osc2.start(time).stop(time + decayTime); @@ -45,15 +47,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.oscillators.forEach(osc => osc.stop(time)); + voice.vca.gain.cancelScheduledValues(time); + voice.vca.gain.exponentialRampToValueAtTime(0.001, 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..86dc0926 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[], envGain: 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); @@ -36,14 +37,14 @@ export class TR808HiHat { envGain.connect(hpf); hpf.connect(this.destination); - // Filter Q values and randomization + // Filter settings bpf1.Q.value = 1.5; bpf2.Q.value = 1.5; bpf1.frequency.value = applyVariance(3440, 0.02); bpf2.frequency.value = applyVariance(7100, 0.02); hpf.frequency.value = applyVariance(7000, 0.02); - // Decay: Closed Hat (40-60ms), Open Hat (300-500ms) + // Decay const decayBase = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02); const decayTime = applyVariance(decayBase, 0.02); @@ -51,15 +52,15 @@ export class TR808HiHat { envGain.gain.setValueAtTime(velocity, time); envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(envGain); + const voice = { oscillators, 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 +68,18 @@ 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.oscillators.forEach(osc => { + osc.stop(time); + }); + voice.envGain.gain.cancelScheduledValues(time); + voice.envGain.gain.exponentialRampToValueAtTime(0.001, 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..b26b103e 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 @@ -18,23 +20,28 @@ export class TR808Kick { osc.connect(masterGain); masterGain.connect(this.destination); - // Micro-randomization using shared utilities + // Micro-randomization const tuneDrift = applyPitchDrift(tune, 1.0); const finalDecay = applyVariance(decayTime, 0.02); - // Pitch Envelope: Start high (Tune * 2.5) and drop quickly (50ms) to simulate the membrane hit ('tonk') - // This rapid sweep generates the punch without needing a separate click oscillator - const startFreq = tuneDrift * 2.5; - const endFreq = tuneDrift; + // Diode Damping Emulation: + // 1. Two-stage frequency sweep + // 2. Two-stage amplitude envelope (fast 20ms initial decay to 50% volume) + const startFreq = tuneDrift * 2.5; osc.frequency.setValueAtTime(startFreq, time); - osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); + osc.frequency.exponentialRampToValueAtTime(tuneDrift * 1.1, time + 0.02); + osc.frequency.exponentialRampToValueAtTime(tuneDrift, time + 0.05); + + // Ensure finalDecay is at least 0.021s to allow for damping stage + const safeFinalDecay = Math.max(finalDecay, 0.021); - // VCA Amp Envelope: Instant attack, adjustable exponential decay masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + // Damping stage: drop to 50% quickly + masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + 0.02); + 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..40690c19 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -1,30 +1,26 @@ 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()); } 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; - // Micro-randomization using shared utilities + // Micro-randomization const vcaDecay = applyVariance(0.2, 0.02); const snappyDecayBase = 0.25 + snappy * 0.15; const snappyDecay = applyVariance(snappyDecayBase, 0.02); const noiseFilterFreq = applyVariance(1800, 0.02); - // 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research + // 808 Membrane modes: fixed at ~238Hz and ~476Hz const oscLow = new Tone.Oscillator(applyPitchDrift(238, 1.0), "sine"); const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 1.0), "sine"); oscLow.phase = Math.random() * 360; @@ -38,8 +34,7 @@ export class TR808Snare { gainLow.connect(this.destination); gainHigh.connect(this.destination); - // Independent envelopes for maximum authenticity as per research - // High mode decays faster (approx 75% of low mode decay) + // Independent envelopes gainLow.gain.setValueAtTime(velocity * (1 - toneBalance), time); gainLow.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); @@ -47,9 +42,8 @@ export class TR808Snare { gainHigh.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay * 0.75); // Snappy Layer + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.1); const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - // High-pass filter (>1800Hz) to prevent phase trap with tonal body - // Q = 0.707 (Butterworth) const noiseFilter = new Tone.Filter({ frequency: noiseFilterFreq, type: "highpass", @@ -66,7 +60,7 @@ export class TR808Snare { 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..3aeda939 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -1,5 +1,5 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR909Kick { private noiseBuffer: AudioBuffer; @@ -7,18 +7,13 @@ export class TR909Kick { 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; - } + this.bodyCurve = makeDistortionCurve(30); + this.noiseBuffer = generateWhiteNoise(Tone.getContext()); } 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 @@ -27,36 +22,33 @@ export class TR909Kick { // decay: 0.5 -> 0.45s, maps to 0.3-0.6s const decayTime = 0.3 + decay * 0.3; - // Micro-randomization using shared utilities + // Micro-randomization const tuneDrift = applyPitchDrift(tune, 0.5); const vcaDecay = applyVariance(decayTime, 0.02); const noiseFilterFreq = applyVariance(1000, 0.02); - // 909 Kick Body: Triangle Oscillator with saturation and Low-Pass smoothing + // 909 Kick Body: Triangle Oscillator with saturation const bodyOsc = new Tone.Oscillator(tuneDrift * 4.7, "triangle"); bodyOsc.phase = Math.random() * 360; const bodyShaper = new Tone.WaveShaper(this.bodyCurve); bodyShaper.oversample = '4x'; - const bodyFilter = new Tone.Filter(1000, "lowpass"); const bodyGain = new Tone.Gain(0); bodyOsc.connect(bodyShaper); - bodyShaper.connect(bodyFilter); - bodyFilter.connect(bodyGain); + bodyShaper.connect(bodyGain); bodyGain.connect(this.destination); // Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over sweepDuration const startFreq = tuneDrift * 4.7; - const endFreq = tuneDrift; - bodyOsc.frequency.setValueAtTime(startFreq, time); - bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + sweepDuration); + bodyOsc.frequency.exponentialRampToValueAtTime(tuneDrift, time + sweepDuration); // VCA Envelope bodyGain.gain.setValueAtTime(velocity, time); bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); // Click Layer (Noise) + const randomStart = Math.random() * (this.noiseBuffer.duration - 0.05); const noiseSrc = new Tone.BufferSource(this.noiseBuffer); const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap const noiseGain = new Tone.Gain(0); @@ -71,7 +63,7 @@ export class TR909Kick { 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); @@ -80,13 +72,12 @@ export class TR909Kick { 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 = () => { bodyOsc.dispose(); bodyShaper.dispose(); - bodyFilter.dispose(); bodyGain.dispose(); pulseOsc.dispose(); pulseGain.dispose(); diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..0a0203cb 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -1,30 +1,23 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateLFSRNoise } from '../DrumUtils' export class TR909Snare { private noiseBuffer: AudioBuffer; private bodyCurve: Float32Array; 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()); } 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; - // Micro-randomization using shared utilities + // Micro-randomization const vcaDecay = applyVariance(0.2, 0.02); const snappyDecayBase = 0.1 + snappy * 0.4; const snappyDecay = applyVariance(snappyDecayBase, 0.02); @@ -36,7 +29,7 @@ export class TR909Snare { const osc2 = new Tone.Oscillator(toneDrift2 * 2, "triangle"); osc1.phase = Math.random() * 360; osc2.phase = Math.random() * 360; - // Routing with gain compensation to prevent clipping before the shaper + const preShaperGain = new Tone.Gain(0.5); const bodyShaper = new Tone.WaveShaper(this.bodyCurve); bodyShaper.oversample = '4x'; @@ -50,23 +43,20 @@ export class TR909Snare { postShaperGain.connect(tonalGain); tonalGain.connect(this.destination); - // Pitch Sweep: ~320Hz to ~160Hz over 30ms (as per research spec) + // Pitch Sweep: ~320Hz to ~160Hz over 30ms const sweepTime = 0.03; - const startFreq1 = toneDrift1 * 2; - const startFreq2 = toneDrift2 * 2; - - osc1.frequency.setValueAtTime(startFreq1, time); + osc1.frequency.setValueAtTime(toneDrift1 * 2, time); osc1.frequency.exponentialRampToValueAtTime(toneDrift1, time + sweepTime); - osc2.frequency.setValueAtTime(startFreq2, time); + osc2.frequency.setValueAtTime(toneDrift2 * 2, time); osc2.frequency.exponentialRampToValueAtTime(toneDrift2, time + sweepTime); tonalGain.gain.setValueAtTime(velocity, time); tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - // Snappy Layer + // Snappy Layer (LFSR) + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.1); const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - 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 hpf = new Tone.Filter(noiseHPFFreq, "highpass"); const toneCutoff = 4000 + pitch * 4000; const lpf = new Tone.Filter(applyVariance(toneCutoff, 0.02), "lowpass"); const noiseGain = new Tone.Gain(0); @@ -81,7 +71,7 @@ export class TR909Snare { 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..eb9fcb58 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() + // Sync initial params from drum store + const drumStore = useDrumStore.getState() + drums.syncInternalParams(drumStore.kit, drumStore.drive, { + kick: drumStore.kick, + snare: drumStore.snare, + hihat: drumStore.hihat, + hihatOpen: drumStore.hihatOpen, + clap: drumStore.clap, + cowbell: drumStore.cowbell + }) + 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..a99cd244 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -45,11 +45,11 @@ interface DrumState { 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 }, + 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) => ({