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
40 changes: 17 additions & 23 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,13 @@ export class DrumMachine {

this.comp.chain(this.shaper, this.output, Tone.Destination)

// Let's bypass compression for individual drum channels for now,
// to simplify routing and allow strict analog synth modeling.
// We'll route them directly to destination or output
this.outputKick.connect(Tone.Destination)
this.outputSnare.connect(Tone.Destination)
this.outputHihat.connect(Tone.Destination)
this.outputOpenHat.connect(Tone.Destination)
this.outputClap.connect(Tone.Destination)
// Connect all drum channels to the master compressor/shaper chain
// for the "glue effect" and analog saturation.
this.outputKick.connect(this.comp)
this.outputSnare.connect(this.comp)
this.outputHihat.connect(this.comp)
this.outputOpenHat.connect(this.comp)
this.outputClap.connect(this.comp)

this.kit808 = {
kick: new TR808Kick(this.outputKick),
Expand Down Expand Up @@ -101,26 +100,21 @@ 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.hihat.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.hihat.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 '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':
// Reuse hihat logic but specify it's open
// Note: technically TR909 uses samples for open hats, but we'll use our analog emulation for now
kit909.hihat.trigger(time, true, p.pitch, p.decay);
// However, we need to route it to the right output if possible. Our TR808HiHat
// currently has one destination baked in at constructor. To mix them separately,
// we will need an architectural tweak or just use the same channel. Let's just trigger it.
kit909.hihat.trigger(time, true, p.pitch, p.decay, velocity);
break
case 'clap': kit909.clap.trigger(time, p.pitch, p.decay); break
case 'clap': kit909.clap.trigger(time, p.pitch, p.decay, velocity); break
}
}
}
Expand Down
22 changes: 15 additions & 7 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,35 @@ 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 bpf = new Tone.Filter(1000 + pitch * 1000, "bandpass");

// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const cutoff = (1000 + pitch * 1000) * (1 + (Math.random() * 0.04 - 0.02));
const bpf = new Tone.Filter(cutoff, "bandpass");
const gain = new Tone.Gain(0).connect(this.destination);

noiseSrc.connect(bpf);
bpf.connect(gain);

// Triple attack "snaps"
const snapCount = 3;
const snapInterval = 0.01;
// Micro-randomization: Snap Intervals (+/- 2%)
const snapInterval = 0.01 * (1 + (Math.random() * 0.04 - 0.02));
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);
// Velocity sensitivity: scale peak gain
gain.gain.setValueAtTime(velocity, snapTime);
gain.gain.exponentialRampToValueAtTime(0.1 * velocity, snapTime + snapInterval * 0.8);
}

// Final decay
const finalDecayStart = time + snapCount * snapInterval;
const decayTime = 0.1 + decay * 0.5;
gain.gain.setValueAtTime(1, finalDecayStart);
let decayTime = 0.1 + decay * 0.5;
// Micro-randomization: Decay Variance (+/- 2%)
decayTime *= (1 + (Math.random() * 0.04 - 0.02));

gain.gain.setValueAtTime(velocity, finalDecayStart);
gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime);

noiseSrc.start(time).stop(finalDecayStart + decayTime);
Expand Down
25 changes: 18 additions & 7 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ 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");
const bpf2 = new Tone.Filter(7100, "bandpass");

// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const bpf1Freq = 3440 * (1 + (Math.random() * 0.04 - 0.02));
const bpf2Freq = 7100 * (1 + (Math.random() * 0.04 - 0.02));

const bpf1 = new Tone.Filter(bpf1Freq, "bandpass");
const bpf2 = new Tone.Filter(bpf2Freq, "bandpass");
const envGain = new Tone.Gain(0);
const hpf = new Tone.Filter(7000, "highpass");

Expand All @@ -18,8 +23,11 @@ export class TR808HiHat {

// Create 6 Square Wave Oscillators (Schmitt Trigger Matrix)
const oscillators = this.frequencies.map(freq => {
const drift = (Math.random() - 0.5) * 4; // Analog drift
// Analog drift: +/- 2Hz
const drift = (Math.random() - 0.5) * 4;
const osc = new Tone.Oscillator(freq * pitchMultiplier + drift, "square");
// Analog phase randomization: ensure each hit starts with random phase
osc.phase = Math.random() * 360;
osc.connect(mixGain);
return osc;
});
Expand All @@ -38,10 +46,13 @@ export class TR808HiHat {
bpf2.Q.value = 1.5;

// Decay: Closed Hat (40-60ms), Open Hat (300-500ms)
const decayTime = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02);
let decayTime = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02);

