forked from averyfelts/Lofi_Generator
- 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>
166 lines
3.9 KiB
TypeScript
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();
|
|
}
|
|
}
|