import { useState, useRef, useEffect, useCallback } from 'react' import { useStore } from '@nanostores/react' import type { TileState } from '../types/tiles' import type { SynthesisMode } from '../stores/synthesisMode' import { PlaybackManager } from '../services/PlaybackManager' import { engineSettings, effectSettings } from '../stores/settings' import { getSampleRateFromIndex } from '../config/parameters' import { DEFAULT_VARIABLES, LOOP_DURATION } from '../constants/defaults' interface UsePlaybackControlProps { mode: SynthesisMode onPlaybackPositionUpdate?: (position: number) => void } export function usePlaybackControl({ mode, onPlaybackPositionUpdate }: UsePlaybackControlProps) { const engineValues = useStore(engineSettings) const effectValues = useStore(effectSettings) const [playing, setPlaying] = useState(null) const [queued, setQueued] = useState(null) const [playbackPosition, setPlaybackPosition] = useState(0) const playbackManagerRef = useRef(null) const switchTimerRef = useRef(null) useEffect(() => { if (playbackManagerRef.current) { playbackManagerRef.current.setPlaybackPositionCallback((position) => { setPlaybackPosition(position) onPlaybackPositionUpdate?.(position) }) } }, [onPlaybackPositionUpdate]) const clearSwitchTimer = useCallback(() => { if (switchTimerRef.current !== null) { clearTimeout(switchTimerRef.current) switchTimerRef.current = null } }, []) const startSwitchTimer = useCallback((callback: () => void) => { clearSwitchTimer() switchTimerRef.current = window.setTimeout(callback, engineValues.loopCount * 1000) }, [engineValues.loopCount, clearSwitchTimer]) useEffect(() => { return () => clearSwitchTimer() }, [clearSwitchTimer]) const play = useCallback(async (formula: string, id: string, tile?: TileState) => { const sampleRate = getSampleRateFromIndex(engineValues.sampleRate) const duration = LOOP_DURATION if (!playbackManagerRef.current) { playbackManagerRef.current = new PlaybackManager({ sampleRate, duration }) playbackManagerRef.current.setMode(mode) } else { await playbackManagerRef.current.updateOptions({ sampleRate, duration }) } playbackManagerRef.current.stop() playbackManagerRef.current.setEffects(effectValues) playbackManagerRef.current.setVariables( engineValues.a ?? DEFAULT_VARIABLES.a, engineValues.b ?? DEFAULT_VARIABLES.b, engineValues.c ?? DEFAULT_VARIABLES.c, engineValues.d ?? DEFAULT_VARIABLES.d ) playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0) const fmPatch = mode === 'fm' ? JSON.parse(formula) : null const lfoRates = fmPatch?.lfoRates || undefined playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0, lfoRates) playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0) if (tile?.lfoConfigs) { playbackManagerRef.current.setLFOConfig(0, tile.lfoConfigs.lfo1) playbackManagerRef.current.setLFOConfig(1, tile.lfoConfigs.lfo2) playbackManagerRef.current.setLFOConfig(2, tile.lfoConfigs.lfo3) playbackManagerRef.current.setLFOConfig(3, tile.lfoConfigs.lfo4) } await playbackManagerRef.current.play(formula) setPlaying(id) setQueued(null) }, [mode, engineValues, effectValues]) const stop = useCallback(() => { clearSwitchTimer() playbackManagerRef.current?.stop() setPlaying(null) setQueued(null) setPlaybackPosition(0) }, [clearSwitchTimer]) const queue = useCallback((id: string, callback: () => void) => { setQueued(id) startSwitchTimer(callback) }, [startSwitchTimer]) const cancelQueue = useCallback(() => { clearSwitchTimer() setQueued(null) }, [clearSwitchTimer]) const updateMode = useCallback((newMode: SynthesisMode) => { if (playbackManagerRef.current) { playbackManagerRef.current.setMode(newMode) playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0) playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0) } }, [engineValues.fmAlgorithm, engineValues.fmFeedback]) return { playing, queued, playbackPosition, playbackManager: playbackManagerRef, play, stop, queue, cancelQueue, updateMode } }