Lofi_Generator/hooks/useAudioEngine.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

161 lines
3.8 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { EngineState, LayerName } from '@/types/audio';
const defaultState: 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,
},
};
export function useAudioEngine() {
const [state, setState] = useState<EngineState>(defaultState);
const [currentStep, setCurrentStep] = useState(0);
const engineRef = useRef<typeof import('@/lib/audio/audioEngine').default | null>(null);
const isInitializingRef = useRef(false);
// Dynamically import the audio engine (client-side only)
const getEngine = useCallback(async () => {
if (typeof window === 'undefined') return null;
if (!engineRef.current) {
const { default: audioEngine } = await import('@/lib/audio/audioEngine');
engineRef.current = audioEngine;
}
return engineRef.current;
}, []);
// Initialize engine and set up callbacks
const initialize = useCallback(async () => {
if (isInitializingRef.current) return;
isInitializingRef.current = true;
try {
const engine = await getEngine();
if (!engine) return;
engine.setCallbacks({
onStepChange: (step) => {
setCurrentStep(step);
},
onStateChange: (newState) => {
setState(newState);
},
});
await engine.initialize();
setState(engine.getState());
} finally {
isInitializingRef.current = false;
}
}, [getEngine]);
// Play/pause toggle
const togglePlayback = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
const currentState = engine.getState();
if (currentState.isPlaying) {
engine.pause();
} else {
await engine.play();
}
}, [getEngine, state.isInitialized, initialize]);
// Stop playback
const stop = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
engine.stop();
setCurrentStep(0);
}, [getEngine]);
// Generate new beat
const generateNewBeat = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
engine.generateNewBeat();
}, [getEngine, state.isInitialized, initialize]);
// Set BPM
const setBpm = useCallback(async (bpm: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setBpm(bpm);
}, [getEngine]);
// Set swing
const setSwing = useCallback(async (swing: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setSwing(swing);
}, [getEngine]);
// Set master volume
const setMasterVolume = useCallback(async (volume: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setMasterVolume(volume);
}, [getEngine]);
// Set layer volume
const setLayerVolume = useCallback(async (layer: LayerName, volume: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setLayerVolume(layer, volume);
}, [getEngine]);
// Toggle layer mute
const toggleMute = useCallback(async (layer: LayerName) => {
const engine = await getEngine();
if (!engine) return;
engine.toggleMute(layer);
}, [getEngine]);
// Cleanup on unmount
useEffect(() => {
return () => {
// Don't dispose on unmount to allow seamless navigation
// The engine is a singleton that persists
};
}, []);
return {
state,
currentStep,
initialize,
togglePlayback,
stop,
generateNewBeat,
setBpm,
setSwing,
setMasterVolume,
setLayerVolume,
toggleMute,
};
}