// Micro-randomization: Decay Variance (+/- 2%)
decayTime *= (1 + (Math.random() * 0.04 - 0.02));

// VCA Envelope
envGain.gain.setValueAtTime(1, time);
// VCA Envelope: scale by velocity
envGain.gain.setValueAtTime(velocity, time);
envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

// Scheduling
Expand Down
12 changes: 8 additions & 4 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ 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
const decayTime = 0.4 + decay * 2.6;
let decayTime = 0.4 + decay * 2.6;

// Micro-randomization: Decay Variance (+/- 2%)
decayTime *= (1 + (Math.random() * 0.04 - 0.02));

// 808 Kick Core: Bridged-T Network emulation
const osc = new Tone.Oscillator(tune, "sine");
Expand All @@ -17,7 +20,7 @@ export class TR808Kick {
osc.connect(masterGain);
masterGain.connect(this.destination);

// Micro-randomization: Pitch Drift (+/- 1-2 cents or ~0.5Hz)
// Micro-randomization: Pitch Drift (+/- 0.5Hz)
const drift = (Math.random() * 2 - 1) * 0.5;

// Pitch Envelope: Start high (Tune * 2.5) and drop quickly to simulate the membrane hit ('tonk')
Expand All @@ -29,7 +32,8 @@ export class TR808Kick {
osc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchDropTime);

// VCA Amp Envelope: Instant attack, adjustable exponential decay
masterGain.gain.setValueAtTime(1, time);
// Velocity sensitivity: scale peak gain
masterGain.gain.setValueAtTime(velocity, time);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

osc.start(time).stop(time + decayTime);
Expand Down
25 changes: 18 additions & 7 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ 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;

// Micro-randomization: Pitch Drift (+/- 1Hz)
const drift = (Math.random() * 2 - 1) * 1.0;

// 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research
const oscLow = new Tone.Oscillator(238, "sine");
const oscHigh = new Tone.Oscillator(476, "sine");
const oscLow = new Tone.Oscillator(238 + drift, "sine");
const oscHigh = new Tone.Oscillator(476 + drift, "sine");
oscLow.phase = Math.random() * 360;
oscHigh.phase = Math.random() * 360;

const gainLow = new Tone.Gain(1 - toneBalance);
const gainHigh = new Tone.Gain(toneBalance);
const masterTonalGain = new Tone.Gain(0);
Expand All @@ -30,24 +36,29 @@ export class TR808Snare {
gainHigh.connect(masterTonalGain);
masterTonalGain.connect(this.destination);

masterTonalGain.gain.setValueAtTime(1, time);
// Velocity sensitivity: scale peak gain
masterTonalGain.gain.setValueAtTime(velocity, time);
// Tonal body decay is short (~200ms)
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
// High-pass filter (>1800Hz) to prevent phase trap with tonal body
const noiseFilter = new Tone.Filter(1800, "highpass");
// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const noiseCutoff = 1800 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(noiseCutoff, "highpass");
const snappyGain = new Tone.Gain(0);

noiseSrc.connect(noiseFilter);
noiseFilter.connect(snappyGain);
snappyGain.connect(this.destination);

// Snappy decay range: 0.25s to 0.4s
const snappyDecay = 0.25 + snappy * 0.15;
let snappyDecay = 0.25 + snappy * 0.15;
// Micro-randomization: Decay Variance (+/- 2%)
snappyDecay *= (1 + (Math.random() * 0.04 - 0.02));

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 + 0.2);
Expand Down
21 changes: 14 additions & 7 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,40 @@ 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;
let decayTime = 0.3 + decay * 0.3;

// Micro-randomization: Decay Variance (+/- 2%)
decayTime *= (1 + (Math.random() * 0.04 - 0.02));

// 909 Kick Body: Triangle Oscillator
const bodyOsc = new Tone.Oscillator(tune * 4.7, "triangle");
bodyOsc.phase = Math.random() * 360; // Analog phase randomization
const bodyGain = new Tone.Gain(0);

bodyOsc.connect(bodyGain);
bodyGain.connect(this.destination);

// Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over 100ms
const startFreq = tune * 4.7;
const endFreq = tune;
const drift = (Math.random() * 2 - 1) * 0.5; // Pitch Drift (+/- 0.5Hz)
const startFreq = tune * 4.7 + drift;
const endFreq = tune + drift;

bodyOsc.frequency.setValueAtTime(startFreq, time);
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1);

