feat: complete Phase 7.4 - real-time track effects system

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>
This commit is contained in:
2025-11-18 12:08:33 +01:00
parent cbcd38b1ed
commit beb7085c89
11 changed files with 2180 additions and 100 deletions

View File

@@ -1,10 +1,11 @@
'use client';
import * as React from 'react';
import { ChevronDown, ChevronUp, Power, X } from 'lucide-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;
@@ -24,84 +25,80 @@ export function EffectDevice({
return (
<div
className={cn(
'flex-shrink-0 w-48 border border-border rounded-md overflow-hidden',
effect.enabled ? 'bg-accent/30' : 'bg-muted/30'
'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'
)}
>
{/* Device Header */}
<div className="flex items-center justify-between px-2 py-1.5 border-b border-border bg-card/50">
{!isExpanded ? (
/* Collapsed State - No Header */
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 flex-1 min-w-0 text-left hover:text-primary transition-colors"
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}`}
>
<span className="text-xs font-medium truncate">{effect.name}</span>
{isExpanded ? (
<ChevronUp className="h-3 w-3 flex-shrink-0" />
) : (
<ChevronDown className="h-3 w-3 flex-shrink-0" />
)}
</button>
<div className="flex items-center gap-0.5 ml-1">
<Button
variant="ghost"
size="icon-sm"
onClick={onToggleEnabled}
title={effect.enabled ? 'Disable effect' : 'Enable effect'}
className="h-5 w-5"
<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',
}}
>
<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"
>
<X className="h-3 w-3 text-destructive" />
</Button>
</div>
</div>
{/* Device Parameters */}
{isExpanded && (
<div className="p-2 space-y-2 bg-card/30">
<div className="text-xs text-muted-foreground">
<div className="mb-1">
<span className="font-medium">Type:</span> {effect.type}
</div>
{effect.parameters && Object.keys(effect.parameters).length > 0 && (
<div>
<span className="font-medium">Parameters:</span>
<div className="mt-1 space-y-1 pl-2">
{Object.entries(effect.parameters).map(([key, value]) => (
<div key={key} className="flex justify-between">
<span className="text-muted-foreground/70">{key}:</span>
<span>{String(value)}</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="text-xs text-muted-foreground/70 italic">
Parameter controls coming soon
</div>
</div>
)}
{/* Collapsed State Indicator */}
{!isExpanded && (
<div className="px-2 py-1 text-center">
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">
{effect.type}
{effect.name}
</span>
</div>
<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>
);