Enhance FM synthesis + cleaning code architecture
This commit is contained in:
126
src/hooks/usePlaybackControl.ts
Normal file
126
src/hooks/usePlaybackControl.ts
Normal file
@ -0,0 +1,126 @@
|
||||
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<string | null>(null)
|
||||
const [queued, setQueued] = useState<string | null>(null)
|
||||
const [playbackPosition, setPlaybackPosition] = useState<number>(0)
|
||||
|
||||
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
||||
const switchTimerRef = useRef<number | null>(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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user