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
18 changes: 8 additions & 10 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,24 @@ 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 individual drum channels to the master compressor 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),
snare: new TR808Snare(this.outputSnare),
hihat: new TR808HiHat(this.outputHihat),
hihat: new TR808HiHat(this.outputHihat, this.outputOpenHat),
clap: new TR808Clap(this.outputClap)
}

this.kit909 = {
kick: new TR909Kick(this.outputKick),
snare: new TR909Snare(this.outputSnare),
hihat: new TR808HiHat(this.outputHihat), // Shared hihat synthesis for now
hihat: new TR808HiHat(this.outputHihat, this.outputOpenHat), // Shared hihat synthesis for now
clap: new TR808Clap(this.outputClap)
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,27 @@ 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");
// Include +/- 2% micro-randomization for filter cutoff
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;
// Snap intervals (+/- 2% randomization)
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);
}

// Final decay
// Final decay (+/- 2% randomization)
const finalDecayStart = time + snapCount * snapInterval;
const decayTime = 0.1 + decay * 0.5;
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
21 changes: 15 additions & 6 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import * as Tone from 'tone'
export class TR808HiHat {
private frequencies = [205.3, 304.4, 369.6, 522.7, 800, 540];

constructor(private destination: Tone.ToneAudioNode) { }
constructor(private destination: Tone.ToneAudioNode, private openDestination?: Tone.ToneAudioNode) { }

trigger(time: number, isOpen: boolean, pitch: number, decay: number) {
// Create nodes
const mixGain = new Tone.Gain(0.15);
const bpf1 = new Tone.Filter(3440, "bandpass");
const bpf2 = new Tone.Filter(7100, "bandpass");

// Parallel BPFs with +/- 2% micro-randomization
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 @@ -20,6 +25,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 @@ -31,14 +37,17 @@ export class TR808HiHat {
bpf1.connect(envGain);
bpf2.connect(envGain);
envGain.connect(hpf);
hpf.connect(this.destination);

// Select destination
const dest = (isOpen && this.openDestination) ? this.openDestination : this.destination;
hpf.connect(dest);

// Filter Q values
bpf1.Q.value = 1.5;
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);
// Decay: Closed Hat (40-60ms), Open Hat (300-500ms). Include +/- 2% decay variance.
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
4 changes: 2 additions & 2 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export class TR808Kick {
trigger(time: number, pitch: number, decay: number) {
// 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;
// decay: 0.5 -> 1.7s, maps to 0.4-3.0s range. Include +/- 2% micro-randomization.
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: 16 additions & 8 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ export class TR808Snare {
// 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; // 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 @@ -31,21 +37,23 @@ export class TR808Snare {
masterTonalGain.connect(this.destination);

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

// 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");
// High-pass filter (>1800Hz) to prevent phase trap with tonal body. Include +/- 2% cutoff variance.
const snapCutoff = 1800 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(snapCutoff, "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;
// Snappy decay range: 0.25s to 0.4s. Include +/- 2% micro-randomization.
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);
Expand Down
22 changes: 14 additions & 8 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ export class TR909Kick {
trigger(time: number, pitch: number, decay: number) {
// 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;
// decay: 0.5 -> 0.45s, maps to 0.3-0.6s. Include +/- 2% micro-randomization.
const decayTime = (0.3 + decay * 0.3) * (1 + (Math.random() * 0.04 - 0.02));

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

// 909 Kick Body: Triangle Oscillator
const bodyOsc = new Tone.Oscillator(tune * 4.7, "triangle");
const bodyOsc = new Tone.Oscillator(tune * 4.7 + drift, "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 + drift;
const endFreq = tune + drift;

bodyOsc.frequency.setValueAtTime(startFreq, time);
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.1);
Expand All @@ -39,15 +43,17 @@ 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
// HPF > 1kHz to avoid phase trap. Include +/- 2% cutoff variance.
const clickCutoff = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(clickCutoff, "highpass");
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;
// Ultra short envelope (10-20ms) for the click. Include +/- 2% decay variance.
const clickDecay = 0.02 * (1 + (Math.random() * 0.04 - 0.02));
noiseGain.gain.setValueAtTime(0.7, time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay);

Expand Down
39 changes: 27 additions & 12 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,51 @@ 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 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; // 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 (Adjusted per research)
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.exponentialRampToValueAtTime(0.001, time + 0.2);
// Body decay include +/- 2% variance.
const bodyDecay = 0.2 * (1 + (Math.random() * 0.04 - 0.02));
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + bodyDecay);

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const hpf = new Tone.Filter(1000, "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");
// HPF to protect fundamental. Include +/- 2% cutoff variance.
const hpfCutoff = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const hpf = new Tone.Filter(hpfCutoff, "highpass");

// LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz. Include +/- 2% cutoff variance.
const lpfCutoff = (4000 + pitch * 4000) * (1 + (Math.random() * 0.04 - 0.02));
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 include +/- 2% variance.
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);
Expand Down
6 changes: 2 additions & 4 deletions src/store/audioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ export const useAudioStore = create<AudioState>((set, get) => ({
if (channel === 'kick') drumMachine.outputKick.gain.value = value
if (channel === 'snare') drumMachine.outputSnare.gain.value = value
if (channel === 'hihat') drumMachine.outputHihat.gain.value = value
// We use hihat output for both open and closed for now in TR808HiHat as it shares a node,
// but we can scale the volume parameter independently here if we had separate synths.
// For now, we'll map openHat to hihat channel, and clap to clap.
if (channel === 'hihatOpen') drumMachine.outputHihat.gain.value = value // Shared
// Open HiHat now has its own output node in DrumMachine
if (channel === 'hihatOpen') drumMachine.outputOpenHat.gain.value = value
if (channel === 'clap') drumMachine.outputClap.gain.value = value
}

Expand Down