From d07683017e8c5f8357578783f1409a1bc786c9e5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 02:45:12 +0000 Subject: [PATCH] feat: implement authentic TR-808/909 synthesis engine - Implement 15-bit LFSR noise generator in DrumUtils. - Add cent-based pitch drift for accurate analog behavior. - Refine TR-808 Kick with two-stage diode damping envelope. - Refine TR-909 Kick with triangle body and noise/pulse click layers. - Implement dual-mode TR-808 Snare and snappy TR-909 Snare. - Implement TR-808 Hi-Hat matrix with 6 square oscillators. - Refine TR-808 Clap and Cowbell models. - Ensure robust node disposal across all instruments to prevent memory leaks. - Add 'Apply Techno Template' feature and haptic feedback integration. Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com> --- src/components/DrumsView.tsx | 22 ++++++++++++++---- src/components/SequencerLoop.tsx | 5 +++++ src/logic/DrumUtils.ts | 37 ++++++++++++++++++++++++++----- src/logic/drums/TR808Clap.ts | 12 +++++----- src/logic/drums/TR808Cowbell.ts | 24 +++++++++++--------- src/logic/drums/TR808HiHat.ts | 28 +++++++++++++---------- src/logic/drums/TR808Kick.ts | 26 +++++++++++++++------- src/logic/drums/TR808Snare.ts | 21 +++++++++--------- src/logic/drums/TR909Kick.ts | 38 ++++++++++++++------------------ src/logic/drums/TR909Snare.ts | 32 ++++++++++++--------------- src/store/instrumentStore.ts | 10 ++++++++- 11 files changed, 159 insertions(+), 96 deletions(-) diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..31da293e 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, applyTechnoTemplate } = useDrumStore() const { drumMachine, volumes, setVolume } = useAudioStore() const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { @@ -30,13 +28,29 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const handleApplyTemplate = () => { + applyTechnoTemplate() + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + } + return (
-

Настройки

+
+

Настройки

+ +
> 0) ^ (reg >> 1)) & 1; + reg = (reg >> 1) | (bit << 14); + data[i] = (bit * 2) - 1; + } + + return buffer; +} diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index 3334a6fe..a269f401 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -6,12 +6,14 @@ export class TR808Clap { constructor(private destination: Tone.ToneAudioNode) { const sampleRate = Tone.getContext().sampleRate; - this.noiseBuffer = Tone.getContext().createBuffer(1, sampleRate * 0.5, sampleRate); + this.noiseBuffer = Tone.getContext().createBuffer(1, Math.floor(sampleRate * 0.5), sampleRate); const data = this.noiseBuffer.getChannelData(0); for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1; } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + const noiseSrc = new Tone.BufferSource(this.noiseBuffer); const bpfFreq = (1000 + pitch * 1000); const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass"); @@ -27,8 +29,8 @@ export class TR808Clap { 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(Math.max(0.001, velocity), snapTime); + gain.gain.exponentialRampToValueAtTime(Math.max(0.001, velocity * 0.1), snapTime + snapInterval * 0.8); } // Final decay @@ -36,10 +38,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(Math.max(0.001, velocity), finalDecayStart); gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime); - noiseSrc.start(time).stop(finalDecayStart + decayTime); + noiseSrc.start(time).stop(finalDecayStart + decayTime + 0.1); noiseSrc.onended = () => { noiseSrc.dispose(); diff --git a/src/logic/drums/TR808Cowbell.ts b/src/logic/drums/TR808Cowbell.ts index 3dd1d4fd..1f4d424d 100644 --- a/src/logic/drums/TR808Cowbell.ts +++ b/src/logic/drums/TR808Cowbell.ts @@ -1,16 +1,18 @@ import * as Tone from 'tone' -import { applyPitchDrift, applyVariance } from '../DrumUtils' +import { applyVariance, applyPitchDrift } from '../DrumUtils' export class TR808Cowbell { - private activeGains: Set = new Set(); + private activeVoices: Set<{ oscillators: Tone.Oscillator[], vca: Tone.Gain, mixGain: Tone.Gain, bpf: Tone.Filter, hpf: Tone.Filter }> = 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 freq1 = applyPitchDrift(540 * pitchMultiplier, 2.0); // +/- 2 cents const freq2 = applyPitchDrift(800 * pitchMultiplier, 2.0); const osc1 = new Tone.Oscillator(freq1, "square"); @@ -30,10 +32,11 @@ export class TR808Cowbell { const decayTime = applyVariance(0.1 + decay * 0.4, 0.02); - vca.gain.setValueAtTime(velocity, time); + vca.gain.setValueAtTime(Math.max(0.001, velocity), time); vca.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(vca); + const voice = { oscillators: [osc1, osc2], vca, mixGain, bpf, hpf }; + 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.vca.gain.cancelScheduledValues(time); + voice.vca.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..47bb58c4 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, mixGain: Tone.Gain, bpf1: Tone.Filter, bpf2: Tone.Filter, hpf: Tone.Filter }> = 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"); @@ -20,7 +22,7 @@ export class TR808HiHat { // Create 6 Square Wave Oscillators (Schmitt Trigger Matrix) const oscillators = this.frequencies.map(freq => { - const driftedFreq = applyPitchDrift(freq * pitchMultiplier, 2.0); // +/- 2Hz drift for hats + const driftedFreq = applyPitchDrift(freq * pitchMultiplier, 2.0); // +/- 2 cents const osc = new Tone.Oscillator(driftedFreq, "square"); osc.phase = Math.random() * 360; osc.connect(mixGain); @@ -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,18 @@ export class TR808HiHat { const decayTime = applyVariance(decayBase, 0.02); // VCA Envelope - envGain.gain.setValueAtTime(velocity, time); + envGain.gain.setValueAtTime(Math.max(0.001, velocity), time); envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(envGain); + const voice = { oscillators, envGain, mixGain, bpf1, bpf2, hpf }; + 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.envGain.gain.cancelScheduledValues(time); + voice.envGain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + // Oscillators will stop naturally because of .stop(time + decayTime) scheduled in trigger + // But if we want immediate stop, we could call .stop(time + 0.02) on oscillators too. + 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..55d66f17 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 @@ -12,29 +14,37 @@ export class TR808Kick { // 808 Kick Core: Bridged-T Network emulation (Sine wave) const osc = new Tone.Oscillator(tune, "sine"); - osc.phase = Math.random() * 360; // Analog phase randomization + osc.phase = Math.random() * 360; const masterGain = new Tone.Gain(0); osc.connect(masterGain); 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') - // This rapid sweep generates the punch without needing a separate click oscillator + // Pitch Envelope: Start high (Tune * 2.5) and drop quickly (50ms) const startFreq = tuneDrift * 2.5; const endFreq = tuneDrift; 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: Diode damping emulation + // Research: "Ускоренное затухание резонанса в начальной фазе, формирующее плотный транзиент." + // Two-stage envelope: fast 20ms initial decay to 50% volume, then the main decay. + const dampingTime = 0.02; + const dampingVolume = velocity * 0.5; + + masterGain.gain.setValueAtTime(Math.max(0.001, velocity), time); + masterGain.gain.exponentialRampToValueAtTime(Math.max(0.001, dampingVolume), time + dampingTime); + + // Ensure finalDecay is always > dampingTime + 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..a47164cf 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -15,18 +15,20 @@ export class TR808Snare { } 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 - const oscLow = new Tone.Oscillator(applyPitchDrift(238, 1.0), "sine"); - const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 1.0), "sine"); + // 808 Membrane modes: fixed at ~238Hz and ~476Hz + const oscLow = new Tone.Oscillator(applyPitchDrift(238, 2.0), "sine"); + const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 2.0), "sine"); oscLow.phase = Math.random() * 360; oscHigh.phase = Math.random() * 360; @@ -38,18 +40,15 @@ 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) - gainLow.gain.setValueAtTime(velocity * (1 - toneBalance), time); + // Independent envelopes + gainLow.gain.setValueAtTime(Math.max(0.001, velocity * (1 - toneBalance)), time); gainLow.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - gainHigh.gain.setValueAtTime(velocity * toneBalance, time); + gainHigh.gain.setValueAtTime(Math.max(0.001, velocity * toneBalance), time); gainHigh.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay * 0.75); // Snappy Layer 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", @@ -61,7 +60,7 @@ export class TR808Snare { noiseFilter.connect(snappyGain); snappyGain.connect(this.destination); - snappyGain.gain.setValueAtTime(velocity * 0.8, time); + snappyGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.8), 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 dd2582ed..f700e389 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -19,20 +19,20 @@ export class TR909Kick { } 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 - // Range: 0.05s to 0.15s + // The 'Pitch' parameter on 909 maps to the frequency sweep duration (0.05s to 0.15s) const sweepDuration = 0.05 + pitch * 0.1; - // decay: 0.5 -> 0.45s, maps to 0.3-0.6s const decayTime = 0.3 + decay * 0.3; - // Micro-randomization using shared utilities - const tuneDrift = applyPitchDrift(tune, 0.5); + // Micro-randomization + const tuneDrift = applyPitchDrift(tune, 2.0); // +/- 2 cents 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); @@ -45,43 +45,39 @@ export class TR909Kick { bodyFilter.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); + // Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over sweepDuration + bodyOsc.frequency.setValueAtTime(tuneDrift * 4.7, time); + bodyOsc.frequency.exponentialRampToValueAtTime(tuneDrift, time + sweepDuration); // VCA Envelope - bodyGain.gain.setValueAtTime(velocity, time); + bodyGain.gain.setValueAtTime(Math.max(0.001, velocity), time); bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); // Click Layer (Noise) const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap + const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); const noiseGain = new Tone.Gain(0); noiseSrc.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(this.destination); - // 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 + // Rectangular Pulse Click: Short 5ms impulse + 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); - pulseOsc.start(time).stop(time + 0.005); + noiseSrc.start(time).stop(time + clickDecay + 0.1); + pulseOsc.start(time).stop(time + 0.01); bodyOsc.onstop = () => { bodyOsc.dispose(); diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..134da972 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,13 @@ 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; - } + // Use authentic 15-bit LFSR noise for 909 Snappy + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 0.5); } 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; @@ -29,14 +24,14 @@ 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"); 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,7 +45,7 @@ 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 (0.03s) as per research spec const sweepTime = 0.03; const startFreq1 = toneDrift1 * 2; const startFreq2 = toneDrift2 * 2; @@ -60,13 +55,14 @@ export class TR909Snare { osc2.frequency.setValueAtTime(startFreq2, time); osc2.frequency.exponentialRampToValueAtTime(toneDrift2, time + sweepTime); - tonalGain.gain.setValueAtTime(velocity, time); + tonalGain.gain.setValueAtTime(Math.max(0.001, velocity), time); tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); // Snappy Layer 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"); + + // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz const toneCutoff = 4000 + pitch * 4000; const lpf = new Tone.Filter(applyVariance(toneCutoff, 0.02), "lowpass"); const noiseGain = new Tone.Gain(0); @@ -76,7 +72,7 @@ export class TR909Snare { lpf.connect(noiseGain); noiseGain.connect(this.destination); - noiseGain.gain.setValueAtTime(velocity * 0.7, time); + noiseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.7), time); noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay); osc1.start(time).stop(time + vcaDecay); diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..71d1cc34 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,6 +41,7 @@ interface DrumState { setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => void setKit: (kit: '808' | '909') => void setDrive: (drive: number) => void + applyTechnoTemplate: () => void } export const useDrumStore = create((set) => ({ @@ -56,7 +57,14 @@ export const useDrumStore = create((set) => ({ [drum]: { ...state[drum], ...params } })), setKit: (kit) => set({ kit }), - setDrive: (drive) => set({ drive }) + setDrive: (drive) => set({ drive }), + applyTechnoTemplate: () => set((state) => ({ + kick: { ...state.kick, pulses: 4, rotate: 0 }, + snare: { ...state.snare, pulses: 4, rotate: 4 }, + hihat: { ...state.hihat, pulses: 12, rotate: 0 }, + hihatOpen: { ...state.hihatOpen, pulses: 4, rotate: 2 }, + cowbell: { ...state.cowbell, pulses: 3, rotate: 2, probability: 0.8 } + })) })) // Pad Store