Implemented comprehensive automation recording system for volume, pan, and effect parameters: - Added automation recording modes: - Write: Records continuously during playback when values change - Touch: Records only while control is being touched/moved - Latch: Records from first touch until playback stops - Implemented value change detection (0.001 threshold) to prevent infinite loops - Fixed React setState-in-render errors by: - Using queueMicrotask() to defer state updates - Moving lane creation logic to useEffect - Properly memoizing touch handlers with useMemo - Added proper value ranges for effect parameters: - Frequency: 20-20000 Hz - Q: 0.1-20 - Gain: -40-40 dB - Enhanced automation lane auto-creation with parameter-specific ranges - Added touch callbacks to all parameter controls (volume, pan, effects) - Implemented throttling (100ms) to avoid excessive automation points Technical improvements: - Used tracksRef and onRecordAutomationRef to ensure latest values in animation loops - Added proper cleanup on playback stop - Optimized recording to only trigger when values actually change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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,
|
|
};
|
|
}
|