Lofi_Generator/lib/audio/drumMachine.ts
Avery Felts 5ed84192d5 Implement lofi hip hop generator with Tone.js
- Set up Next.js project with shadcn/ui and Tailwind CSS
- Created audio engine with MembraneSynth drums, FMSynth chords, and ambient noise layers
- Implemented 16-step drum sequencer with boom bap patterns
- Added jazz chord progressions (ii-V-I, minor key, neo soul)
- Built React hook for audio state management
- Created UI components: transport controls, volume sliders, layer mixer, beat visualizer
- Applied lofi-themed dark color scheme with oklch colors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:29:28 -07:00

166 lines
3.9 KiB
TypeScript

import * as Tone from 'tone';
import { DrumPattern } from '@/types/audio';
import { getRandomPattern, generateRandomPattern } from './patterns';
export class DrumMachine {
private kick: Tone.MembraneSynth;
private snare: Tone.NoiseSynth;
private hihat: Tone.NoiseSynth;
private openhat: Tone.NoiseSynth;
private sequence: Tone.Sequence | null = null;
private pattern: DrumPattern;
private output: Tone.Gain;
private lowpass: Tone.Filter;
constructor(destination: Tone.InputNode) {
this.output = new Tone.Gain(0.8);
this.lowpass = new Tone.Filter({
frequency: 8000,
type: 'lowpass',
rolloff: -12,
});
// Kick drum - deep and punchy
this.kick = new Tone.MembraneSynth({
pitchDecay: 0.05,
octaves: 6,
oscillator: { type: 'sine' },
envelope: {
attack: 0.001,
decay: 0.4,
sustain: 0.01,
release: 0.4,
},
});
// Snare - filtered noise
this.snare = new Tone.NoiseSynth({
noise: { type: 'white' },
envelope: {
attack: 0.001,
decay: 0.2,
sustain: 0,
release: 0.1,
},
});
const snareFilter = new Tone.Filter({
frequency: 5000,
type: 'bandpass',
Q: 1,
});
this.snare.connect(snareFilter);
snareFilter.connect(this.lowpass);
// Closed hi-hat - high filtered noise
this.hihat = new Tone.NoiseSynth({
noise: { type: 'white' },
envelope: {
attack: 0.001,
decay: 0.05,
sustain: 0,
release: 0.02,
},
});
const hihatFilter = new Tone.Filter({
frequency: 10000,
type: 'highpass',
});
this.hihat.connect(hihatFilter);
hihatFilter.connect(this.lowpass);
// Open hi-hat
this.openhat = new Tone.NoiseSynth({
noise: { type: 'white' },
envelope: {
attack: 0.001,
decay: 0.3,
sustain: 0,
release: 0.15,
},
});
const openhatFilter = new Tone.Filter({
frequency: 8000,
type: 'highpass',
});
this.openhat.connect(openhatFilter);
openhatFilter.connect(this.lowpass);
// Connect kick directly to lowpass
this.kick.connect(this.lowpass);
// Chain: lowpass -> output -> destination
this.lowpass.connect(this.output);
this.output.connect(destination);
// Initialize with a random pattern
this.pattern = getRandomPattern();
}
createSequence(onStep?: (step: number) => void): void {
if (this.sequence) {
this.sequence.dispose();
}
const steps = Array.from({ length: 16 }, (_, i) => i);
this.sequence = new Tone.Sequence(
(time, step) => {
if (this.pattern.kick[step]) {
this.kick.triggerAttackRelease('C1', '8n', time, 0.8);
}
if (this.pattern.snare[step]) {
this.snare.triggerAttackRelease('8n', time, 0.5);
}
if (this.pattern.hihat[step]) {
this.hihat.triggerAttackRelease('32n', time, 0.3);
}
if (this.pattern.openhat[step]) {
this.openhat.triggerAttackRelease('16n', time, 0.25);
}
// Call step callback on main thread
if (onStep) {
Tone.getDraw().schedule(() => {
onStep(step);
}, time);
}
},
steps,
'16n'
);
this.sequence.start(0);
}
setPattern(pattern: DrumPattern): void {
this.pattern = pattern;
}
randomize(): DrumPattern {
this.pattern = Math.random() > 0.5 ? getRandomPattern() : generateRandomPattern();
return this.pattern;
}
setVolume(volume: number): void {
this.output.gain.rampTo(volume, 0.1);
}
mute(muted: boolean): void {
this.output.gain.rampTo(muted ? 0 : 0.8, 0.1);
}
getPattern(): DrumPattern {
return this.pattern;
}
dispose(): void {
this.sequence?.dispose();
this.kick.dispose();
this.snare.dispose();
this.hihat.dispose();
this.openhat.dispose();
this.lowpass.dispose();
this.output.dispose();
}
}