Lofi_Generator/lib/audio/audioEngine.ts
Avery Felts 0f17775f3f Add multi-genre support and expanded instrument options
- 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>
2026-01-20 17:57:12 -07:00

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;