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>
This commit is contained in:
2025-11-18 23:29:18 +01:00
parent a1f230a6e6
commit c54d5089c5
13 changed files with 1040 additions and 70 deletions

View File

@@ -13,6 +13,10 @@ export interface EffectDeviceProps {
onRemove?: () => void;
onUpdateParameters?: (parameters: any) => void;
onToggleExpanded?: () => void;
trackId?: string;
isPlaying?: boolean;
onParameterTouched?: (trackId: string, laneId: string, touched: boolean) => void;
automationLanes?: Array<{ id: string; parameterId: string; mode: string }>;
}
export function EffectDevice({
@@ -21,6 +25,10 @@ export function EffectDevice({
onRemove,
onUpdateParameters,
onToggleExpanded,
trackId,
isPlaying,
onParameterTouched,
automationLanes,
}: EffectDeviceProps) {
const isExpanded = effect.expanded || false;
@@ -108,7 +116,14 @@ export function EffectDevice({
{/* Device Body */}
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar p-3 bg-card/50">
<EffectParameters effect={effect} onUpdateParameters={onUpdateParameters} />
<EffectParameters
effect={effect}
onUpdateParameters={onUpdateParameters}
trackId={trackId}
isPlaying={isPlaying}
onParameterTouched={onParameterTouched}
automationLanes={automationLanes}
/>
</div>
</>
)}

View File

@@ -27,9 +27,20 @@ import type { FilterOptions } from '@/lib/audio/effects/filters';
export interface EffectParametersProps {
effect: ChainEffect;
onUpdateParameters?: (parameters: any) => void;
trackId?: string;
isPlaying?: boolean;
onParameterTouched?: (trackId: string, laneId: string, touched: boolean) => void;
automationLanes?: Array<{ id: string; parameterId: string; mode: string }>;
}
export function EffectParameters({ effect, onUpdateParameters }: EffectParametersProps) {
export function EffectParameters({
effect,
onUpdateParameters,
trackId,
isPlaying,
onParameterTouched,
automationLanes = []
}: EffectParametersProps) {
const params = effect.parameters || {};
const updateParam = (key: string, value: any) => {
@@ -38,6 +49,47 @@ export function EffectParameters({ effect, onUpdateParameters }: EffectParameter
}
};
// Memoize touch handlers for all parameters
const touchHandlers = React.useMemo(() => {
if (!trackId || !isPlaying || !onParameterTouched || !automationLanes) {
return {};
}
const handlers: Record<string, { onTouchStart: () => void; onTouchEnd: () => void }> = {};
automationLanes.forEach(lane => {
if (!lane.parameterId.startsWith(`effect.${effect.id}.`)) {
return;
}
// For effect parameters, write mode works like touch mode
if (lane.mode !== 'touch' && lane.mode !== 'latch' && lane.mode !== 'write') {
return;
}
// Extract parameter name from parameterId (effect.{effectId}.{paramName})
const parts = lane.parameterId.split('.');
if (parts.length !== 3) return;
const paramName = parts[2];
handlers[paramName] = {
onTouchStart: () => {
queueMicrotask(() => onParameterTouched(trackId, lane.id, true));
},
onTouchEnd: () => {
queueMicrotask(() => onParameterTouched(trackId, lane.id, false));
},
};
});
return handlers;
}, [trackId, isPlaying, onParameterTouched, effect.id, automationLanes]);
// Helper to get touch handlers for a parameter
const getTouchHandlers = (paramName: string) => {
return touchHandlers[paramName] || {};
};
// Filter effects
if (['lowpass', 'highpass', 'bandpass', 'notch', 'lowshelf', 'highshelf', 'peaking'].includes(effect.type)) {
const filterParams = params as FilterOptions;
@@ -53,6 +105,7 @@ export function EffectParameters({ effect, onUpdateParameters }: EffectParameter
min={20}
max={20000}
step={1}
{...getTouchHandlers('frequency')}
/>
</div>
<div className="space-y-1">
@@ -65,6 +118,7 @@ export function EffectParameters({ effect, onUpdateParameters }: EffectParameter
min={0.1}
max={20}
step={0.1}
{...getTouchHandlers('Q')}
/>
</div>
{['lowshelf', 'highshelf', 'peaking'].includes(effect.type) && (
@@ -78,6 +132,7 @@ export function EffectParameters({ effect, onUpdateParameters }: EffectParameter
min={-40}
max={40}
step={0.5}
{...getTouchHandlers('gain')}
/>
</div>
)}