Files
audio-ui/components/layout/SidePanel.tsx
Sebastian Krüger f640f2f9d4 feat: implement per-track and master effect chains (Option 3)
Architecture:
- Each track now has its own effect chain stored in track.effectChain
- Separate master effect chain for the final mix output
- SidePanel has 3 tabs: Tracks, Track FX, Master FX

Changes:
- types/track.ts: Add effectChain field to Track interface
- lib/audio/track-utils.ts: Initialize effect chain when creating tracks
- lib/hooks/useMultiTrack.ts: Exclude effectChain from localStorage, recreate on load
- components/editor/AudioEditor.tsx:
  - Add master effect chain state using useEffectChain hook
  - Add handlers for per-track effect chain manipulation
  - Pass both track and master effect chains to SidePanel
- components/layout/SidePanel.tsx:
  - Update to 3-tab interface (Tracks | Track FX | Master FX)
  - Track FX tab shows effects for currently selected track
  - Master FX tab shows master bus effects with preset management
  - Different icons for track vs master effects tabs

Note: Effect processing in Web Audio API not yet implemented.
This commit sets up the data structures and UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 07:30:46 +01:00

301 lines
9.8 KiB
TypeScript

'use client';
import * as React from 'react';
import {
ChevronLeft,
ChevronRight,
Upload,
Plus,
Trash2,
Link2,
FolderOpen,
Music2,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import type { Track } from '@/types/track';
import type { EffectChain, EffectPreset } from '@/lib/audio/effects/chain';
import { EffectRack } from '@/components/effects/EffectRack';
import { PresetManager } from '@/components/effects/PresetManager';
export interface SidePanelProps {
tracks: Track[];
selectedTrackId: string | null;
onSelectTrack: (trackId: string | null) => void;
onAddTrack: () => void;
onImportTracks: () => void;
onUpdateTrack: (trackId: string, updates: Partial<Track>) => void;
onRemoveTrack: (trackId: string) => void;
onClearTracks: () => void;
// Track effect chain (for selected track)
trackEffectChain: EffectChain | null;
onToggleTrackEffect: (effectId: string) => void;
onRemoveTrackEffect: (effectId: string) => void;
onReorderTrackEffects: (fromIndex: number, toIndex: number) => void;
onClearTrackChain: () => void;
// Master effect chain
masterEffectChain: EffectChain;
masterEffectPresets: EffectPreset[];
onToggleMasterEffect: (effectId: string) => void;
onRemoveMasterEffect: (effectId: string) => void;
onReorderMasterEffects: (fromIndex: number, toIndex: number) => void;
onSaveMasterPreset: (preset: EffectPreset) => void;
onLoadMasterPreset: (preset: EffectPreset) => void;
onDeleteMasterPreset: (presetId: string) => void;
onClearMasterChain: () => void;
className?: string;
}
export function SidePanel({
tracks,
selectedTrackId,
onSelectTrack,
onAddTrack,
onImportTracks,
onUpdateTrack,
onRemoveTrack,
onClearTracks,
trackEffectChain,
onToggleTrackEffect,
onRemoveTrackEffect,
onReorderTrackEffects,
onClearTrackChain,
masterEffectChain,
masterEffectPresets,
onToggleMasterEffect,
onRemoveMasterEffect,
onReorderMasterEffects,
onSaveMasterPreset,
onLoadMasterPreset,
onDeleteMasterPreset,
onClearMasterChain,
className,
}: SidePanelProps) {
const [isCollapsed, setIsCollapsed] = React.useState(false);
const [activeTab, setActiveTab] = React.useState<'tracks' | 'trackFx' | 'masterFx'>('tracks');
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
if (isCollapsed) {
return (
<div
className={cn(
'w-12 bg-card border-r border-border flex flex-col items-center py-2',
className
)}
>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsCollapsed(false)}
title="Expand Side Panel"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
}
return (
<div className={cn('w-80 bg-card border-r border-border flex flex-col', className)}>
{/* Header */}
<div className="flex items-center justify-between p-2 border-b border-border">
<div className="flex items-center gap-1">
<Button
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('tracks')}
title="Tracks"
>
<Music2 className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'trackFx' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('trackFx')}
title="Track Effects"
>
<Link2 className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'masterFx' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('masterFx')}
title="Master Effects"
>
<Link2 className="h-4 w-4 text-primary" />
</Button>
</div>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsCollapsed(true)}
title="Collapse Side Panel"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
{activeTab === 'tracks' && (
<>
{/* Track Actions */}
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Multi-Track Editor
</h3>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={onAddTrack}
className="flex-1"
>
<Plus className="h-3.5 w-3.5 mr-1.5" />
Add Track
</Button>
<Button
variant="outline"
size="sm"
onClick={onImportTracks}
className="flex-1"
>
<Upload className="h-3.5 w-3.5 mr-1.5" />
Import
</Button>
</div>
{tracks.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={onClearTracks}
className="w-full"
>
<Trash2 className="h-3.5 w-3.5 mr-1.5 text-destructive" />
Clear All Tracks
</Button>
)}
</div>
{/* Track List - Simplified */}
{tracks.length > 0 ? (
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Tracks ({tracks.length})
</h3>
<div className="text-xs text-muted-foreground">
<p className="mb-2">
Track controls are located on the left side of each track in the timeline.
</p>
<p>Click a track to select it and apply effects from the Effect Chain tab.</p>
</div>
</div>
) : (
<div className="text-center py-8">
<Music2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-2" />
<p className="text-sm text-muted-foreground mb-4">
No tracks yet. Add or import audio files to get started.
</p>
</div>
)}
</>
)}
{activeTab === 'trackFx' && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Track Effects
{selectedTrack && (
<span className="text-primary ml-2">({String(selectedTrack.name || 'Untitled Track')})</span>
)}
</h3>
<div className="flex gap-1">
{trackEffectChain && trackEffectChain.effects.length > 0 && (
<Button
variant="ghost"
size="icon-sm"
onClick={onClearTrackChain}
title="Clear all effects"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</div>
{!selectedTrack ? (
<div className="text-center py-8">
<Link2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-2" />
<p className="text-sm text-muted-foreground">
Select a track to apply effects
</p>
</div>
) : (
<EffectRack
chain={trackEffectChain!}
onToggleEffect={onToggleTrackEffect}
onRemoveEffect={onRemoveTrackEffect}
onReorderEffects={onReorderTrackEffects}
/>
)}
</div>
)}
{activeTab === 'masterFx' && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Master Effects
</h3>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon-sm"
onClick={() => setPresetDialogOpen(true)}
title="Manage presets"
>
<FolderOpen className="h-4 w-4" />
</Button>
{masterEffectChain.effects.length > 0 && (
<Button
variant="ghost"
size="icon-sm"
onClick={onClearMasterChain}
title="Clear all effects"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</div>
<EffectRack
chain={masterEffectChain}
onToggleEffect={onToggleMasterEffect}
onRemoveEffect={onRemoveMasterEffect}
onReorderEffects={onReorderMasterEffects}
/>
<PresetManager
open={presetDialogOpen}
onClose={() => setPresetDialogOpen(false)}
currentChain={masterEffectChain}
presets={masterEffectPresets}
onSavePreset={onSaveMasterPreset}
onLoadPreset={onLoadMasterPreset}
onDeletePreset={onDeleteMasterPreset}
onExportPreset={() => {}}
onImportPreset={(preset) => onSaveMasterPreset(preset)}
/>
</div>
)}
</div>
</div>
);
}