forked from averyfelts/Lofi_Generator
- Add 4 genres: Hip Hop, Classical, Trap, Pop with unique patterns - Add new instrument layers: Bass, Brass, Piano - Each layer now has 4 instrument variations to choose from - Add genre-specific drum patterns, chord progressions, and melodies - Add duration control (1-10 minutes) - Rename app to "Beat Generator" with modern gradient header - Redesign UI with 2-column instrument grid layout - Add color-coded accent for each instrument section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
381 lines
10 KiB
TypeScript
381 lines
10 KiB
TypeScript
import * as Tone from 'tone';
|
|
import { DrumMachine } from './drumMachine';
|
|
import { ChordEngine } from './chordEngine';
|
|
import { AmbientLayer } from './ambientLayer';
|
|
import { BassEngine } from './bassEngine';
|
|
import { BrassEngine } from './brassEngine';
|
|
import { PianoEngine } from './pianoEngine';
|
|
import {
|
|
EngineState,
|
|
AudioEngineCallbacks,
|
|
LayerName,
|
|
Genre,
|
|
GENRE_CONFIG,
|
|
DrumKit,
|
|
BassType,
|
|
BrassType,
|
|
PianoType,
|
|
ChordType,
|
|
AmbientType,
|
|
} from '@/types/audio';
|
|
|
|
class AudioEngine {
|
|
private static instance: AudioEngine | null = null;
|
|
|
|
private drumMachine: DrumMachine | null = null;
|
|
private chordEngine: ChordEngine | null = null;
|
|
private ambientLayer: AmbientLayer | null = null;
|
|
private bassEngine: BassEngine | null = null;
|
|
private brassEngine: BrassEngine | null = null;
|
|
private pianoEngine: PianoEngine | null = null;
|
|
|
|
private masterGain: Tone.Gain | null = null;
|
|
private masterCompressor: Tone.Compressor | null = null;
|
|
private masterLimiter: Tone.Limiter | null = null;
|
|
private masterReverb: Tone.Reverb | null = null;
|
|
|
|
private callbacks: AudioEngineCallbacks = {};
|
|
|
|
private state: EngineState = {
|
|
isPlaying: false,
|
|
isInitialized: false,
|
|
bpm: 90,
|
|
swing: 0.15,
|
|
currentStep: 0,
|
|
genre: 'hiphop',
|
|
duration: 3,
|
|
instruments: {
|
|
drums: 'acoustic',
|
|
bass: 'synth',
|
|
brass: 'trumpet',
|
|
piano: 'grand',
|
|
chords: 'pad',
|
|
ambient: 'rain',
|
|
},
|
|
volumes: {
|
|
master: 0.8,
|
|
drums: 0.8,
|
|
bass: 0.6,
|
|
brass: 0.4,
|
|
piano: 0.5,
|
|
chords: 0.6,
|
|
ambient: 0.4,
|
|
},
|
|
muted: {
|
|
drums: false,
|
|
bass: false,
|
|
brass: true,
|
|
piano: true,
|
|
chords: false,
|
|
ambient: false,
|
|
},
|
|
};
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): AudioEngine {
|
|
if (!AudioEngine.instance) {
|
|
AudioEngine.instance = new AudioEngine();
|
|
}
|
|
return AudioEngine.instance;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.state.isInitialized) return;
|
|
|
|
await Tone.start();
|
|
|
|
const genreConfig = GENRE_CONFIG[this.state.genre];
|
|
Tone.getTransport().bpm.value = genreConfig.bpm;
|
|
Tone.getTransport().swing = genreConfig.swing;
|
|
Tone.getTransport().swingSubdivision = '16n';
|
|
|
|
// Master chain
|
|
this.masterGain = new Tone.Gain(this.state.volumes.master);
|
|
this.masterCompressor = new Tone.Compressor({
|
|
threshold: -20,
|
|
ratio: 4,
|
|
attack: 0.003,
|
|
release: 0.25,
|
|
});
|
|
this.masterLimiter = new Tone.Limiter(-1);
|
|
this.masterReverb = new Tone.Reverb({
|
|
decay: 2,
|
|
wet: 0.15,
|
|
});
|
|
|
|
this.masterGain.connect(this.masterReverb);
|
|
this.masterReverb.connect(this.masterCompressor);
|
|
this.masterCompressor.connect(this.masterLimiter);
|
|
this.masterLimiter.toDestination();
|
|
|
|
// Initialize all layers
|
|
this.drumMachine = new DrumMachine(this.masterGain);
|
|
this.chordEngine = new ChordEngine(this.masterGain);
|
|
this.ambientLayer = new AmbientLayer(this.masterGain);
|
|
this.bassEngine = new BassEngine(this.masterGain);
|
|
this.brassEngine = new BrassEngine(this.masterGain);
|
|
this.pianoEngine = new PianoEngine(this.masterGain);
|
|
|
|
// Set initial genre for all engines
|
|
this.drumMachine.setGenre(this.state.genre);
|
|
this.chordEngine.setGenre(this.state.genre);
|
|
this.bassEngine.setGenre(this.state.genre);
|
|
this.brassEngine.setGenre(this.state.genre);
|
|
this.pianoEngine.setGenre(this.state.genre);
|
|
|
|
// Apply initial mute states
|
|
this.brassEngine.mute(this.state.muted.brass);
|
|
this.pianoEngine.mute(this.state.muted.piano);
|
|
|
|
// Create sequences
|
|
this.drumMachine.createSequence((step) => {
|
|
this.state.currentStep = step;
|
|
this.callbacks.onStepChange?.(step);
|
|
});
|
|
this.chordEngine.createSequence();
|
|
this.bassEngine.createSequence();
|
|
this.brassEngine.createSequence();
|
|
this.pianoEngine.createSequence();
|
|
|
|
this.state.bpm = genreConfig.bpm;
|
|
this.state.swing = genreConfig.swing;
|
|
this.state.isInitialized = true;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setCallbacks(callbacks: AudioEngineCallbacks): void {
|
|
this.callbacks = callbacks;
|
|
}
|
|
|
|
private notifyStateChange(): void {
|
|
this.callbacks.onStateChange?.({ ...this.state });
|
|
}
|
|
|
|
async play(): Promise<void> {
|
|
if (!this.state.isInitialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
this.ambientLayer?.start();
|
|
Tone.getTransport().start();
|
|
this.state.isPlaying = true;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
pause(): void {
|
|
Tone.getTransport().pause();
|
|
this.ambientLayer?.stop();
|
|
this.state.isPlaying = false;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
stop(): void {
|
|
Tone.getTransport().stop();
|
|
this.ambientLayer?.stop();
|
|
this.state.isPlaying = false;
|
|
this.state.currentStep = 0;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
generateNewBeat(): void {
|
|
this.drumMachine?.randomize();
|
|
this.chordEngine?.randomize();
|
|
this.bassEngine?.randomize();
|
|
this.brassEngine?.randomize();
|
|
this.pianoEngine?.randomize();
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setGenre(genre: Genre): void {
|
|
this.state.genre = genre;
|
|
const genreConfig = GENRE_CONFIG[genre];
|
|
|
|
// Update BPM and swing for genre
|
|
this.state.bpm = genreConfig.bpm;
|
|
this.state.swing = genreConfig.swing;
|
|
Tone.getTransport().bpm.value = genreConfig.bpm;
|
|
Tone.getTransport().swing = genreConfig.swing;
|
|
|
|
// Update all engines with new genre
|
|
this.drumMachine?.setGenre(genre);
|
|
this.chordEngine?.setGenre(genre);
|
|
this.bassEngine?.setGenre(genre);
|
|
this.brassEngine?.setGenre(genre);
|
|
this.pianoEngine?.setGenre(genre);
|
|
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setDuration(minutes: number): void {
|
|
this.state.duration = Math.max(1, Math.min(10, minutes));
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setInstrument(layer: LayerName, instrument: string): void {
|
|
switch (layer) {
|
|
case 'drums':
|
|
this.state.instruments.drums = instrument as DrumKit;
|
|
this.drumMachine?.setInstrument(instrument as DrumKit);
|
|
break;
|
|
case 'bass':
|
|
this.state.instruments.bass = instrument as BassType;
|
|
this.bassEngine?.setInstrument(instrument as BassType);
|
|
break;
|
|
case 'brass':
|
|
this.state.instruments.brass = instrument as BrassType;
|
|
this.brassEngine?.setInstrument(instrument as BrassType);
|
|
break;
|
|
case 'piano':
|
|
this.state.instruments.piano = instrument as PianoType;
|
|
this.pianoEngine?.setInstrument(instrument as PianoType);
|
|
break;
|
|
case 'chords':
|
|
this.state.instruments.chords = instrument as ChordType;
|
|
this.chordEngine?.setInstrument(instrument as ChordType);
|
|
break;
|
|
case 'ambient':
|
|
this.state.instruments.ambient = instrument as AmbientType;
|
|
this.ambientLayer?.setInstrument(instrument as AmbientType);
|
|
break;
|
|
}
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setBpm(bpm: number): void {
|
|
this.state.bpm = Math.max(60, Math.min(180, bpm));
|
|
Tone.getTransport().bpm.value = this.state.bpm;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setSwing(swing: number): void {
|
|
this.state.swing = Math.max(0, Math.min(0.5, swing));
|
|
Tone.getTransport().swing = this.state.swing;
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setMasterVolume(volume: number): void {
|
|
this.state.volumes.master = Math.max(0, Math.min(1, volume));
|
|
this.masterGain?.gain.rampTo(this.state.volumes.master, 0.1);
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setLayerVolume(layer: LayerName, volume: number): void {
|
|
const normalizedVolume = Math.max(0, Math.min(1, volume));
|
|
this.state.volumes[layer] = normalizedVolume;
|
|
|
|
switch (layer) {
|
|
case 'drums':
|
|
this.drumMachine?.setVolume(normalizedVolume);
|
|
break;
|
|
case 'bass':
|
|
this.bassEngine?.setVolume(normalizedVolume);
|
|
break;
|
|
case 'brass':
|
|
this.brassEngine?.setVolume(normalizedVolume);
|
|
break;
|
|
case 'piano':
|
|
this.pianoEngine?.setVolume(normalizedVolume);
|
|
break;
|
|
case 'chords':
|
|
this.chordEngine?.setVolume(normalizedVolume);
|
|
break;
|
|
case 'ambient':
|
|
this.ambientLayer?.setVolume(normalizedVolume);
|
|
break;
|
|
}
|
|
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
toggleMute(layer: LayerName): void {
|
|
this.state.muted[layer] = !this.state.muted[layer];
|
|
|
|
switch (layer) {
|
|
case 'drums':
|
|
this.drumMachine?.mute(this.state.muted[layer]);
|
|
break;
|
|
case 'bass':
|
|
this.bassEngine?.mute(this.state.muted[layer]);
|
|
break;
|
|
case 'brass':
|
|
this.brassEngine?.mute(this.state.muted[layer]);
|
|
break;
|
|
case 'piano':
|
|
this.pianoEngine?.mute(this.state.muted[layer]);
|
|
break;
|
|
case 'chords':
|
|
this.chordEngine?.mute(this.state.muted[layer]);
|
|
break;
|
|
case 'ambient':
|
|
this.ambientLayer?.mute(this.state.muted[layer]);
|
|
break;
|
|
}
|
|
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
setMuted(layer: LayerName, muted: boolean): void {
|
|
this.state.muted[layer] = muted;
|
|
|
|
switch (layer) {
|
|
case 'drums':
|
|
this.drumMachine?.mute(muted);
|
|
break;
|
|
case 'bass':
|
|
this.bassEngine?.mute(muted);
|
|
break;
|
|
case 'brass':
|
|
this.brassEngine?.mute(muted);
|
|
break;
|
|
case 'piano':
|
|
this.pianoEngine?.mute(muted);
|
|
break;
|
|
case 'chords':
|
|
this.chordEngine?.mute(muted);
|
|
break;
|
|
case 'ambient':
|
|
this.ambientLayer?.mute(muted);
|
|
break;
|
|
}
|
|
|
|
this.notifyStateChange();
|
|
}
|
|
|
|
getState(): EngineState {
|
|
return { ...this.state };
|
|
}
|
|
|
|
dispose(): void {
|
|
this.stop();
|
|
this.drumMachine?.dispose();
|
|
this.chordEngine?.dispose();
|
|
this.ambientLayer?.dispose();
|
|
this.bassEngine?.dispose();
|
|
this.brassEngine?.dispose();
|
|
this.pianoEngine?.dispose();
|
|
this.masterGain?.dispose();
|
|
this.masterCompressor?.dispose();
|
|
this.masterLimiter?.dispose();
|
|
this.masterReverb?.dispose();
|
|
|
|
this.drumMachine = null;
|
|
this.chordEngine = null;
|
|
this.ambientLayer = null;
|
|
this.bassEngine = null;
|
|
this.brassEngine = null;
|
|
this.pianoEngine = null;
|
|
this.masterGain = null;
|
|
this.masterCompressor = null;
|
|
this.masterLimiter = null;
|
|
this.masterReverb = null;
|
|
|
|
this.state.isInitialized = false;
|
|
this.state.isPlaying = false;
|
|
AudioEngine.instance = null;
|
|
}
|
|
}
|
|
|
|
export const audioEngine = AudioEngine.getInstance();
|
|
export default audioEngine;
|