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

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

const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap', params: any) => {
setParams(drum, params)
Expand All @@ -31,7 +31,16 @@ export function DrumsView() {

<section className="card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>Настройки</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<h3 style={{ margin: 0 }}>Настройки</h3>
<Knob
label="DRIVE"
value={saturation}
min={0} max={100} step={1}
onChange={(v) => setSaturation(v)}
size={40}
/>
</div>
<div style={{ display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.05)', padding: '4px', borderRadius: '8px' }}>
{(['808', '909'] as const).map(k => (
<button
Expand Down
11 changes: 6 additions & 5 deletions src/components/SequencerLoop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ 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)
const drumVelocity = () => 0.7 + Math.random() * 0.3 // Randomize velocity 0.7-1.0
if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time, drumVelocity())
if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time, drumVelocity())
if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time, drumVelocity())
if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time, drumVelocity())
if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time, drumVelocity())

// 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 @@ -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
11 changes: 7 additions & 4 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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
Expand All @@ -27,9 +27,11 @@ export class TR909Kick {
// 909 Kick Body: Triangle Oscillator
const bodyOsc = new Tone.Oscillator(tune * 4.7 + drift, "triangle");
bodyOsc.phase = Math.random() * 360;
const bodyFilter = new Tone.Filter(1000 * filterVariance, "lowpass"); // Smoothing filter per research
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
Expand All @@ -40,7 +42,7 @@ export class TR909Kick {
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1);

// 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 +56,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
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
16 changes: 11 additions & 5 deletions src/store/audioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ export interface AudioState {
clap: number,
pads: number
}
saturation: number
initialize: () => Promise<void>
togglePlay: () => void
setBpm: (bpm: number) => void
setSwing: (swing: number) => void
setCurrentStep: (step: number) => void
setVolume: (channel: 'bass' | 'lead' | 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'pads', value: number) => void
setSaturation: (value: number) => void
}

export const useAudioStore = create<AudioState>((set, get) => ({
Expand All @@ -43,6 +45,7 @@ export const useAudioStore = create<AudioState>((set, get) => ({
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, pads: 0.5 },
saturation: 15,

initialize: async () => {
if (get().isInitialized) return
Expand Down Expand Up @@ -93,10 +96,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 All @@ -113,5 +113,11 @@ export const useAudioStore = create<AudioState>((set, get) => ({
set({ swing })
},

setCurrentStep: (currentStep: number) => set({ currentStep })
setCurrentStep: (currentStep: number) => set({ currentStep }),

setSaturation: (value: number) => {
const { drumMachine } = get()
if (drumMachine) drumMachine.setSaturation(value)
set({ saturation: value })
}
}))