Files
audio-ui/components/layout/SidePanel.tsx
Sebastian Krüger 832a18dd9c feat: integrate multi-track functionality into main AudioEditor
Added comprehensive multi-track support to the main application:
- Added "Tracks" tab to SidePanel with track management controls
- Integrated useMultiTrack and useMultiTrackPlayer hooks into AudioEditor
- Added view mode switching between waveform and tracks views
- Implemented "Convert to Track" to convert current audio buffer to track
- Added TrackList view with multi-track playback controls
- Wired up ImportTrackDialog for importing multiple audio files

Users can now:
- Click "Tracks" tab in side panel to access multi-track mode
- Convert current audio to a track
- Import multiple audio files as tracks
- View and manage tracks in dedicated TrackList view
- Play multiple tracks simultaneously with individual controls

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 21:57:31 +01:00

693 lines
22 KiB
TypeScript

'use client';
import * as React from 'react';
import {
FileAudio,
History,
Info,
ChevronLeft,
ChevronRight,
Upload,
Download,
X,
Sparkles,
Link2,
FolderOpen,
Trash2,
Layers,
Plus,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import { formatDuration } from '@/lib/audio/decoder';
import type { Selection } from '@/types/selection';
import type { HistoryState } from '@/lib/history/history-manager';
import type { EffectChain, ChainEffect, EffectPreset } from '@/lib/audio/effects/chain';
import { EffectRack } from '@/components/effects/EffectRack';
import { PresetManager } from '@/components/effects/PresetManager';
import type { Track } from '@/types/track';
export interface SidePanelProps {
// File info
fileName: string | null;
audioBuffer: AudioBuffer | null;
onFileSelect: (file: File) => void;
onClear: () => void;
// Selection info
selection: Selection | null;
// History info
historyState: HistoryState;
// Effect chain
effectChain: EffectChain;
effectPresets: EffectPreset[];
onToggleEffect: (effectId: string) => void;
onRemoveEffect: (effectId: string) => void;
onReorderEffects: (fromIndex: number, toIndex: number) => void;
onSavePreset: (preset: EffectPreset) => void;
onLoadPreset: (preset: EffectPreset) => void;
onDeletePreset: (presetId: string) => void;
onClearChain: () => void;
// Effects handlers
onNormalize: () => void;
onFadeIn: () => void;
onFadeOut: () => void;
onReverse: () => void;
onLowPassFilter: () => void;
onHighPassFilter: () => void;
onBandPassFilter: () => void;
onCompressor: () => void;
onLimiter: () => void;
onGate: () => void;
onDelay: () => void;
onReverb: () => void;
onChorus: () => void;
onFlanger: () => void;
onPhaser: () => void;
onPitchShift: () => void;
onTimeStretch: () => void;
onDistortion: () => void;
onBitcrusher: () => void;
// Multi-track
tracks?: Track[];
onAddTrack?: () => void;
onImportTracks?: () => void;
onConvertToTrack?: () => void;
onClearTracks?: () => void;
className?: string;
}
export function SidePanel({
fileName,
audioBuffer,
onFileSelect,
onClear,
selection,
historyState,
effectChain,
effectPresets,
onToggleEffect,
onRemoveEffect,
onReorderEffects,
onSavePreset,
onLoadPreset,
onDeletePreset,
onClearChain,
onNormalize,
onFadeIn,
onFadeOut,
onReverse,
onLowPassFilter,
onHighPassFilter,
onBandPassFilter,
onCompressor,
onLimiter,
onGate,
onDelay,
onReverb,
onChorus,
onFlanger,
onPhaser,
onPitchShift,
onTimeStretch,
onDistortion,
onBitcrusher,
tracks,
onAddTrack,
onImportTracks,
onConvertToTrack,
onClearTracks,
className,
}: SidePanelProps) {
const [isCollapsed, setIsCollapsed] = React.useState(false);
const [activeTab, setActiveTab] = React.useState<'file' | 'chain' | 'history' | 'info' | 'effects' | 'tracks'>('file');
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleFileClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onFileSelect(file);
}
};
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-64 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 === 'file' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('file')}
title="File"
>
<FileAudio className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'chain' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('chain')}
title="Effect Chain"
>
<Link2 className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'effects' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('effects')}
title="Effects"
>
<Sparkles className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('tracks')}
title="Tracks"
>
<Layers className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'history' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('history')}
title="History"
>
<History className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'info' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('info')}
title="Info"
>
<Info className="h-4 w-4" />
</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 === 'file' && (
<>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Audio File
</h3>
{audioBuffer ? (
<div className="space-y-2">
<div className="p-2 bg-secondary/30 rounded text-xs">
<div className="font-medium text-foreground truncate" title={fileName || 'Unknown'}>
{fileName || 'Unknown'}
</div>
<div className="text-muted-foreground mt-1">
Duration: {formatDuration(audioBuffer.duration)}
</div>
<div className="text-muted-foreground">
Channels: {audioBuffer.numberOfChannels}
</div>
<div className="text-muted-foreground">
Sample Rate: {audioBuffer.sampleRate} Hz
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={onClear}
className="w-full"
>
<X className="h-3.5 w-3.5 mr-1.5" />
Clear File
</Button>
</div>
) : (
<div className="space-y-2">
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={handleFileChange}
className="hidden"
/>
<Button
variant="outline"
size="sm"
onClick={handleFileClick}
className="w-full"
>
<Upload className="h-3.5 w-3.5 mr-1.5" />
Load Audio File
</Button>
<div className="text-xs text-muted-foreground">
Or drag and drop an audio file onto the waveform area.
</div>
</div>
)}
</div>
</>
)}
{activeTab === 'chain' && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Effect Chain
</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>
{effectChain.effects.length > 0 && (
<Button
variant="ghost"
size="icon-sm"
onClick={onClearChain}
title="Clear all effects"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</div>
<EffectRack
chain={effectChain}
onToggleEffect={onToggleEffect}
onRemoveEffect={onRemoveEffect}
onReorderEffects={onReorderEffects}
/>
<PresetManager
open={presetDialogOpen}
onClose={() => setPresetDialogOpen(false)}
currentChain={effectChain}
presets={effectPresets}
onSavePreset={onSavePreset}
onLoadPreset={onLoadPreset}
onDeletePreset={onDeletePreset}
onExportPreset={() => {}}
onImportPreset={(preset) => onSavePreset(preset)}
/>
</div>
)}
{activeTab === 'history' && (
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Edit History
</h3>
{historyState.historySize > 0 ? (
<div className="space-y-1 text-xs">
<div className="p-2 bg-secondary/30 rounded">
<div className="text-foreground">
{historyState.historySize} action{historyState.historySize !== 1 ? 's' : ''}
</div>
{historyState.undoDescription && (
<div className="text-muted-foreground mt-1">
Next undo: {historyState.undoDescription}
</div>
)}
{historyState.redoDescription && (
<div className="text-muted-foreground mt-1">
Next redo: {historyState.redoDescription}
</div>
)}
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
No history available. Edit operations will appear here.
</div>
)}
</div>
)}
{activeTab === 'info' && (
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Selection Info
</h3>
{selection ? (
<div className="p-2 bg-secondary/30 rounded text-xs">
<div className="text-foreground font-medium">Selection Active</div>
<div className="text-muted-foreground mt-1">
Duration: {formatDuration(selection.end - selection.start)}
</div>
<div className="text-muted-foreground">
Start: {formatDuration(selection.start)}
</div>
<div className="text-muted-foreground">
End: {formatDuration(selection.end)}
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
No selection. Drag on the waveform to select a region.
</div>
)}
</div>
)}
{activeTab === 'effects' && (
<div className="space-y-3">
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Basic Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onNormalize}
className="w-full justify-start text-xs"
>
Normalize
</Button>
<Button
variant="outline"
size="sm"
onClick={onFadeIn}
className="w-full justify-start text-xs"
>
Fade In
</Button>
<Button
variant="outline"
size="sm"
onClick={onFadeOut}
className="w-full justify-start text-xs"
>
Fade Out
</Button>
<Button
variant="outline"
size="sm"
onClick={onReverse}
className="w-full justify-start text-xs"
>
Reverse
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply effects.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Filters
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onLowPassFilter}
className="w-full justify-start text-xs"
>
Low-Pass Filter
</Button>
<Button
variant="outline"
size="sm"
onClick={onHighPassFilter}
className="w-full justify-start text-xs"
>
High-Pass Filter
</Button>
<Button
variant="outline"
size="sm"
onClick={onBandPassFilter}
className="w-full justify-start text-xs"
>
Band-Pass Filter
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply filters.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Dynamics Processing
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onCompressor}
className="w-full justify-start text-xs"
>
Compressor
</Button>
<Button
variant="outline"
size="sm"
onClick={onLimiter}
className="w-full justify-start text-xs"
>
Limiter
</Button>
<Button
variant="outline"
size="sm"
onClick={onGate}
className="w-full justify-start text-xs"
>
Gate/Expander
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply dynamics processing.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Time-Based Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onDelay}
className="w-full justify-start text-xs"
>
Delay/Echo
</Button>
<Button
variant="outline"
size="sm"
onClick={onReverb}
className="w-full justify-start text-xs"
>
Reverb
</Button>
<Button
variant="outline"
size="sm"
onClick={onChorus}
className="w-full justify-start text-xs"
>
Chorus
</Button>
<Button
variant="outline"
size="sm"
onClick={onFlanger}
className="w-full justify-start text-xs"
>
Flanger
</Button>
<Button
variant="outline"
size="sm"
onClick={onPhaser}
className="w-full justify-start text-xs"
>
Phaser
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply time-based effects.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Advanced Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onPitchShift}
className="w-full justify-start text-xs"
>
Pitch Shifter
</Button>
<Button
variant="outline"
size="sm"
onClick={onTimeStretch}
className="w-full justify-start text-xs"
>
Time Stretch
</Button>
<Button
variant="outline"
size="sm"
onClick={onDistortion}
className="w-full justify-start text-xs"
>
Distortion
</Button>
<Button
variant="outline"
size="sm"
onClick={onBitcrusher}
className="w-full justify-start text-xs"
>
Bitcrusher
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply advanced effects.
</div>
)}
</div>
</div>
)}
{activeTab === 'tracks' && (
<div className="space-y-3">
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Multi-Track Mode
</h3>
{tracks && tracks.length > 0 ? (
<div className="space-y-2">
<div className="p-2 bg-secondary/30 rounded text-xs">
<div className="text-foreground font-medium">
{tracks.length} {tracks.length === 1 ? 'track' : 'tracks'} active
</div>
<div className="text-muted-foreground mt-1">
Switch to Tracks view to manage
</div>
</div>
{onClearTracks && (
<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>
) : (
<div className="space-y-2">
<div className="text-xs text-muted-foreground">
Add tracks to work with multiple audio files simultaneously.
</div>
{audioBuffer && onConvertToTrack && (
<Button
variant="outline"
size="sm"
onClick={onConvertToTrack}
className="w-full"
>
<Plus className="h-3.5 w-3.5 mr-1.5" />
Convert Current to Track
</Button>
)}
{onAddTrack && (
<Button
variant="outline"
size="sm"
onClick={onAddTrack}
className="w-full"
>
<Plus className="h-3.5 w-3.5 mr-1.5" />
Add Empty Track
</Button>
)}
{onImportTracks && (
<Button
variant="outline"
size="sm"
onClick={onImportTracks}
className="w-full"
>
<Upload className="h-3.5 w-3.5 mr-1.5" />
Import Audio Files
</Button>
)}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}