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>
133 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|