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
44 changes: 20 additions & 24 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export class DrumMachine {

constructor() {
this.comp = new Tone.Compressor(-24, 4)
this.shaper = new Tone.WaveShaper(this.makeDistortionCurve(15))
this.shaper = new Tone.WaveShaper(this.makeDistortionCurve(20)) // Amount increased to 20 per research recommendation
this.shaper.oversample = '4x'
this.output = new Tone.Gain(1)
this.outputKick = new Tone.Gain(1)
this.outputSnare = new Tone.Gain(1)
Expand All @@ -50,14 +51,12 @@ 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)
// Route all individual drum outputs through the master compression/saturation chain for the "glue effect"
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 All @@ -81,6 +80,7 @@ export class DrumMachine {
const deg = Math.PI / 180
for (let i = 0; i < n_samples; ++i) {
let x = i * 2 / n_samples - 1
// Formula from research: (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x))
curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x))
}
return curve
Expand All @@ -101,26 +101,22 @@ 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.
// Map to openHat output gain for visualization/mixing if needed
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
21 changes: 13 additions & 8 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,37 @@ export class TR808Clap {

constructor(private destination: Tone.ToneAudioNode) {
const sampleRate = Tone.getContext().sampleRate;
this.noiseBuffer = Tone.getContext().createBuffer(1, sampleRate * 0.5, sampleRate);
this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, sampleRate * 0.5, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
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) {
// Micro-randomization (+/- 2%)
const filterVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
const snapVariance = 1 + (Math.random() * 0.04 - 0.02);

const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const bpf = new Tone.Filter(1000 + pitch * 1000, "bandpass");
const bpf = new Tone.Filter((1000 + pitch * 1000) * filterVariance, "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;
const snapInterval = 0.01 * snapVariance;
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 decayTime = (0.1 + decay * 0.5) * decayVariance;
gain.gain.setValueAtTime(velocity, finalDecayStart);
gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime);

noiseSrc.start(time).stop(finalDecayStart + decayTime);
Expand Down
16 changes: 13 additions & 3 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 All @@ -16,10 +16,15 @@ export class TR808HiHat {
// Pitch Multiplier (0.8x to 1.2x)
const pitchMultiplier = 0.8 + pitch * 0.4;

// Micro-randomization (+/- 2%)
const filterVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);

// Create 6 Square Wave Oscillators (Schmitt Trigger Matrix)
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;
osc.connect(mixGain);
return osc;
});
Expand All @@ -37,11 +42,16 @@ export class TR808HiHat {
bpf1.Q.value = 1.5;
bpf2.Q.value = 1.5;

// Randomized filter cutoffs
bpf1.frequency.value = 3440 * filterVariance;
bpf2.frequency.value = 7100 * filterVariance;
hpf.frequency.value = 7000 * filterVariance;

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

// VCA Envelope
envGain.gain.setValueAtTime(1, time);
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;

// Micro-randomization on decay time (+/- 2%)
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
// decay: 0.5 -> 1.7s, maps to 0.4-3.0s range
const decayTime = 0.4 + decay * 2.6;
const decayTime = (0.4 + decay * 2.6) * decayVariance;

// 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,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 + decayTime);

osc.start(time).stop(time + decayTime);
Expand Down
30 changes: 19 additions & 11 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@ export class TR808Snare {
constructor(private destination: Tone.ToneAudioNode) {
const sampleRate = Tone.getContext().sampleRate;
const bufferSize = sampleRate * 0.5; // 500ms
this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate);
this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, bufferSize, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
}

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 (+/- 2%)
const filterVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
const drift = (Math.random() * 2 - 1) * 1; // +/- 1Hz

// 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,28 +38,28 @@ 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);
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2 * decayVariance);

// 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");
const noiseFilter = new Tone.Filter(1800 * filterVariance, "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 snappyDecay = (0.25 + snappy * 0.15) * decayVariance;

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);
oscHigh.start(time).stop(time + 0.2);
oscLow.start(time).stop(time + 0.2 * decayVariance);
oscHigh.start(time).stop(time + 0.2 * decayVariance);
noiseSrc.start(time).stop(time + snappyDecay + 0.1);

// Cleanup
Expand Down
25 changes: 16 additions & 9 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,56 @@ export class TR909Kick {
constructor(private destination: Tone.ToneAudioNode) {
const sampleRate = Tone.getContext().sampleRate;
const bufferSize = sampleRate * 0.05; // 50ms click
this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate);
this.noiseBuffer = (Tone.getContext().rawContext as AudioContext).createBuffer(1, bufferSize, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; 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) {
// pitch: 0.5 -> 50Hz, maps to 45-55Hz
const tune = 45 + pitch * 10;

// Micro-randomization on filter and decay (+/- 2%)
const filterVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);

// decay: 0.5 -> 0.45s, maps to 0.3-0.6s
const decayTime = 0.3 + decay * 0.3;
const decayTime = (0.3 + decay * 0.3) * decayVariance;

// 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);
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
const noiseFilter = new Tone.Filter(1000 * filterVariance, "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);
const clickDecay = 0.02 * decayVariance;
noiseGain.gain.setValueAtTime(0.7 * velocity, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay);

bodyOsc.start(time).stop(time + decayTime);
Expand Down
Loading