diff --git a/src/logic/DrumMachine.ts b/src/logic/DrumMachine.ts index e7cef13a..7b2c8bc9 100644 --- a/src/logic/DrumMachine.ts +++ b/src/logic/DrumMachine.ts @@ -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) @@ -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), diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index ebdaa83e..021d20c6 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -12,7 +12,9 @@ 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); @@ -20,7 +22,8 @@ export class TR808Clap { // 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); @@ -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); diff --git a/src/logic/drums/TR808HiHat.ts b/src/logic/drums/TR808HiHat.ts index 2208b50a..8f69bfe6 100644 --- a/src/logic/drums/TR808HiHat.ts +++ b/src/logic/drums/TR808HiHat.ts @@ -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); @@ -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); diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index f477d531..40944abc 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -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"); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index df3804a1..f1a30f4a 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -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); @@ -32,12 +37,16 @@ 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); @@ -45,13 +54,14 @@ export class TR808Snare { 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 diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index da99d9f1..bdd1e29f 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -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); @@ -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); diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 080bd03e..f3cf645d 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -19,28 +19,41 @@ 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); @@ -48,13 +61,14 @@ export class TR909Snare { 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 = () => {