Lofi_Generator/lib/audio/audioEngine.ts
Avery Felts 5ed84192d5 Implement lofi hip hop generator with Tone.js
- 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>
2026-01-20 17:29:28 -07:00

239 lines
6.1 KiB
TypeScript

import * as Tone from 'tone';
import { DrumMachine } from './drumMachine';
import { ChordEngine } from './chordEngine';
import { AmbientLayer } from './ambientLayer';
import { EngineState, AudioEngineCallbacks, LayerName } 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 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: 78,
swing: 0.12,
currentStep: 0,
volumes: {
master: 0.8,
drums: 0.8,
chords: 0.6,
ambient: 0.4,
},
muted: {
drums: false,
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;
// Start Tone.js audio context (requires user gesture)
await Tone.start();
// Set up transport
Tone.getTransport().bpm.value = this.state.bpm;
Tone.getTransport().swing = this.state.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,
});
// Chain: gain -> reverb -> compressor -> limiter -> destination
this.masterGain.connect(this.masterReverb);
this.masterReverb.connect(this.masterCompressor);
this.masterCompressor.connect(this.masterLimiter);
this.masterLimiter.toDestination();
// Initialize layers
this.drumMachine = new DrumMachine(this.masterGain);
this.chordEngine = new ChordEngine(this.masterGain);
this.ambientLayer = new AmbientLayer(this.masterGain);
// Create sequences
this.drumMachine.createSequence((step) => {
this.state.currentStep = step;
this.callbacks.onStepChange?.(step);
});
this.chordEngine.createSequence();
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.notifyStateChange();
}
setBpm(bpm: number): void {
this.state.bpm = Math.max(60, Math.min(100, 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 '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 '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 '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.masterGain?.dispose();
this.masterCompressor?.dispose();
this.masterLimiter?.dispose();
this.masterReverb?.dispose();
this.drumMachine = null;
this.chordEngine = null;
this.ambientLayer = 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;