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: 11 additions & 0 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ export class DrumMachine {
this.params[drum] = { pitch, decay }
}

/**
* Synchronizes all internal parameters with the provided state.
*/
syncInternalParams(kit: '808' | '909', saturation: number, drumParams: Record<string, { pitch: number, decay: number }>) {
this.setKit(kit)
this.setSaturation(saturation)
Object.entries(drumParams).forEach(([id, p]) => {
this.setDrumParams(id, p.pitch, p.decay)
})
}

triggerDrum(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', time: number, velocity: number = 0.8) {
const p = this.params[drum]
const kit808 = this.kit808
Expand Down
35 changes: 35 additions & 0 deletions src/logic/DrumUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Tone from 'tone'

/**
* Shared DSP utilities for drum synthesis based on research specs.
*/
Expand Down Expand Up @@ -40,3 +42,36 @@ export function applyVariance(base: number, variance: number = 0.02): number {
// Math.random() * 0.04 - 0.02 gives range [-0.02, 0.02]
return base * (1 + (Math.random() * (variance * 2) - variance));
}

/**
* Generates an authentic 15-bit LFSR noise buffer (TR-909 style).
* Polynomial: x^15 + x^14 + 1
*/
export function generateLFSRNoise(context: Tone.BaseContext): AudioBuffer {
const sampleRate = context.sampleRate;
const duration = 2.0;
const buffer = context.createBuffer(1, sampleRate * duration, sampleRate);
const data = buffer.getChannelData(0);

let state = 0x7FFF; // 15-bit seed
for (let i = 0; i < data.length; i++) {
// x^15 + x^14 + 1
const bit = ((state >> 14) ^ (state >> 13)) & 1;
state = ((state << 1) | bit) & 0x7FFF;
data[i] = (state / 0x7FFF) * 2 - 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Output the LFSR bit instead of the whole register

When the 909 snare uses this buffer, each sample is derived from the entire shifted register state rather than the generated LFSR output bit, so consecutive samples remain strongly correlated as the register shifts instead of producing the intended pseudo-random bitstream. This makes the new “LFSR noise” behave more like a stepped/ramping waveform than TR-909-style noise; map the feedback/output bit (for example the LSB after the shift) to -1/1 instead of scaling state.

Useful? React with 👍 / 👎.

}
return buffer;
}

/**
* Generates a high-quality white noise buffer.
*/
export function generateWhiteNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer {
const sampleRate = context.sampleRate;
const buffer = context.createBuffer(1, sampleRate * duration, sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
1 change: 1 addition & 0 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class TR808Clap {
}

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const bpfFreq = (1000 + pitch * 1000);
const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass");
Expand Down
1 change: 1 addition & 0 deletions src/logic/drums/TR808Cowbell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export class TR808Cowbell {
constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// Core: Dual square wave oscillators from Schmitt trigger matrix
// Standard frequencies: 540Hz and 800Hz
const pitchMultiplier = 0.5 + pitch; // Range approx 0.5x to 1.5x
Expand Down
1 change: 1 addition & 0 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class TR808HiHat {
constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// Create nodes
const mixGain = new Tone.Gain(0.15);
const bpf1 = new Tone.Filter(3440, "bandpass");
Expand Down
16 changes: 12 additions & 4 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export class TR808Kick {
constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;

// 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
Expand All @@ -30,11 +32,17 @@ export class TR808Kick {
osc.frequency.setValueAtTime(startFreq, time);
osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05);

// VCA Amp Envelope: Instant attack, adjustable exponential decay
masterGain.gain.setValueAtTime(velocity, time);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay);
// VCA Amp Envelope: Two-stage "diode damping" emulation
// Stage 1: Fast 20ms initial decay to 50% volume (emulates diode resistance shift)
// Stage 2: Final decay (Tone.js requires values > 0 for exponentialRamp)
const dampingTime = 0.02;
const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay);

masterGain.gain.setValueAtTime(Math.max(0.001, velocity), time);
masterGain.gain.exponentialRampToValueAtTime(Math.max(0.001, velocity * 0.5), time + dampingTime);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay);

osc.start(time).stop(time + finalDecay);
osc.start(time).stop(time + safeFinalDecay);

osc.onstop = () => {
osc.dispose();
Expand Down
14 changes: 5 additions & 9 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import * as Tone from 'tone'
import { applyPitchDrift, applyVariance } from '../DrumUtils'
import { applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils'

export class TR808Snare {
private noiseBuffer: AudioBuffer;

constructor(private destination: Tone.ToneAudioNode) {
const sampleRate = Tone.getContext().sampleRate;
const bufferSize = sampleRate * 0.5; // 500ms
this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 2.0);
}

trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// pitch maps to tone balance here (balance between low and high modes)
const toneBalance = pitch;

Expand Down Expand Up @@ -48,6 +43,7 @@ export class TR808Snare {

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const randomStart = Math.random() * (this.noiseBuffer.duration - 0.5);
// High-pass filter (>1800Hz) to prevent phase trap with tonal body
// Q = 0.707 (Butterworth)
const noiseFilter = new Tone.Filter({
Expand All @@ -66,7 +62,7 @@ export class TR808Snare {

oscLow.start(time).stop(time + vcaDecay);
oscHigh.start(time).stop(time + vcaDecay);
noiseSrc.start(time).stop(time + snappyDecay + 0.1);
noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1);

// Cleanup
oscLow.onstop = () => {
Expand Down
18 changes: 7 additions & 11 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import * as Tone from 'tone'
import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils'
import { makeDistortionCurve, applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils'

export class TR909Kick {
private noiseBuffer: AudioBuffer;
private bodyCurve: Float32Array;

constructor(private destination: Tone.ToneAudioNode) {
// Soft Clipping curve from research
this.bodyCurve = makeDistortionCurve(10);

const sampleRate = Tone.getContext().sampleRate;
const bufferSize = sampleRate * 0.05; // 50ms click
this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
this.bodyCurve = makeDistortionCurve(30);
this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 2.0);
}

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// 909 Kick base frequency is fixed around 50Hz
const tune = 50;
// The 'Pitch' parameter on 909 maps to the frequency sweep duration
Expand Down Expand Up @@ -58,6 +52,8 @@ export class TR909Kick {

// Click Layer (Noise)
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
// Random start offset to avoid machine-gun effect
const randomStart = Math.random() * (this.noiseBuffer.duration - 0.1);
const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap
const noiseGain = new Tone.Gain(0);

Expand All @@ -80,7 +76,7 @@ export class TR909Kick {
pulseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.005);

bodyOsc.start(time).stop(time + vcaDecay);
noiseSrc.start(time).stop(time + clickDecay);
noiseSrc.start(time, randomStart).stop(time + clickDecay);
pulseOsc.start(time).stop(time + 0.005);

bodyOsc.onstop = () => {
Expand Down
20 changes: 7 additions & 13 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Tone from 'tone'
import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils'
import { makeDistortionCurve, applyPitchDrift, applyVariance, generateLFSRNoise } from '../DrumUtils'

export class TR909Snare {
private noiseBuffer: AudioBuffer;
Expand All @@ -8,18 +8,11 @@ export class TR909Snare {
constructor(private destination: Tone.ToneAudioNode) {
// Soft Clipping curve from research
this.bodyCurve = makeDistortionCurve(15);

const sampleRate = Tone.getContext().sampleRate;
const bufferSize = sampleRate * 0.5;
this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate);
const data = this.noiseBuffer.getChannelData(0);
// While original used LFSR, research says Math.random() is sufficient for Web Audio API context
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
this.noiseBuffer = generateLFSRNoise(Tone.getContext());
}

trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz
const freq1 = 160;
const freq2 = 220;
Expand Down Expand Up @@ -50,8 +43,8 @@ export class TR909Snare {
postShaperGain.connect(tonalGain);
tonalGain.connect(this.destination);

// Pitch Sweep: ~320Hz to ~160Hz over 30ms (as per research spec)
const sweepTime = 0.03;
// Pitch Sweep: ~320Hz to ~160Hz over 50ms (as per research spec)
const sweepTime = 0.05;
const startFreq1 = toneDrift1 * 2;
const startFreq2 = toneDrift2 * 2;

Expand All @@ -65,6 +58,7 @@ export class TR909Snare {

// Snappy Layer
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const randomStart = Math.random() * (this.noiseBuffer.duration - 0.5);
const hpf = new Tone.Filter(noiseHPFFreq, "highpass"); // HPF to protect fundamental
// LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz (research: toneCutoff)
const toneCutoff = 4000 + pitch * 4000;
Expand All @@ -81,7 +75,7 @@ export class TR909Snare {

osc1.start(time).stop(time + vcaDecay);
osc2.start(time).stop(time + vcaDecay);
noiseSrc.start(time).stop(time + snappyDecay + 0.1);
noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1);

osc1.onstop = () => {
osc1.dispose();
Expand Down
12 changes: 12 additions & 0 deletions src/store/audioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Tone from 'tone'
import { AcidSynth } from '../logic/AcidSynth'
import { DrumMachine } from '../logic/DrumMachine'
import { PadSynth } from '../logic/PadSynth'
import { useDrumStore } from './instrumentStore'

export interface AudioState {
isInitialized: boolean
Expand Down Expand Up @@ -63,6 +64,17 @@ export const useAudioStore = create<AudioState>((set, get) => ({
Tone.Transport.bpm.value = get().bpm
Tone.Transport.swing = get().swing

// Sync initial drum parameters
const drumStore = useDrumStore.getState()
drums.syncInternalParams(drumStore.kit, drumStore.drive, {
kick: drumStore.kick,
snare: drumStore.snare,
hihat: drumStore.hihat,
hihatOpen: drumStore.hihatOpen,
clap: drumStore.clap,
cowbell: drumStore.cowbell
})

set({
isInitialized: true,
bassSynth: bassSynth,
Expand Down