// VCA Envelope
bodyGain.gain.setValueAtTime(1, time);
// VCA Envelope: scale by velocity
bodyGain.gain.setValueAtTime(velocity, time);
bodyGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

// Click Layer (Noise)
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const noiseFilter = new Tone.Filter(1000, "highpass"); // HPF > 1kHz to avoid phase trap
// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const noiseCutoff = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(noiseCutoff, "highpass"); // HPF > 1kHz to avoid phase trap
const noiseGain = new Tone.Gain(0);

noiseSrc.connect(noiseFilter);
Expand Down
42 changes: 29 additions & 13 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,59 @@ 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;

const osc1 = new Tone.Oscillator(freq1 * 2, "triangle");
const osc2 = new Tone.Oscillator(freq2 * 2, "triangle");
// Micro-randomization: Pitch Drift (+/- 1Hz)
const drift = (Math.random() * 2 - 1) * 1.0;

const osc1 = new Tone.Oscillator(freq1 * 2 + drift, "triangle");
const osc2 = new Tone.Oscillator(freq2 * 2 + drift, "triangle");
osc1.phase = Math.random() * 360;
osc2.phase = Math.random() * 360;

const tonalGain = new Tone.Gain(0);

osc1.connect(tonalGain);
osc2.connect(tonalGain);
tonalGain.connect(this.destination);

// 2x Pitch Sweep over 50ms
osc1.frequency.setValueAtTime(freq1 * 2, time);
osc1.frequency.exponentialRampToValueAtTime(freq1, time + 0.05);
osc2.frequency.setValueAtTime(freq2 * 2, time);
osc2.frequency.exponentialRampToValueAtTime(freq2, time + 0.05);
// 2x Pitch Sweep over 30ms (Adjusted based on research for TR-909 Snare)
const sweepTime = 0.03;
osc1.frequency.setValueAtTime(freq1 * 2 + drift, time);
osc1.frequency.exponentialRampToValueAtTime(freq1 + drift, time + sweepTime);
osc2.frequency.setValueAtTime(freq2 * 2 + drift, time);
osc2.frequency.exponentialRampToValueAtTime(freq2 + drift, time + sweepTime);

tonalGain.gain.setValueAtTime(1, time);
// Velocity sensitivity: scale peak gain
tonalGain.gain.setValueAtTime(velocity, time);
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const hpf = new Tone.Filter(1000, "highpass"); // HPF to protect fundamental

// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const hpfCutoff = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const lpfCutoff = (4000 + pitch * 4000) * (1 + (Math.random() * 0.04 - 0.02));

const hpf = new Tone.Filter(hpfCutoff, "highpass"); // HPF to protect fundamental
// LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz
const lpf = new Tone.Filter(4000 + pitch * 4000, "lowpass");
const lpf = new Tone.Filter(lpfCutoff, "lowpass");
const noiseGain = new Tone.Gain(0);

noiseSrc.connect(hpf);
hpf.connect(lpf);
lpf.connect(noiseGain);
noiseGain.connect(this.destination);

const snappyDecay = 0.1 + snappy * 0.4;
// Snappy decay range: 0.1s to 0.5s
let snappyDecay = 0.1 + snappy * 0.4;
// Micro-randomization: Decay Variance (+/- 2%)
snappyDecay *= (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 + snappyDecay);

osc1.start(time).stop(time + 0.2);
Expand Down
8 changes: 4 additions & 4 deletions src/store/instrumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ interface DrumState {

export const useDrumStore = create<DrumState>((set) => ({
kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5 },
snare: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 },
hihat: { steps: 16, pulses: 8, rotate: 0, decay: 0.5, pitch: 0.5 },
hihatOpen: { steps: 16, pulses: 0, rotate: 0, decay: 0.5, pitch: 0.5 },
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 },
kit: '808',
kit: '909',
setParams: (drum, params) => set((state) => ({
[drum]: { ...state[drum], ...params }
})),
Expand Down