forked from averyfelts/Lofi_Generator
- 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>
239 lines
6.1 KiB
TypeScript
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;
|