import * as Tone from 'tone'; import { DrumPattern, DrumKit, Genre } from '@/types/audio'; import { getRandomPattern } from './patterns'; export class DrumMachine { private kick: Tone.MembraneSynth | null = null; private snare: Tone.NoiseSynth | null = null; private hihat: Tone.NoiseSynth | null = null; private openhat: Tone.NoiseSynth | null = null; private snareFilter: Tone.Filter | null = null; private hihatFilter: Tone.Filter | null = null; private openhatFilter: Tone.Filter | null = null; private sequence: Tone.Sequence | null = null; private pattern: DrumPattern; private output: Tone.Gain; private lowpass: Tone.Filter; private currentKit: DrumKit = 'acoustic'; private genre: Genre = 'hiphop'; constructor(destination: Tone.InputNode) { this.output = new Tone.Gain(0.8); this.lowpass = new Tone.Filter({ frequency: 8000, type: 'lowpass', rolloff: -12, }); this.lowpass.connect(this.output); this.output.connect(destination); this.pattern = getRandomPattern(this.genre); this.createKit('acoustic'); } private createKit(kit: DrumKit): void { // Dispose existing instruments this.kick?.dispose(); this.snare?.dispose(); this.hihat?.dispose(); this.openhat?.dispose(); this.snareFilter?.dispose(); this.hihatFilter?.dispose(); this.openhatFilter?.dispose(); const kitConfigs: Record; snareFilter: number; hihatFilter: number; snareDecay: number; hihatDecay: number; }> = { acoustic: { kick: { pitchDecay: 0.05, octaves: 6, envelope: { attack: 0.001, decay: 0.4, sustain: 0.01, release: 0.4 } }, snareFilter: 5000, hihatFilter: 10000, snareDecay: 0.2, hihatDecay: 0.05, }, electronic: { kick: { pitchDecay: 0.08, octaves: 8, envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.3 } }, snareFilter: 4000, hihatFilter: 12000, snareDecay: 0.15, hihatDecay: 0.03, }, '808': { kick: { pitchDecay: 0.15, octaves: 10, envelope: { attack: 0.001, decay: 0.8, sustain: 0.1, release: 0.6 } }, snareFilter: 3000, hihatFilter: 8000, snareDecay: 0.25, hihatDecay: 0.04, }, orchestral: { kick: { pitchDecay: 0.02, octaves: 4, envelope: { attack: 0.01, decay: 0.5, sustain: 0.05, release: 0.5 } }, snareFilter: 6000, hihatFilter: 6000, snareDecay: 0.3, hihatDecay: 0.1, }, }; const config = kitConfigs[kit]; // Kick drum this.kick = new Tone.MembraneSynth({ ...config.kick, oscillator: { type: 'sine' }, }); this.kick.connect(this.lowpass); // Snare this.snareFilter = new Tone.Filter({ frequency: config.snareFilter, type: 'bandpass', Q: 1, }); this.snare = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: config.snareDecay, sustain: 0, release: 0.1 }, }); this.snare.connect(this.snareFilter); this.snareFilter.connect(this.lowpass); // Closed hi-hat this.hihatFilter = new Tone.Filter({ frequency: config.hihatFilter, type: 'highpass', }); this.hihat = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: config.hihatDecay, sustain: 0, release: 0.02 }, }); this.hihat.connect(this.hihatFilter); this.hihatFilter.connect(this.lowpass); // Open hi-hat this.openhatFilter = new Tone.Filter({ frequency: config.hihatFilter - 2000, type: 'highpass', }); this.openhat = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.15 }, }); this.openhat.connect(this.openhatFilter); this.openhatFilter.connect(this.lowpass); this.currentKit = kit; } setInstrument(kit: DrumKit): void { this.createKit(kit); } setGenre(genre: Genre): void { this.genre = genre; this.pattern = getRandomPattern(genre); } 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) { this.kick.triggerAttackRelease('C1', '8n', time, 0.8); } if (this.pattern.snare[step] && this.snare) { this.snare.triggerAttackRelease('8n', time, 0.5); } if (this.pattern.hihat[step] && this.hihat) { this.hihat.triggerAttackRelease('32n', time, 0.3); } if (this.pattern.openhat[step] && this.openhat) { this.openhat.triggerAttackRelease('16n', time, 0.25); } 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 = getRandomPattern(this.genre); 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.snareFilter?.dispose(); this.hihatFilter?.dispose(); this.openhatFilter?.dispose(); this.lowpass.dispose(); this.output.dispose(); } }