From caf25c94a83bf823b9168d14013d795fd92ed5de Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:27:23 +0000 Subject: [PATCH] feat: implement authentic TR-808 and TR-909 drum synthesis This commit enhances the drum synthesis engine based on deep DSP research: - Implemented 15-bit LFSR pseudo-random noise for TR-909 Snare authenticity. - Refactored pitch drift to use cents (logarithmic) for realistic analog deviation. - Added two-stage amplitude envelope to TR-808 Kick to simulate diode damping. - Optimized default Euclidean patterns for a better techno foundation. - Ensured robust node disposal and defensive scheduling for Web Audio stability. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/logic/DrumUtils.ts | 44 ++++++++++++++++++++++++++++++----- src/logic/drums/TR808Kick.ts | 14 +++++++---- src/logic/drums/TR808Snare.ts | 4 ++-- src/logic/drums/TR909Kick.ts | 2 +- src/logic/drums/TR909Snare.ts | 16 ++++--------- src/store/instrumentStore.ts | 13 ++++++----- 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/src/logic/DrumUtils.ts b/src/logic/DrumUtils.ts index b9917c79..20c603c4 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -21,13 +21,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. + * Typically +/- 1-2 cents as per research. * @param base - Base frequency in Hz - * @param range - Drift range in Hz (default 1.0) + * @param centsRange - Drift range in cents (default 2.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 = 2.0): number { + const cents = (Math.random() * 2 - 1) * centsRange; + return base * Math.pow(2, cents / 1200); } /** @@ -37,6 +38,37 @@ 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] + // Math.random() * (variance * 2) - variance gives range [-variance, variance] return base * (1 + (Math.random() * (variance * 2) - variance)); } + +/** + * Generates an authentic LFSR (Linear Feedback Shift Register) noise buffer. + * Emulates the 15-bit digital noise used in TR-909. + * Polynomial: x^15 + x^14 + 1 + * @param context - AudioContext to create the buffer in + * @param duration - Duration in seconds + */ +export function generateLFSRNoise(context: any, duration: number = 0.5): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = sampleRate * duration; + const buffer = context.createBuffer(1, bufferSize, sampleRate); + const data = buffer.getChannelData(0); + + let state = 0x7FFF; // 15-bit initial state (all ones) + + for (let i = 0; i < bufferSize; i++) { + // Feedback bit = bit 14 XOR bit 13 (0-indexed bits of a 15-bit register) + const bit14 = (state >> 14) & 1; + const bit13 = (state >> 13) & 1; + const feedback = bit14 ^ bit13; + + state = ((state << 1) | feedback) & 0x7FFF; + + // Convert to [-1, 1] range + // We use bit 0 as the output bit + data[i] = ((state & 1) * 2) - 1; + } + + return buffer; +} diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..817d5571 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -19,7 +19,7 @@ export class TR808Kick { masterGain.connect(this.destination); // Micro-randomization using shared utilities - const tuneDrift = applyPitchDrift(tune, 1.0); + const tuneDrift = applyPitchDrift(tune, 2.0); // +/- 2 cents const finalDecay = applyVariance(decayTime, 0.02); // Pitch Envelope: Start high (Tune * 2.5) and drop quickly (50ms) to simulate the membrane hit ('tonk') @@ -30,11 +30,17 @@ export class TR808Kick { osc.frequency.setValueAtTime(startFreq, time); osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); - // VCA Amp Envelope: Instant attack, adjustable exponential decay + // VCA Amp Envelope: Two-stage decay to simulate diode damping + // 1. Initial fast decay to 50% over 20ms + // 2. Long exponential decay to zero masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + 0.02); - osc.start(time).stop(time + finalDecay); + // Ensure finalDecay is at least 0.021s to avoid scheduling errors + const safeFinalDecay = Math.max(finalDecay, 0.021); + masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay); + + 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..28314217 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -25,8 +25,8 @@ export class TR808Snare { const noiseFilterFreq = applyVariance(1800, 0.02); // 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research - const oscLow = new Tone.Oscillator(applyPitchDrift(238, 1.0), "sine"); - const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 1.0), "sine"); + const oscLow = new Tone.Oscillator(applyPitchDrift(238, 2.0), "sine"); // +/- 2 cents + const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 2.0), "sine"); oscLow.phase = Math.random() * 360; oscHigh.phase = Math.random() * 360; diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index dd2582ed..1e1350a0 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -28,7 +28,7 @@ export class TR909Kick { const decayTime = 0.3 + decay * 0.3; // Micro-randomization using shared utilities - const tuneDrift = applyPitchDrift(tune, 0.5); + const tuneDrift = applyPitchDrift(tune, 2.0); // +/- 2 cents const vcaDecay = applyVariance(decayTime, 0.02); const noiseFilterFreq = applyVariance(1000, 0.02); diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..6aa4d6ce 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; @@ -9,14 +9,8 @@ export class TR909Snare { // 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; - } + // Use authentic LFSR noise as per research for 909 Snare + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 0.5); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { @@ -29,8 +23,8 @@ export class TR909Snare { const snappyDecayBase = 0.1 + snappy * 0.4; const snappyDecay = applyVariance(snappyDecayBase, 0.02); const noiseHPFFreq = applyVariance(1000, 0.02); - const toneDrift1 = applyPitchDrift(freq1, 1.0); - const toneDrift2 = applyPitchDrift(freq2, 1.0); + const toneDrift1 = applyPitchDrift(freq1, 2.0); // +/- 2 cents + const toneDrift2 = applyPitchDrift(freq2, 2.0); const osc1 = new Tone.Oscillator(toneDrift1 * 2, "triangle"); const osc2 = new Tone.Oscillator(toneDrift2 * 2, "triangle"); diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..c68fc853 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -44,12 +44,13 @@ 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 }, - 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 }, + // Optimized Techno Foundation Patterns + kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, // 4-on-the-floor + snare: { steps: 16, pulses: 4, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, // Standard backbeat + hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, // 16th notes / shuffle + hihatOpen: { steps: 16, pulses: 4, rotate: 2, decay: 0.5, pitch: 0.5, probability: 1.0 },// Off-beat hats + clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, // Occasional clap + cowbell: { steps: 16, pulses: 3, rotate: 2, decay: 0.5, pitch: 0.5, probability: 0.8 }, // Syncopated cowbell kit: '909', drive: 20, setParams: (drum, params) => set((state) => ({