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
11 changes: 6 additions & 5 deletions src/components/SequencerLoop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ export function SequencerLoop() {

// 1. Drums (Euclidean - using cached patterns)
const patterns = drumPatternsRef.current
if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time)
if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time)
if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time)
if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time)
if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time)
const velocity = 0.8 // Default velocity for Euclidean steps
if (patterns.kick[step % patterns.kick.length]) drumMachine.triggerDrum('kick', time, velocity)
if (patterns.snare[step % patterns.snare.length]) drumMachine.triggerDrum('snare', time, velocity)
if (patterns.hihat[step % patterns.hihat.length]) drumMachine.triggerDrum('hihat', time, velocity)
if (patterns.hihatOpen[step % patterns.hihatOpen.length]) drumMachine.triggerDrum('hihatOpen', time, velocity)
if (patterns.clap[step % patterns.clap.length]) drumMachine.triggerDrum('clap', time, velocity)

// 2. Bass (Sting logic)
const bassStep = currentBass.pattern[step]
Expand Down
49 changes: 21 additions & 28 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,36 @@ 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)
this.outputHihat = new Tone.Gain(1)
this.outputOpenHat = new Tone.Gain(1)
this.outputClap = new Tone.Gain(1)

// Master Chain: Comp -> Shaper -> Output -> Destination
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 instruments to the 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),
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),
clap: new TR808Clap(this.outputClap)
}
}
Expand Down Expand Up @@ -101,26 +101,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
21 changes: 13 additions & 8 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,32 @@ 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: Filter Cutoff Variance (+/- 2%)
const bpfCutoff = (1000 + pitch * 1000) * (1 + (Math.random() * 0.04 - 0.02));
const bpf = new Tone.Filter(bpfCutoff, "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 Intervals +/- 2%
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);
gain.gain.setValueAtTime(velocity, snapTime);
gain.gain.exponentialRampToValueAtTime(0.1 * velocity, snapTime + snapInterval * 0.8);
}

// Final decay
// Final decay with +/- 2% variance
const finalDecayStart = time + snapCount * snapInterval;
const decayTime = 0.1 + decay * 0.5;
gain.gain.setValueAtTime(1, finalDecayStart);
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
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
31 changes: 21 additions & 10 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ 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 closedDestination: Tone.ToneAudioNode, private openDestination: Tone.ToneAudioNode) { }

trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) {
// Choose destination
const destination = isOpen ? this.openDestination : this.closedDestination;

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");

// Micro-randomization: Filter Cutoff Variance (+/- 2%)
const bpf1Cutoff = 3440 * (1 + (Math.random() * 0.04 - 0.02));
const bpf2Cutoff = 7100 * (1 + (Math.random() * 0.04 - 0.02));
const hpfCutoff = 7000 * (1 + (Math.random() * 0.04 - 0.02));

const bpf1 = new Tone.Filter(bpf1Cutoff, "bandpass");
const bpf2 = new Tone.Filter(bpf2Cutoff, "bandpass");
const envGain = new Tone.Gain(0);
const hpf = new Tone.Filter(7000, "highpass");
const hpf = new Tone.Filter(hpfCutoff, "highpass");

// Pitch Multiplier (0.8x to 1.2x)
const pitchMultiplier = 0.8 + pitch * 0.4;
Expand All @@ -20,6 +29,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; // Random phase
osc.connect(mixGain);
return osc;
});
Expand All @@ -31,17 +41,18 @@ export class TR808HiHat {
bpf1.connect(envGain);
bpf2.connect(envGain);
envGain.connect(hpf);
hpf.connect(this.destination);
hpf.connect(destination);

// 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) with +/- 2% variance
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayTime = (isOpen ? (0.3 + decay * 0.2) : (0.04 + decay * 0.02)) * decayVariance;

// 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
12 changes: 8 additions & 4 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ 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;
// Add +/- 2% decay variance
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayTime = (0.4 + decay * 2.6) * decayVariance;

// 808 Kick Core: Bridged-T Network emulation
const osc = new Tone.Oscillator(tune, "sine");
osc.phase = Math.random() * 360; // Analog phase randomization
const masterGain = new Tone.Gain(0);

// Velocity scaling
const masterGain = new Tone.Gain(velocity);

osc.connect(masterGain);
masterGain.connect(this.destination);
Expand All @@ -29,7 +33,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
44 changes: 27 additions & 17 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ 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);

// 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; // Random phase
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,39 +37,42 @@ export class TR808Snare {
gainHigh.connect(masterTonalGain);
masterTonalGain.connect(this.destination);

masterTonalGain.gain.setValueAtTime(1, time);
// Tonal body decay is short (~200ms)
masterTonalGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
// Velocity scaled amplitude
masterTonalGain.gain.setValueAtTime(velocity, time);
// Tonal body decay is short (~200ms) with +/- 2% variance
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: High-pass filter (>1800Hz) +/- 2% cutoff variance
const hpfCutoff = 1800 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(hpfCutoff, "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 with +/- 2% variance
const snappyDecay = (0.25 + snappy * 0.15) * (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);
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
oscLow.onstop = () => {
// Cleanup: Use the longest-running node (noiseSource) to trigger disposal
noiseSrc.onended = () => {
oscLow.dispose();
oscHigh.dispose();
gainLow.dispose();
gainHigh.dispose();
masterTonalGain.dispose();
};
noiseSrc.onended = () => {
noiseSrc.dispose();
noiseFilter.dispose();
snappyGain.dispose();
Expand Down
36 changes: 24 additions & 12 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,64 @@ 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;
// Add +/- 2% decay variance
const decayVariance = 1 + (Math.random() * 0.04 - 0.02);
const decayTime = (0.3 + decay * 0.3) * decayVariance;

// 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; // Random phase

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);

// 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 Variance (+/- 2%)
const filterCutoff = 1000 * (1 + (Math.random() * 0.04 - 0.02));
const noiseFilter = new Tone.Filter(filterCutoff, "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;
noiseGain.gain.setValueAtTime(0.7, time);
// Also add +/- 2% click decay variance
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);
noiseSrc.start(time).stop(time + clickDecay);

// disposal logic: noiseSrc.onended is only reliable if it's the longest
// but here bodyOsc is definitely longer (decayTime > 0.3s)
bodyOsc.onstop = () => {
bodyOsc.dispose();
bodyGain.dispose();
};
noiseSrc.onended = () => {
noiseSrc.dispose();
noiseFilter.dispose();
noiseGain.dispose();
Expand Down
Loading