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:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user