Implemented comprehensive real-time effect processing for multi-track audio: Core Features: - Per-track effect chains with drag-and-drop reordering - Effect bypass/enable toggle per effect - Real-time parameter updates (filters, dynamics, time-based, distortion, bitcrusher, pitch, timestretch) - Add/remove effects during playback without interruption - Effect chain persistence via localStorage - Automatic playback stop when tracks are deleted Technical Implementation: - Effect processor with dry/wet routing for bypass functionality - Real-time effect parameter updates using AudioParam setValueAtTime - Structure change detection for add/remove/reorder operations - Stale closure fix using refs for latest track state - ScriptProcessorNode for bitcrusher, pitch shifter, and time stretch - Dual-tap delay line for pitch shifting - Overlap-add synthesis for time stretching UI Components: - EffectBrowser dialog with categorized effects - EffectDevice component with parameter controls - EffectParameters for all 19 real-time effect types - Device rack with horizontal scrolling (Ableton-style) Removed offline-only effects (normalize, fadeIn, fadeOut, reverse) as they don't fit the real-time processing model. Completed all items in Phase 7.4: - [x] Per-track effect chain - [x] Effect rack UI - [x] Effect bypass per track - [x] Real-time effect processing during playback - [x] Add/remove effects during playback - [x] Real-time parameter updates - [x] Effect chain persistence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
106 lines
3.5 KiB
TypeScript
106 lines
3.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;
|
|
}
|
|
|
|
export function EffectDevice({
|
|
effect,
|
|
onToggleEnabled,
|
|
onRemove,
|
|
onUpdateParameters,
|
|
}: EffectDeviceProps) {
|
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex-shrink-0 flex flex-col h-full border-l border-border transition-all duration-200',
|
|
effect.enabled ? 'bg-accent/20' : 'bg-muted/20',
|
|
isExpanded ? 'min-w-96' : 'w-10'
|
|
)}
|
|
>
|
|
{!isExpanded ? (
|
|
/* Collapsed State - No Header */
|
|
<button
|
|
onClick={() => setIsExpanded(true)}
|
|
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 h-1 rounded-full flex-shrink-0 mb-1',
|
|
effect.enabled ? 'bg-primary' : 'bg-muted-foreground/30'
|
|
)}
|
|
title={effect.enabled ? 'Enabled' : 'Disabled'}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<>
|
|
{/* Full-Width Header Row */}
|
|
<div className="flex items-center gap-1 px-2 py-1 border-b border-border bg-card/30 flex-shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={() => setIsExpanded(false)}
|
|
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-2">
|
|
<EffectParameters effect={effect} onUpdateParameters={onUpdateParameters} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|