Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions src/components/DrumsView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import { useDrumStore } from '../store/instrumentStore'
import { Knob } from './Knob'
import { useBassStore, useHarmonyStore } from '../store/instrumentStore'
Expand Down Expand Up @@ -25,28 +26,42 @@ export function DrumsView() {
if (drumMachine) drumMachine.setKit(newKit)
}

const [drive, setDrive] = React.useState(15)

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<TransportControls title="Драм-машина" />

<section className="card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>Настройки</h3>
<div style={{ display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.05)', padding: '4px', borderRadius: '8px' }}>
{(['808', '909'] as const).map(k => (
<button
key={k}
onClick={() => handleKitChange(k)}
style={{
padding: '4px 12px',
fontSize: '11px',
borderRadius: '6px',
background: kit === k ? 'var(--tg-theme-button-color)' : 'transparent',
color: kit === k ? 'white' : 'inherit',
border: 'none'
}}
>{k}</button>
))}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<Knob
label="DRIVE"
value={drive}
min={0} max={100} step={1}
onChange={(v) => {
setDrive(v)
if (drumMachine) drumMachine.setSaturation(v)
}}
size={40}
/>
<div style={{ display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.05)', padding: '4px', borderRadius: '8px' }}>
{(['808', '909'] as const).map(k => (
<button
key={k}
onClick={() => handleKitChange(k)}
style={{
padding: '4px 12px',
fontSize: '11px',
borderRadius: '6px',
background: kit === k ? 'var(--tg-theme-button-color)' : 'transparent',
color: kit === k ? 'white' : 'inherit',
border: 'none'
}}
>{k}</button>
))}
</div>
</div>
</div>

Expand Down
12 changes: 7 additions & 5 deletions src/components/SequencerLoop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 14 additions & 10 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,30 @@ 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
const kit909 = this.kit909

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
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -26,16 +26,16 @@ 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
const finalDecayStart = time + snapCount * snapInterval;
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);
Expand Down
4 changes: 2 additions & 2 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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%)
Expand All @@ -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();
};
}
}
6 changes: 3 additions & 3 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand All @@ -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);
Expand Down
17 changes: 10 additions & 7 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
5 changes: 1 addition & 4 deletions src/store/audioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,7 @@ export const useAudioStore = create<AudioState>((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
}

Expand Down
2 changes: 1 addition & 1 deletion src/store/instrumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const useDrumStore = create<DrumState>((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 }
Expand Down