import { create } from 'zustand'; interface TrackPlaybackState { isPlaying: boolean; currentTime: number; startTime: number; endTime: number; playbackSpeed: number; loop: boolean; loopStart: number; loopEnd: number; play: () => void; pause: () => void; stop: () => void; setCurrentTime: (time: number) => void; setPlaybackSpeed: (speed: number) => void; toggleLoop: () => void; setLoopSection: (start: number, end: number) => void; setTimeRange: (start: number, end: number) => void; syncToRangeStart: () => void; reset: () => void; } let animationFrameId: number | null = null; let lastFrameTime: number | null = null; function clearAnimation(): void { if (animationFrameId != null) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } lastFrameTime = null; } export const useTrackPlaybackStore = create()((set, get) => { const animate = (): void => { const state = get(); if (!state.isPlaying) return; const now = performance.now(); if (lastFrameTime == null) { lastFrameTime = now; } const delta = now - lastFrameTime; lastFrameTime = now; const advanceMs = delta * state.playbackSpeed; let nextTime = state.currentTime + advanceMs; const rangeStart = state.loop ? state.loopStart : state.startTime; const rangeEnd = state.loop ? state.loopEnd : state.endTime; if (nextTime >= rangeEnd) { if (state.loop) { nextTime = rangeStart; } else { nextTime = state.endTime; set({ currentTime: nextTime, isPlaying: false }); clearAnimation(); return; } } set({ currentTime: nextTime }); animationFrameId = requestAnimationFrame(animate); }; return { isPlaying: false, currentTime: 0, startTime: 0, endTime: 0, playbackSpeed: 100, loop: false, loopStart: 0, loopEnd: 0, play: () => { const state = get(); if (state.endTime <= state.startTime) return; if (state.currentTime < state.startTime || state.currentTime > state.endTime) { set({ currentTime: state.startTime }); } set({ isPlaying: true }); clearAnimation(); animationFrameId = requestAnimationFrame(animate); }, pause: () => { clearAnimation(); set({ isPlaying: false }); }, stop: () => { clearAnimation(); set((state) => ({ isPlaying: false, currentTime: state.startTime })); }, setCurrentTime: (time: number) => { const { startTime, endTime } = get(); const clamped = Math.max(startTime, Math.min(endTime, time)); set({ currentTime: clamped }); }, setPlaybackSpeed: (speed: number) => { const normalized = Number.isFinite(speed) && speed > 0 ? speed : 1; set({ playbackSpeed: normalized }); }, toggleLoop: () => { set((state) => ({ loop: !state.loop })); }, setLoopSection: (start: number, end: number) => { const state = get(); const clampedStart = Math.max(state.startTime, Math.min(end, start)); const clampedEnd = Math.min(state.endTime, Math.max(start, end)); set({ loopStart: clampedStart, loopEnd: clampedEnd }); }, setTimeRange: (start: number, end: number) => { const safeStart = Number.isFinite(start) ? start : 0; const safeEnd = Number.isFinite(end) ? end : safeStart; clearAnimation(); set({ isPlaying: false, startTime: safeStart, endTime: safeEnd, currentTime: safeStart, loopStart: safeStart, loopEnd: safeEnd, }); }, syncToRangeStart: () => { clearAnimation(); set((state) => ({ isPlaying: false, currentTime: state.startTime, })); }, reset: () => { clearAnimation(); set({ isPlaying: false, currentTime: 0, startTime: 0, endTime: 0, playbackSpeed: 100, loop: false, loopStart: 0, loopEnd: 0, }); }, }; }); export const TRACK_PLAYBACK_SPEED_OPTIONS = [1, 5, 10, 25, 50, 100] as const;