Lofi_Generator/hooks/useAudioEngine.ts
Nicholai d3158e4c6a feat(timeline-mixer): WIP timeline and mixer components
work in progress implementation of:
- mixer with channel strips, faders, pan knobs, level meters
- timeline with ruler, playhead, sections, keyframe tracks
- pattern and progression pickers for drums/chords
- automation lanes and mute tracks
- loop bracket for loop region selection
- export modal placeholder

known issues:
- drum pattern changes don't update audio engine
- timeline keyframes not connected to scheduler
- some UI bugs remain

this is a checkpoint commit for further iteration
2026-01-20 18:22:10 -07:00

267 lines
6.7 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { EngineState, LayerName, MeterLevels, LoopRegion } 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,
},
};
const defaultMeterLevels: MeterLevels = {
drums: 0,
chords: 0,
ambient: 0,
master: 0,
};
const defaultPans: Record<LayerName, number> = {
drums: 0,
chords: 0,
ambient: 0,
};
const defaultSoloed: Record<LayerName, boolean> = {
drums: false,
chords: false,
ambient: false,
};
export function useAudioEngine() {
const [state, setState] = useState<EngineState>(defaultState);
const [currentStep, setCurrentStep] = useState(0);
const [meterLevels, setMeterLevels] = useState<MeterLevels>(defaultMeterLevels);
const [pans, setPans] = useState<Record<LayerName, number>>(defaultPans);
const [soloed, setSoloed] = useState<Record<LayerName, boolean>>(defaultSoloed);
const [playheadPosition, setPlayheadPosition] = useState({ bar: 0, beat: 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);
},
onBarChange: (bar, beat) => {
setPlayheadPosition({ bar, beat });
},
onMeterUpdate: (levels) => {
setMeterLevels(levels);
},
});
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]);
// Set muted state directly
const setMuted = useCallback(async (layer: LayerName, muted: boolean) => {
const engine = await getEngine();
if (!engine) return;
engine.setMuted(layer, muted);
}, [getEngine]);
// Set pan
const setPan = useCallback(async (layer: LayerName, value: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setPan(layer, value);
setPans((prev) => ({ ...prev, [layer]: value }));
}, [getEngine]);
// Set solo
const setSolo = useCallback(async (layer: LayerName, enabled: boolean) => {
const engine = await getEngine();
if (!engine) return;
engine.setSolo(layer, enabled);
setSoloed(engine.getSoloState());
}, [getEngine]);
// Toggle solo
const toggleSolo = useCallback(async (layer: LayerName) => {
const engine = await getEngine();
if (!engine) return;
const current = engine.getSoloState();
engine.setSolo(layer, !current[layer]);
setSoloed(engine.getSoloState());
}, [getEngine]);
// Seek to position
const seek = useCallback(async (bar: number, beat: number = 0) => {
const engine = await getEngine();
if (!engine) return;
engine.seek(bar, beat);
setPlayheadPosition({ bar, beat });
}, [getEngine]);
// Set duration
const setDuration = useCallback(async (bars: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setDuration(bars);
}, [getEngine]);
// Set loop region
const setLoopRegion = useCallback(
async (start: number, end: number, enabled: boolean) => {
const engine = await getEngine();
if (!engine) return;
engine.setLoopRegion(start, end, enabled);
},
[getEngine]
);
// Get meter levels on demand
const getMeterLevels = useCallback(async (): Promise<MeterLevels> => {
const engine = await getEngine();
if (!engine) return defaultMeterLevels;
return engine.getMeterLevels();
}, [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,
meterLevels,
pans,
soloed,
playheadPosition,
initialize,
togglePlayback,
stop,
generateNewBeat,
setBpm,
setSwing,
setMasterVolume,
setLayerVolume,
toggleMute,
setMuted,
setPan,
setSolo,
toggleSolo,
seek,
setDuration,
setLoopRegion,
getMeterLevels,
};
}