174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<Track>) => void
|
||
|
|
) {
|
||
|
|
const recordingStateRef = useRef<Map<string, AutomationRecordingState>>(new Map());
|
||
|
|
const recordingIntervalRef = useRef<Map<string, number>>(new Map());
|
||
|
|
const lastRecordedValueRef = useRef<Map<string, number>>(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,
|
||
|
|
};
|
||
|
|
}
|