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