Files
audio-ui/components/effects/EffectDevice.tsx
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

133 lines
4.5 KiB
TypeScript

'use client';
import * as React from 'react';
import { ChevronLeft, ChevronRight, Power, X } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import type { ChainEffect } from '@/lib/audio/effects/chain';
import { EffectParameters } from './EffectParameters';
export interface EffectDeviceProps {
effect: ChainEffect;
onToggleEnabled?: () => void;
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({
effect,
onToggleEnabled,
onRemove,
onUpdateParameters,
onToggleExpanded,
trackId,
isPlaying,
onParameterTouched,
automationLanes,
}: EffectDeviceProps) {
const isExpanded = effect.expanded || false;
return (
<div
className={cn(
'flex-shrink-0 flex flex-col h-full transition-all duration-200 overflow-hidden rounded-md',
effect.enabled
? 'bg-card border-l border-r border-b border-border'
: 'bg-card/40 border-l border-r border-b border-border/50 opacity-60 hover:opacity-80',
isExpanded ? 'min-w-96' : 'w-10'
)}
>
{!isExpanded ? (
/* Collapsed State */
<>
{/* Colored top indicator */}
<div className={cn('h-0.5 w-full', effect.enabled ? 'bg-primary' : 'bg-muted-foreground/20')} />
<button
onClick={onToggleExpanded}
className="w-full h-full flex flex-col items-center justify-between py-1 hover:bg-primary/10 transition-colors group"
title={`Expand ${effect.name}`}
>
<ChevronRight className="h-3 w-3 flex-shrink-0 text-muted-foreground group-hover:text-primary transition-colors" />
<span
className="flex-1 text-xs font-medium whitespace-nowrap text-muted-foreground group-hover:text-primary transition-colors"
style={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
}}
>
{effect.name}
</span>
<div
className={cn(
'w-1.5 h-1.5 rounded-full flex-shrink-0 mb-1',
effect.enabled ? 'bg-primary shadow-sm shadow-primary/50' : 'bg-muted-foreground/30'
)}
title={effect.enabled ? 'Enabled' : 'Disabled'}
/>
</button>
</>
) : (
<>
{/* Colored top indicator */}
<div className={cn('h-0.5 w-full', effect.enabled ? 'bg-primary' : 'bg-muted-foreground/20')} />
{/* Full-Width Header Row */}
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border/50 bg-muted/30 flex-shrink-0">
<Button
variant="ghost"
size="icon-sm"
onClick={onToggleExpanded}
title="Collapse device"
className="h-5 w-5 flex-shrink-0"
>
<ChevronLeft className="h-3 w-3" />
</Button>
<span className="text-xs font-semibold flex-1 min-w-0 truncate">{effect.name}</span>
<Button
variant="ghost"
size="icon-sm"
onClick={onToggleEnabled}
title={effect.enabled ? 'Disable effect' : 'Enable effect'}
className="h-5 w-5 flex-shrink-0"
>
<Power
className={cn(
'h-3 w-3',
effect.enabled ? 'text-primary' : 'text-muted-foreground'
)}
/>
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onRemove}
title="Remove effect"
className="h-5 w-5 flex-shrink-0"
>
<X className="h-3 w-3 text-destructive" />
</Button>
</div>
{/* Device Body */}
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar p-3 bg-card/50">
<EffectParameters
effect={effect}
onUpdateParameters={onUpdateParameters}
trackId={trackId}
isPlaying={isPlaying}
onParameterTouched={onParameterTouched}
automationLanes={automationLanes}
/>
</div>
</>
)}
</div>
);
}