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
18 changes: 16 additions & 2 deletions src/components/DrumsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { bjorklund } from '../logic/bjorklund'
import { TransportControls } from './TransportControls'

export function DrumsView() {
const { kick, snare, hihat, hihatOpen, clap, kit, setParams, setKit } = useDrumStore()
const { kick, snare, hihat, hihatOpen, clap, kit, drive, setParams, setKit, setDrive } = useDrumStore()
const { drumMachine, volumes, setVolume } = useAudioStore()

const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', params: any) => {
Expand All @@ -25,14 +25,27 @@ export function DrumsView() {
if (drumMachine) drumMachine.setKit(newKit)
}

const handleDriveChange = (v: number) => {
setDrive(v)
if (drumMachine) drumMachine.setSaturation(v)
}

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' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<Knob
label="DRIVE"
value={drive}
min={0} max={100} step={1}
onChange={handleDriveChange}
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}
Expand All @@ -47,6 +60,7 @@ export function DrumsView() {
}}
>{k}</button>
))}
</div>
</div>
</div>

Expand Down
24 changes: 14 additions & 10 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export class DrumMachine {
this.currentKit = kit
}

setSaturation(amount: number) {
this.shaper.curve = this.makeDistortionCurve(amount)
}

setDrumParams(drum: string, pitch: number, decay: number) {
this.params[drum] = { pitch, decay }
}
Expand All @@ -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
}
}
}
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
4 changes: 2 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 Down Expand Up @@ -31,7 +31,7 @@ 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);

osc.start(time).stop(time + finalDecay);
Expand Down
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
18 changes: 12 additions & 6 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export class TR909Kick {
}
}

trigger(time: number, pitch: number, decay: number) {
trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
// pitch: 0.5 -> 50Hz, maps to 45-55Hz
const tune = 45 + pitch * 10;
// decay: 0.5 -> 0.45s, maps to 0.3-0.6s
const decayTime = 0.3 + decay * 0.3;
// Pitch envelope sweep duration: 0.15s to 0.05s as per research
const sweepDuration = 0.15 - pitch * 0.1;

// Micro-randomization
const drift = (Math.random() * 2 - 1) * 0.5; // +/- 0.5Hz drift
Expand All @@ -27,20 +29,23 @@ export class TR909Kick {
// 909 Kick Body: Triangle Oscillator
const bodyOsc = new Tone.Oscillator(tune * 4.7 + drift, "triangle");
bodyOsc.phase = Math.random() * 360;
// Lowpass filter to "round" the triangle wave as per research
const bodyFilter = new Tone.Filter(1000, "lowpass");
const bodyGain = new Tone.Gain(0);

bodyOsc.connect(bodyGain);
bodyOsc.connect(bodyFilter);
bodyFilter.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 sweepDuration
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 + sweepDuration);

// 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,14 +59,15 @@ 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);
noiseSrc.start(time).stop(time + clickDecay);

bodyOsc.onstop = () => {
bodyOsc.dispose();
bodyFilter.dispose();
bodyGain.dispose();
};
noiseSrc.onended = () => {
Expand Down
12 changes: 6 additions & 6 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 All @@ -37,13 +37,13 @@ export class TR909Snare {
tonalGain.connect(this.destination);

// 2x Pitch Sweep over 30ms (research says ~30ms for 909 Snare)
const sweepTime = 0.03;
const sweepDuration = 0.03;
osc1.frequency.setValueAtTime(freq1 * 2 + drift, time);
osc1.frequency.exponentialRampToValueAtTime(freq1 + drift, time + sweepTime);
osc1.frequency.exponentialRampToValueAtTime(freq1 + drift, time + sweepDuration);
osc2.frequency.setValueAtTime(freq2 * 2 + drift, time);
osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepTime);
osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepDuration);

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
6 changes: 5 additions & 1 deletion src/store/instrumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ interface DrumState {
hihatOpen: { steps: number, pulses: number, rotate: number, decay: number, pitch: number }
clap: { steps: number, pulses: number, rotate: number, decay: number, pitch: number }
kit: '808' | '909'
drive: number
setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', params: Partial<{ steps: number, pulses: number, rotate: number, decay: number, pitch: number }>) => void
setKit: (kit: '808' | '909') => void
setDrive: (drive: number) => void
}

export const useDrumStore = create<DrumState>((set) => ({
Expand All @@ -38,10 +40,12 @@ export const useDrumStore = create<DrumState>((set) => ({
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 },
kit: '909',
drive: 15,
setParams: (drum, params) => set((state) => ({
[drum]: { ...state[drum], ...params }
})),
setKit: (kit) => set({ kit })
setKit: (kit) => set({ kit }),
setDrive: (drive) => set({ drive })
}))

// Pad Store
Expand Down