/** * Hook for recording automation data during playback * Supports write, touch, and latch modes */ import { useCallback, useRef } from 'react'; import type { Track } from '@/types/track'; import type { AutomationPoint, AutomationMode } from '@/types/automation'; export interface AutomationRecordingState { isRecording: boolean; recordingLaneId: string | null; touchActive: boolean; // For touch mode - tracks if control is being touched latchTriggered: boolean; // For latch mode - tracks if recording has started } export function useAutomationRecording( track: Track, onUpdateTrack: (trackId: string, updates: Partial) => void ) { const recordingStateRef = useRef>(new Map()); const recordingIntervalRef = useRef>(new Map()); const lastRecordedValueRef = useRef>(new Map()); /** * Start recording automation for a specific lane */ const startRecording = useCallback((laneId: string, mode: AutomationMode) => { const state: AutomationRecordingState = { isRecording: mode === 'write', recordingLaneId: laneId, touchActive: false, latchTriggered: false, }; recordingStateRef.current.set(laneId, state); }, []); /** * Stop recording automation for a specific lane */ const stopRecording = useCallback((laneId: string) => { recordingStateRef.current.delete(laneId); const intervalId = recordingIntervalRef.current.get(laneId); if (intervalId) { clearInterval(intervalId); recordingIntervalRef.current.delete(laneId); } lastRecordedValueRef.current.delete(laneId); }, []); /** * Record a single automation point */ const recordPoint = useCallback(( laneId: string, currentTime: number, value: number, mode: AutomationMode ) => { const lane = track.automation.lanes.find(l => l.id === laneId); if (!lane) return; const state = recordingStateRef.current.get(laneId); if (!state) return; // Check if we should record based on mode let shouldRecord = false; switch (mode) { case 'write': // Always record in write mode shouldRecord = true; break; case 'touch': // Only record when control is being touched shouldRecord = state.touchActive; break; case 'latch': // Record from first touch until stop if (state.touchActive && !state.latchTriggered) { state.latchTriggered = true; } shouldRecord = state.latchTriggered; break; default: shouldRecord = false; } if (!shouldRecord) return; // Check if value has changed significantly (avoid redundant points) const lastValue = lastRecordedValueRef.current.get(laneId); if (lastValue !== undefined && Math.abs(lastValue - value) < 0.001) { return; // Skip if value hasn't changed } lastRecordedValueRef.current.set(laneId, value); // In write mode, clear existing points in the time range let updatedPoints = [...lane.points]; if (mode === 'write') { // Remove points that are within a small time window of current time updatedPoints = updatedPoints.filter(p => Math.abs(p.time - currentTime) > 0.05 // 50ms threshold ); } // Add new point const newPoint: AutomationPoint = { id: `point-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, time: currentTime, value, curve: 'linear', }; updatedPoints.push(newPoint); // Sort points by time updatedPoints.sort((a, b) => a.time - b.time); // Update track with new automation points const updatedLanes = track.automation.lanes.map(l => l.id === laneId ? { ...l, points: updatedPoints } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes, }, }); }, [track, onUpdateTrack]); /** * Set touch state for touch mode */ const setTouchActive = useCallback((laneId: string, active: boolean) => { const state = recordingStateRef.current.get(laneId); if (state) { state.touchActive = active; } }, []); /** * Check if a lane is currently recording */ const isRecordingLane = useCallback((laneId: string): boolean => { const state = recordingStateRef.current.get(laneId); return state?.isRecording ?? false; }, []); /** * Cleanup - stop all recording */ const cleanup = useCallback(() => { recordingStateRef.current.forEach((_, laneId) => { stopRecording(laneId); }); recordingStateRef.current.clear(); }, [stopRecording]); return { startRecording, stopRecording, recordPoint, setTouchActive, isRecordingLane, cleanup, }; }