From f41641e4f30eabf68d0c3d1ac3b754f4cc29f19f Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Wed, 17 Jun 2026 02:35:50 +0000
Subject: [PATCH] Upgrade drum synthesis engine based on DSP research
- Implemented authentic 15-bit LFSR noise for TR-909 Snare.
- Updated TR-808 Kick with two-stage envelopes for damping and 'tonk' articulation.
- Refactored pitch drift to use cent-based logarithmic frequency deviation.
- Implemented robust `activeVoices` tracking and disposal for metallic instruments.
- Standardized volume keys (cow -> cowbell) across the application.
- Added 'Randomize Techno' feature with haptic feedback.
- Updated default Euclidean patterns to techno foundation.
Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com>
---
src/components/DrumsView.tsx | 38 +++++++++++++++++++++++++++++---
src/components/MixerView.tsx | 6 ++---
src/components/SequencerLoop.tsx | 4 ++++
src/logic/DrumUtils.ts | 33 +++++++++++++++++++++++----
src/logic/drums/TR808Clap.ts | 8 ++++---
src/logic/drums/TR808Cowbell.ts | 22 +++++++++++-------
src/logic/drums/TR808HiHat.ts | 26 ++++++++++++----------
src/logic/drums/TR808Kick.ts | 23 ++++++++++++++-----
src/logic/drums/TR808Snare.ts | 10 ++++++---
src/logic/drums/TR909Kick.ts | 10 ++++++---
src/logic/drums/TR909Snare.ts | 18 +++++++--------
src/store/audioStore.ts | 30 +++++++++++++++++++++----
src/store/instrumentStore.ts | 5 +++--
13 files changed, 173 insertions(+), 60 deletions(-)
diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx
index 7d191e90..9b335bf0 100644
--- a/src/components/DrumsView.tsx
+++ b/src/components/DrumsView.tsx
@@ -30,13 +30,45 @@ export function DrumsView() {
if (drumMachine) drumMachine.setSaturation(v)
}
+ const randomizeTechno = () => {
+ // Haptic Feedback for randomization
+ if (window.Telegram?.WebApp?.HapticFeedback) {
+ window.Telegram.WebApp.HapticFeedback.impactOccurred('medium')
+ }
+
+ // Apply Techno Foundation defaults
+ updateDrum('kick', { steps: 16, pulses: 4, rotate: 0, probability: 1.0 })
+ updateDrum('snare', { steps: 16, pulses: 4, rotate: 4, probability: 1.0 })
+ updateDrum('hihat', { steps: 16, pulses: 12, rotate: 0, probability: 1.0 })
+ updateDrum('hihatOpen', { steps: 16, pulses: 4, rotate: 2, probability: 1.0 })
+ updateDrum('clap', { steps: 16, pulses: 2, rotate: 4, probability: 1.0 })
+ updateDrum('cowbell', { steps: 16, pulses: 3, rotate: 2, probability: 0.8 })
+ }
+
return (
-
Настройки
+
+
Настройки
+
+
setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)}
+ onChange={(v) => setVolume(d.id === 'cowbell' ? 'cowbell' : d.id, v)}
size={40}
/>
diff --git a/src/components/MixerView.tsx b/src/components/MixerView.tsx
index cadf2b00..8236d21c 100644
--- a/src/components/MixerView.tsx
+++ b/src/components/MixerView.tsx
@@ -58,10 +58,10 @@ export function MixerView() {
size={48}
/>
setVolume('cow', v)}
+ onChange={(v) => setVolume('cowbell', v)}
size={48}
/>
> 14) ^ (state >> 13)) & 1;
+ state = ((state << 1) | bit) & 0x7FFF;
+ data[i] = (state & 1) ? 1 : -1;
+ }
+ return buffer;
+}
diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts
index 3334a6fe..6b8b19af 100644
--- a/src/logic/drums/TR808Clap.ts
+++ b/src/logic/drums/TR808Clap.ts
@@ -12,6 +12,8 @@ export class TR808Clap {
}
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,7 +38,7 @@ 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);
diff --git a/src/logic/drums/TR808Cowbell.ts b/src/logic/drums/TR808Cowbell.ts
index 3dd1d4fd..0a94afdc 100644
--- a/src/logic/drums/TR808Cowbell.ts
+++ b/src/logic/drums/TR808Cowbell.ts
@@ -2,11 +2,13 @@ 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
@@ -15,6 +17,8 @@ export class TR808Cowbell {
const osc1 = new Tone.Oscillator(freq1, "square");
const osc2 = new Tone.Oscillator(freq2, "square");
+ osc1.phase = Math.random() * 360;
+ osc2.phase = Math.random() * 360;
const mixGain = new Tone.Gain(0.5);
const bpf = new Tone.Filter(applyVariance(800, 0.02), "bandpass");
@@ -30,10 +34,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], gain: vca };
+ this.activeVoices.add(voice);
osc1.start(time).stop(time + decayTime);
osc2.start(time).stop(time + decayTime);
@@ -45,15 +50,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..077654c5 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");
@@ -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, 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 +68,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..49433b21 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
@@ -19,22 +21,31 @@ export class TR808Kick {
masterGain.connect(this.destination);
// Micro-randomization using shared utilities
- const tuneDrift = applyPitchDrift(tune, 1.0);
+ const tuneDrift = applyPitchDrift(tune, 1.0); // +/- 1 cent
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
+ // Two-stage Pitch Envelope:
+ // 1. Rapid drop (membrane hit 'tonk') from startFreq to midFreq in 20ms
+ // 2. Slower drop to final tune in another 30ms (total 50ms sweep)
const startFreq = tuneDrift * 2.5;
+ const midFreq = tuneDrift * 1.2;
const endFreq = tuneDrift;
osc.frequency.setValueAtTime(startFreq, time);
+ osc.frequency.exponentialRampToValueAtTime(midFreq, time + 0.02);
osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05);
- // VCA Amp Envelope: Instant attack, adjustable exponential decay
+ // Two-stage VCA Amp Envelope:
+ // 1. Fast damping (20ms) to 50% volume (emulating diode damping)
+ // 2. Long exponential decay to zero
+ const dampingTime = 0.02;
+ const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay);
+
masterGain.gain.setValueAtTime(velocity, time);
- masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay);
+ masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + dampingTime);
+ 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..bd0e5011 100644
--- a/src/logic/drums/TR808Snare.ts
+++ b/src/logic/drums/TR808Snare.ts
@@ -15,6 +15,8 @@ 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;
@@ -40,10 +42,10 @@ 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);
+ 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
@@ -61,11 +63,13 @@ 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);
oscHigh.start(time).stop(time + vcaDecay);
+
+ // disposal is anchored to the noise source
noiseSrc.start(time).stop(time + snappyDecay + 0.1);
// Cleanup
diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts
index dd2582ed..90f08b2b 100644
--- a/src/logic/drums/TR909Kick.ts
+++ b/src/logic/drums/TR909Kick.ts
@@ -19,6 +19,8 @@ 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
@@ -53,7 +55,7 @@ export class TR909Kick {
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, 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)
@@ -67,7 +69,7 @@ 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
@@ -76,10 +78,12 @@ export class TR909Kick {
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);
+
+ // disposal is anchored to the noise source or body oscillator (longest)
noiseSrc.start(time).stop(time + clickDecay);
pulseOsc.start(time).stop(time + 0.005);
diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts
index 4841f055..d103844d 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,17 +9,13 @@ 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;
- }
+ // 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;
@@ -36,6 +32,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);
@@ -66,6 +63,7 @@ export class TR909Snare {
// 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 toneCutoff = 4000 + pitch * 4000;
const lpf = new Tone.Filter(applyVariance(toneCutoff, 0.02), "lowpass");
@@ -81,6 +79,8 @@ export class TR909Snare {
osc1.start(time).stop(time + vcaDecay);
osc2.start(time).stop(time + vcaDecay);
+
+ // disposal is anchored to the noise source (the longest signal component)
noiseSrc.start(time).stop(time + snappyDecay + 0.1);
osc1.onstop = () => {
diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts
index c90d17e8..19dd9e0b 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
@@ -22,7 +23,7 @@ export interface AudioState {
hihat: number,
hihatOpen: number,
clap: number,
- cow: number,
+ cowbell: number,
pads: number
}
initialize: () => Promise
@@ -30,7 +31,7 @@ export interface AudioState {
setBpm: (bpm: number) => void
setSwing: (swing: number) => void
setCurrentStep: (step: number) => void
- setVolume: (channel: 'bass' | 'lead' | 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cow' | 'pads', value: number) => void
+ setVolume: (channel: 'bass' | 'lead' | 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell' | 'pads', value: number) => void
}
export const useAudioStore = create((set, get) => ({
@@ -43,7 +44,7 @@ export const useAudioStore = create((set, get) => ({
leadSynth: null,
drumMachine: null,
padSynth: null,
- volumes: { bass: 0.8, lead: 0.8, kick: 0.8, snare: 0.8, hihat: 0.8, hihatOpen: 0.8, clap: 0.8, cow: 0.8, pads: 0.5 },
+ volumes: { bass: 0.8, lead: 0.8, kick: 0.8, snare: 0.8, hihat: 0.8, hihatOpen: 0.8, clap: 0.8, cowbell: 0.8, pads: 0.5 },
initialize: async () => {
if (get().isInitialized) return
@@ -63,6 +64,27 @@ export const useAudioStore = create((set, get) => ({
Tone.Transport.bpm.value = get().bpm
Tone.Transport.swing = get().swing
+ // Initial sync from instrument store
+ const drumStore = useDrumStore.getState()
+ drums.setKit(drumStore.kit)
+ drums.setSaturation(drumStore.drive)
+
+ const instruments: ('kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell')[] =
+ ['kick', 'snare', 'hihat', 'hihatOpen', 'clap', 'cowbell']
+
+ instruments.forEach(id => {
+ drums.setDrumParams(id, drumStore[id].pitch, drumStore[id].decay)
+ })
+
+ // Sync initial volumes
+ const vols = get().volumes
+ drums.outputKick.gain.value = vols.kick
+ drums.outputSnare.gain.value = vols.snare
+ drums.outputHihat.gain.value = vols.hihat
+ drums.outputOpenHat.gain.value = vols.hihatOpen
+ drums.outputClap.gain.value = vols.clap
+ drums.outputCowbell.gain.value = vols.cowbell
+
set({
isInitialized: true,
bassSynth: bassSynth,
@@ -96,7 +118,7 @@ export const useAudioStore = create((set, get) => ({
if (channel === 'hihat') drumMachine.outputHihat.gain.value = value
if (channel === 'hihatOpen') drumMachine.outputOpenHat.gain.value = value
if (channel === 'clap') drumMachine.outputClap.gain.value = value
- if (channel === 'cow') drumMachine.outputCowbell.gain.value = value
+ if (channel === 'cowbell') drumMachine.outputCowbell.gain.value = value
}
if (channel === 'pads' && padSynth) padSynth.synth.volume.value = Tone.gainToDb(value)
diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts
index 9e96a9b2..360a29e2 100644
--- a/src/store/instrumentStore.ts
+++ b/src/store/instrumentStore.ts
@@ -44,12 +44,13 @@ interface DrumState {
}
export const useDrumStore = create((set) => ({
+ // Techno Foundation: Kick (16/4), Snare (16/4, rotate 4), Hi-Hat (16/12)
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) => ({