- 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>
161 lines
3.8 KiB
TypeScript
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,
|
|
};
|
|
}
|