Настройки
-
- {(['808', '909'] as const).map(k => (
-
- ))}
+
+
{
+ setDrive(v)
+ if (drumMachine) drumMachine.setSaturation(v)
+ }}
+ size={40}
+ />
+
+ {(['808', '909'] as const).map(k => (
+
+ ))}
+
diff --git a/src/components/SequencerLoop.tsx b/src/components/SequencerLoop.tsx
index bd7a08e7..b908379d 100644
--- a/src/components/SequencerLoop.tsx
+++ b/src/components/SequencerLoop.tsx
@@ -65,11 +65,13 @@ export function SequencerLoop() {
// 1. Drums (Euclidean - using cached patterns)
const patterns = drumPatternsRef.current
- if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time)
- if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time)
- if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time)
- if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time)
- if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time)
+ // Trigger with slight velocity randomization to simulate analog hit variance
+ const drumVel = () => 0.7 + Math.random() * 0.2
+ if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time, drumVel())
+ if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time, drumVel())
+ if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time, drumVel())
+ if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time, drumVel())
+ if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time, drumVel())
// 2. Bass (Sting logic)
const bassStep = currentBass.pattern[step]
diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts
index 47df2ded..882799d3 100644
--- a/src/logic/DrumMachine.ts
+++ b/src/logic/DrumMachine.ts
@@ -97,6 +97,10 @@ export class DrumMachine {
this.params[drum] = { pitch, decay }
}
+ setSaturation(amount: number) {
+ this.shaper.curve = this.makeDistortionCurve(amount)
+ }
+
triggerDrum(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', time: number, velocity: number = 0.8) {
const p = this.params[drum]
const kit808 = this.kit808
@@ -104,19 +108,19 @@ export class DrumMachine {
if (this.currentKit === '808') {
switch (drum) {
- case 'kick': kit808.kick.trigger(time, p.pitch, p.decay); break
- case 'snare': kit808.snare.trigger(time, p.pitch, p.decay); break
- case 'hihat': kit808.hihat.trigger(time, false, p.pitch, p.decay); break
- case 'hihatOpen': kit808.hihatOpen.trigger(time, true, p.pitch, p.decay); break
- case 'clap': kit808.clap.trigger(time, p.pitch, p.decay); break
+ case 'kick': kit808.kick.trigger(time, p.pitch, p.decay, velocity); break
+ case 'snare': kit808.snare.trigger(time, p.pitch, p.decay, velocity); break
+ case 'hihat': kit808.hihat.trigger(time, false, p.pitch, p.decay, velocity); break
+ case 'hihatOpen': kit808.hihatOpen.trigger(time, true, p.pitch, p.decay, velocity); break
+ case 'clap': kit808.clap.trigger(time, p.pitch, p.decay, velocity); break
}
} else {
switch (drum) {
- case 'kick': kit909.kick.trigger(time, p.pitch, p.decay); break
- case 'snare': kit909.snare.trigger(time, p.pitch, p.decay); break
- case 'hihat': kit909.hihat.trigger(time, false, p.pitch, p.decay); break
- case 'hihatOpen': kit909.hihatOpen.trigger(time, true, p.pitch, p.decay); break
- case 'clap': kit909.clap.trigger(time, p.pitch, p.decay); break
+ case 'kick': kit909.kick.trigger(time, p.pitch, p.decay, velocity); break
+ case 'snare': kit909.snare.trigger(time, p.pitch, p.decay, velocity); break
+ case 'hihat': kit909.hihat.trigger(time, false, p.pitch, p.decay, velocity); break
+ case 'hihatOpen': kit909.hihatOpen.trigger(time, true, p.pitch, p.decay, velocity); break
+ case 'clap': kit909.clap.trigger(time, p.pitch, p.decay, velocity); break
}
}
}
diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts
index 989f3c88..1c8fcedc 100644
--- a/src/logic/drums/TR808Clap.ts
+++ b/src/logic/drums/TR808Clap.ts
@@ -10,7 +10,7 @@ export class TR808Clap {
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
}
- trigger(time: number, pitch: number, decay: number) {
+ trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const filterVariance = 1 + (Math.random() * 0.04 - 0.02); // +/- 2% filter
const bpf = new Tone.Filter((1000 + pitch * 1000) * filterVariance, "bandpass");
@@ -26,8 +26,8 @@ export class TR808Clap {
for (let i = 0; i < snapCount; i++) {
const snapTime = time + i * snapInterval;
- gain.gain.setValueAtTime(1, snapTime);
- gain.gain.exponentialRampToValueAtTime(0.1, snapTime + snapInterval * 0.8);
+ gain.gain.setValueAtTime(velocity, snapTime);
+ gain.gain.exponentialRampToValueAtTime(0.1 * velocity, snapTime + snapInterval * 0.8);
}
// Final decay
@@ -35,7 +35,7 @@ export class TR808Clap {
const decayTimeBase = 0.1 + decay * 0.5;
const decayTime = decayTimeBase * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay
- gain.gain.setValueAtTime(1, finalDecayStart);
+ gain.gain.setValueAtTime(velocity, finalDecayStart);
gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime);
noiseSrc.start(time).stop(finalDecayStart + decayTime);
diff --git a/src/logic/drums/TR808HiHat.ts b/src/logic/drums/TR808HiHat.ts
index 01f3854c..aef1b106 100644
--- a/src/logic/drums/TR808HiHat.ts
+++ b/src/logic/drums/TR808HiHat.ts
@@ -5,7 +5,7 @@ export class TR808HiHat {
constructor(private destination: Tone.ToneAudioNode) { }
- trigger(time: number, isOpen: boolean, pitch: number, decay: number) {
+ trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) {
// Create nodes
const mixGain = new Tone.Gain(0.15);
const bpf1 = new Tone.Filter(3440, "bandpass");
@@ -48,7 +48,7 @@ export class TR808HiHat {
const decayTime = decayBase * (1 + (Math.random() * 0.04 - 0.02)); // +/- 2% decay
// VCA Envelope
- envGain.gain.setValueAtTime(1, time);
+ envGain.gain.setValueAtTime(velocity, time);
envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);
// Scheduling
diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts
index fd15b475..0547f703 100644
--- a/src/logic/drums/TR808Kick.ts
+++ b/src/logic/drums/TR808Kick.ts
@@ -3,7 +3,7 @@ import * as Tone from 'tone'
export class TR808Kick {
constructor(private destination: Tone.ToneAudioNode) { }
- trigger(time: number, pitch: number, decay: number) {
+ trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
// 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
@@ -17,6 +17,12 @@ export class TR808Kick {
osc.connect(masterGain);
masterGain.connect(this.destination);
+ // Click layer (roadmap item 7: Fast Pitch Sweep for attack transient)
+ const clickOsc = new Tone.Oscillator(tune * 5, "sine");
+ const clickGain = new Tone.Gain(0);
+ clickOsc.connect(clickGain);
+ clickGain.connect(this.destination);
+
// Micro-randomization: Pitch Drift (+/- 0.5Hz)
const drift = (Math.random() * 2 - 1) * 0.5;
// VCA Decay variance (+/- 2%)
@@ -31,14 +37,22 @@ export class TR808Kick {
osc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchDropTime);
// VCA Amp Envelope: Instant attack, adjustable exponential decay
- masterGain.gain.setValueAtTime(1, time);
+ masterGain.gain.setValueAtTime(velocity, time);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay);
+ // Click Envelope (5ms)
+ clickGain.gain.setValueAtTime(velocity * 0.5, time);
+ clickGain.gain.exponentialRampToValueAtTime(0.001, time + 0.005);
+ clickOsc.frequency.exponentialRampToValueAtTime(tune, time + 0.005);
+
osc.start(time).stop(time + finalDecay);
+ clickOsc.start(time).stop(time + 0.01);
osc.onstop = () => {
osc.dispose();
masterGain.dispose();
+ clickOsc.dispose();
+ clickGain.dispose();
};
}
}
diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts
index 861e411a..42c62c3c 100644
--- a/src/logic/drums/TR808Snare.ts
+++ b/src/logic/drums/TR808Snare.ts
@@ -13,7 +13,7 @@ export class TR808Snare {
}
}
- trigger(time: number, pitch: number, snappy: number) {
+ trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
// pitch maps to tone balance here (balance between low and high modes)
const toneBalance = pitch;
@@ -40,7 +40,7 @@ export class TR808Snare {
gainHigh.connect(masterTonalGain);
masterTonalGain.connect(this.destination);
- masterTonalGain.gain.setValueAtTime(1, time);
+ masterTonalGain.gain.setValueAtTime(velocity, time);
// Tonal body decay is short (~200ms)
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);
@@ -54,7 +54,7 @@ export class TR808Snare {
noiseFilter.connect(snappyGain);
snappyGain.connect(this.destination);
- snappyGain.gain.setValueAtTime(0.8, time);
+ snappyGain.gain.setValueAtTime(0.8 * velocity, 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 0a44f1ad..5844573c 100644
--- a/src/logic/drums/TR909Kick.ts
+++ b/src/logic/drums/TR909Kick.ts
@@ -13,9 +13,12 @@ export class TR909Kick {
}
}
- trigger(time: number, pitch: number, decay: number) {
- // pitch: 0.5 -> 50Hz, maps to 45-55Hz
- const tune = 45 + pitch * 10;
+ trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
+ // In TR-909, the Tune knob controls the pitch envelope decay time
+ // Research: Pitch Env Decay 0.05s - 0.15s
+ const pitchEnvDecay = 0.15 - pitch * 0.1;
+ // Base frequency remains relatively stable (~50Hz)
+ const tune = 50;
// decay: 0.5 -> 0.45s, maps to 0.3-0.6s
const decayTime = 0.3 + decay * 0.3;
@@ -32,15 +35,15 @@ export class TR909Kick {
bodyOsc.connect(bodyGain);
bodyGain.connect(this.destination);
- // Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over 100ms
+ // Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over variable time
const startFreq = tune * 4.7 + drift;
const endFreq = tune + drift;
bodyOsc.frequency.setValueAtTime(startFreq, time);
- bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1);
+ bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchEnvDecay);
// VCA Envelope
- bodyGain.gain.setValueAtTime(1, time);
+ bodyGain.gain.setValueAtTime(velocity, time);
bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);
// Click Layer (Noise)
@@ -54,7 +57,7 @@ export class TR909Kick {
// Ultra short envelope (10-20ms) for the click
const clickDecay = 0.02 * (1 + (Math.random() * 0.04 - 0.02));
- noiseGain.gain.setValueAtTime(0.7, time);
+ noiseGain.gain.setValueAtTime(0.7 * velocity, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay);
bodyOsc.start(time).stop(time + vcaDecay);
diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts
index 8f77f539..63694c1a 100644
--- a/src/logic/drums/TR909Snare.ts
+++ b/src/logic/drums/TR909Snare.ts
@@ -14,7 +14,7 @@ export class TR909Snare {
}
}
- trigger(time: number, pitch: number, snappy: number) {
+ trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
// 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz
const freq1 = 160;
const freq2 = 220;
@@ -43,7 +43,7 @@ export class TR909Snare {
osc2.frequency.setValueAtTime(freq2 * 2 + drift, time);
osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepTime);
- tonalGain.gain.setValueAtTime(1, time);
+ tonalGain.gain.setValueAtTime(velocity, time);
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);
// Snappy Layer
@@ -58,7 +58,7 @@ export class TR909Snare {
lpf.connect(noiseGain);
noiseGain.connect(this.destination);
- noiseGain.gain.setValueAtTime(0.7, time);
+ noiseGain.gain.setValueAtTime(0.7 * velocity, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay);
osc1.start(time).stop(time + vcaDecay);
diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts
index 9e51b8bb..d9ee951c 100644
--- a/src/store/audioStore.ts
+++ b/src/store/audioStore.ts
@@ -93,10 +93,7 @@ export const useAudioStore = create
((set, get) => ({
if (channel === 'kick') drumMachine.outputKick.gain.value = value
if (channel === 'snare') drumMachine.outputSnare.gain.value = value
if (channel === 'hihat') drumMachine.outputHihat.gain.value = value
- // We use hihat output for both open and closed for now in TR808HiHat as it shares a node,
- // but we can scale the volume parameter independently here if we had separate synths.
- // For now, we'll map openHat to hihat channel, and clap to clap.
- if (channel === 'hihatOpen') drumMachine.outputHihat.gain.value = value // Shared
+ if (channel === 'hihatOpen') drumMachine.outputOpenHat.gain.value = value
if (channel === 'clap') drumMachine.outputClap.gain.value = value
}
diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts
index 07c1c23a..9f250d53 100644
--- a/src/store/instrumentStore.ts
+++ b/src/store/instrumentStore.ts
@@ -36,7 +36,7 @@ export const useDrumStore = create((set) => ({
snare: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 },
hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5 },
hihatOpen: { steps: 16, pulses: 2, rotate: 2, decay: 0.5, pitch: 0.5 },
- clap: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 },
+ clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5 },
kit: '909',
setParams: (drum, params) => set((state) => ({
[drum]: { ...state[drum], ...params }