Files
audio-ui/lib/hooks/useAutomationRecording.ts
Sebastian Krüger c54d5089c5 feat: complete Phase 9.3 - automation recording with write/touch/latch modes
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>
2025-11-18 23:29:18 +01:00

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,
};
}