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
17 changes: 8 additions & 9 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))
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 individual drum channels to the master compressor for "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 Down
10 changes: 7 additions & 3 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ export class TR808Clap {

trigger(time: number, pitch: number, decay: number) {
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const bpf = new Tone.Filter(1000 + pitch * 1000, "bandpass");
// Micro-randomization: Filter Cutoff (+/- 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 Interval (+/- 2ms)
const snapInterval = 0.01 + (Math.random() * 0.004 - 0.002);
for (let i = 0; i < snapCount; i++) {
const snapTime = time + i * snapInterval;
gain.gain.setValueAtTime(1, snapTime);
Expand All @@ -29,7 +32,8 @@ export class TR808Clap {

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

Expand Down
9 changes: 8 additions & 1 deletion src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ 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;
});

// Micro-randomization: Filter Cutoff (+/- 2%)
bpf1.frequency.value = 3440 * (1 + (Math.random() * 0.04 - 0.02));
bpf2.frequency.value = 7100 * (1 + (Math.random() * 0.04 - 0.02));
hpf.frequency.value = 7000 * (1 + (Math.random() * 0.04 - 0.02));

// Routing Graph
// Oscillators -> MixGain -> [BPF1, BPF2] (Parallel) -> EnvGain -> HPF -> Destination
mixGain.connect(bpf1);
Expand All @@ -38,7 +44,8 @@ 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);
// Micro-randomization: Decay Time (+/- 2%)
const decayTime = (isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02)) * (1 + (Math.random() * 0.04 - 0.02));

// VCA Envelope
envGain.gain.setValueAtTime(1, time);
Expand Down
3 changes: 2 additions & 1 deletion src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export class TR808Kick {
// 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;
// Micro-randomization: Decay Time (+/- 2%)
const decayTime = (0.4 + decay * 2.6) * (1 + (Math.random() * 0.04 - 0.02));

// 808 Kick Core: Bridged-T Network emulation
const osc = new Tone.Oscillator(tune, "sine");
Expand Down
24 changes: 17 additions & 7 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ export class TR808Snare {
const toneBalance = pitch;

// 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");
// Micro-randomization: Pitch Drift (+/- 1Hz)
const lowDrift = (Math.random() * 2 - 1) * 1;
const highDrift = (Math.random() * 2 - 1) * 1;
const oscLow = new Tone.Oscillator(238 + lowDrift, "sine");
const oscHigh = new Tone.Oscillator(476 + highDrift, "sine");
oscLow.phase = Math.random() * 360; // Analog phase randomization
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 @@ -32,26 +37,31 @@ export class TR808Snare {

masterTonalGain.gain.setValueAtTime(1, time);
// Tonal body decay is short (~200ms)
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
// Micro-randomization: Tonal Decay (+/- 2%)
const tonalDecay = 0.2 * (1 + (Math.random() * 0.04 - 0.02));
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + tonalDecay);

// 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 (+/- 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;
// Micro-randomization: Snappy Decay (+/- 2%)
const snappyDecay = (0.25 + snappy * 0.15) * (1 + (Math.random() * 0.04 - 0.02));

snappyGain.gain.setValueAtTime(0.8, 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 + tonalDecay);
oscHigh.start(time).stop(time + tonalDecay);
noiseSrc.start(time).stop(time + snappyDecay + 0.1);

// Cleanup
Expand Down
16 changes: 11 additions & 5 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ export class TR909Kick {
// 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;
// Micro-randomization: Decay Time (+/- 2%)
const decayTime = (0.3 + decay * 0.3) * (1 + (Math.random() * 0.04 - 0.02));

// 909 Kick Body: Triangle Oscillator
const bodyOsc = new Tone.Oscillator(tune * 4.7, "triangle");
// Micro-randomization: Pitch Drift (+/- 0.5Hz)
const bodyDrift = (Math.random() * 2 - 1) * 0.5;
const bodyOsc = new Tone.Oscillator(tune * 4.7 + bodyDrift, "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 startFreq = tune * 4.7 + bodyDrift;
const endFreq = tune + bodyDrift;

bodyOsc.frequency.setValueAtTime(startFreq, time);
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1);
Expand All @@ -39,7 +43,9 @@ export class TR909Kick {

// 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 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
40 changes: 27 additions & 13 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,56 @@ export class TR909Snare {
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 drift1 = (Math.random() * 2 - 1) * 1;
const drift2 = (Math.random() * 2 - 1) * 1;

const osc1 = new Tone.Oscillator((freq1 + drift1) * 2, "triangle");
const osc2 = new Tone.Oscillator((freq2 + drift2) * 2, "triangle");
osc1.phase = Math.random() * 360; // Analog phase randomization
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 (Tweak from 50ms as per research)
const sweepTime = 0.03;
osc1.frequency.setValueAtTime((freq1 + drift1) * 2, time);
osc1.frequency.exponentialRampToValueAtTime(freq1 + drift1, time + sweepTime);
osc2.frequency.setValueAtTime((freq2 + drift2) * 2, time);
osc2.frequency.exponentialRampToValueAtTime(freq2 + drift2, time + sweepTime);

tonalGain.gain.setValueAtTime(1, time);
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
// Micro-randomization: Tonal Decay (+/- 2%)
const tonalDecay = 0.2 * (1 + (Math.random() * 0.04 - 0.02));
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + tonalDecay);

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const hpf = new Tone.Filter(1000, "highpass"); // HPF to protect fundamental
// Micro-randomization: Filter Cutoff (+/- 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;
// Micro-randomization: Snappy Decay (+/- 2%)
const snappyDecay = (0.1 + snappy * 0.4) * (1 + (Math.random() * 0.04 - 0.02));

noiseGain.gain.setValueAtTime(0.7, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay);

osc1.start(time).stop(time + 0.2);
osc2.start(time).stop(time + 0.2);
osc1.start(time).stop(time + tonalDecay);
osc2.start(time).stop(time + tonalDecay);
noiseSrc.start(time).stop(time + snappyDecay + 0.1);

osc1.onstop = () => {
Expand Down