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
43 changes: 17 additions & 26 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@ export class DrumMachine {
this.outputOpenHat = new Tone.Gain(1)
this.outputClap = new Tone.Gain(1)

this.comp.chain(this.shaper, this.output, Tone.Destination)
// Route individual drum channels through Compressor and WaveShaper
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)

// 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)
this.comp.chain(this.shaper, this.output, Tone.Destination)

this.kit808 = {
kick: new TR808Kick(this.outputKick),
Expand Down Expand Up @@ -101,26 +99,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.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 '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.
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.hihat.trigger(time, true, p.pitch, p.decay, velocity); break
case 'clap': kit909.clap.trigger(time, p.pitch, p.decay, velocity); break
}
}
}
Expand Down
23 changes: 16 additions & 7 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,36 @@ 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: BPF Cutoff (+/- 2%)
const bpfFreq = (1000 + pitch * 1000) * (1 + (Math.random() * 0.04 - 0.02));
const bpf = new Tone.Filter(bpfFreq, "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 Interval (+/- 2%)
const baseSnapInterval = 0.01;
const snapInterval = baseSnapInterval * (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);
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);
const baseDecay = 0.1 + decay * 0.5;
// Micro-randomization: Final Decay Time (+/- 2%)
const decayTime = baseDecay * (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
21 changes: 15 additions & 6 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ 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 Cutoffs (+/- 2%)
const bpf1Freq = 3440 * (1 + (Math.random() * 0.04 - 0.02));
const bpf1 = new Tone.Filter(bpf1Freq, "bandpass");

const bpf2Freq = 7100 * (1 + (Math.random() * 0.04 - 0.02));
const bpf2 = new Tone.Filter(bpf2Freq, "bandpass");

const envGain = new Tone.Gain(0);
const hpf = new Tone.Filter(7000, "highpass");

Expand All @@ -20,6 +26,7 @@ export class TR808HiHat {
const oscillators = this.frequencies.map(freq => {
const drift = (Math.random() - 0.5) * 4; // Analog drift
const osc = new Tone.Oscillator(freq * pitchMultiplier + drift, "square");
osc.phase = Math.random() * 360; // Analog phase randomization
osc.connect(mixGain);
return osc;
});
Expand All @@ -38,10 +45,12 @@ 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);
const baseDecay = isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02);
// Micro-randomization: Decay (+/- 2%)
const decayTime = baseDecay * (1 + (Math.random() * 0.04 - 0.02));

// VCA Envelope
envGain.gain.setValueAtTime(1, time);
// VCA Envelope with velocity scaling
envGain.gain.setValueAtTime(velocity, time);
envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

// Scheduling
Expand Down
11 changes: 7 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;
const baseDecay = 0.4 + decay * 2.6;

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

// 808 Kick Core: Bridged-T Network emulation
const osc = new Tone.Oscillator(tune, "sine");
Expand All @@ -28,8 +31,8 @@ export class TR808Kick {
osc.frequency.setValueAtTime(startFreq, time);
osc.frequency.exponentialRampToValueAtTime(endFreq, time + pitchDropTime);

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

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

// Analog phase randomization
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 +38,29 @@ 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 + 0.2);

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);

// Micro-randomization: Filter Cutoff (+/- 2%)
const filterFreq = 1800 * (1 + (Math.random() * 0.04 - 0.02));
// High-pass filter (>1800Hz) to prevent phase trap with tonal body
const noiseFilter = new Tone.Filter(1800, "highpass");
const noiseFilter = new Tone.Filter(filterFreq, "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;
const baseSnappyDecay = 0.25 + snappy * 0.15;
// Micro-randomization: Snappy Decay (+/- 2%)
const snappyDecay = baseSnappyDecay * (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
31 changes: 21 additions & 10 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,53 @@ 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;
const baseDecay = 0.3 + decay * 0.3;

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

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

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

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

// Aggressive Pitch Envelope: Start at Tune * 4.7 (~235Hz) and drop over 100ms
const startFreq = tune * 4.7;
const endFreq = tune;
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 with velocity scaling
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 (+/- 2%)
const filterFreq = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(filterFreq, "highpass"); // HPF > 1kHz to avoid phase trap
const noiseGain = new Tone.Gain(0);

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

// Ultra short envelope (10-20ms) for the click
const clickDecay = 0.02;
noiseGain.gain.setValueAtTime(0.7, time);
// Ultra short envelope (10-20ms) for the click, with randomization
const clickDecay = 0.02 * (1 + (Math.random() * 0.04 - 0.02));
noiseGain.gain.setValueAtTime(0.7 * velocity, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay);

bodyOsc.start(time).stop(time + decayTime);
Expand Down
40 changes: 29 additions & 11 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,61 @@ 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;

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

const osc1 = new Tone.Oscillator(freq1 * 2, "triangle");
const osc2 = new Tone.Oscillator(freq2 * 2, "triangle");

// Analog phase randomization
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 from 50ms based on research for 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);
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: HPF Cutoff (+/- 2%)
const hpfFreq = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const hpf = new Tone.Filter(hpfFreq, "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 baseLpfFreq = 4000 + pitch * 4000;
// Micro-randomization: LPF Cutoff (+/- 2%)
const lpfFreq = baseLpfFreq * (1 + (Math.random() * 0.04 - 0.02));
const lpf = new Tone.Filter(lpfFreq, "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;
const baseSnappyDecay = 0.1 + snappy * 0.4;
// Micro-randomization: Snappy Decay (+/- 2%)
const snappyDecay = baseSnappyDecay * (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