- Fix volume sliders by tracking currentVolume separately from mute state - Add song structure system with intro, verse, bridge, chorus, outro sections - Implement automatic section transitions during playback based on duration - Add 30-second loading bar with progress animation during beat generation - Update instrument box colors (grey drums, red bass, green piano, baby blue brass) - Add custom trumpet SVG icon for brass section - Set duration slider max to 3 minutes - Fix TypeScript type errors in audio engine classes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
5.8 KiB
TypeScript
211 lines
5.8 KiB
TypeScript
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';
|
|
private currentVolume: number = 0.8;
|
|
private isMuted: boolean = false;
|
|
|
|
constructor(destination: Tone.InputNode) {
|
|
this.output = new Tone.Gain(this.currentVolume);
|
|
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 {
|
|
this.kick?.dispose();
|
|
this.snare?.dispose();
|
|
this.hihat?.dispose();
|
|
this.openhat?.dispose();
|
|
this.snareFilter?.dispose();
|
|
this.hihatFilter?.dispose();
|
|
this.openhatFilter?.dispose();
|
|
|
|
const kitConfigs: Record<DrumKit, {
|
|
kick: object;
|
|
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];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
this.kick = new Tone.MembraneSynth({
|
|
...(config.kick as any),
|
|
oscillator: { type: 'sine' },
|
|
});
|
|
this.kick.connect(this.lowpass);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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.currentVolume = volume;
|
|
if (!this.isMuted) {
|
|
this.output.gain.rampTo(volume, 0.1);
|
|
}
|
|
}
|
|
|
|
mute(muted: boolean): void {
|
|
this.isMuted = muted;
|
|
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 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();
|
|
}
|
|
}
|