feat: refactor to multi-track only editor with sidebar controls
Major refactor to simplify the editor and focus exclusively on multi-track editing: **AudioEditor Changes:** - Removed single-file waveform view and useAudioPlayer - Removed all single-file editing operations (cut, copy, paste, trim) - Simplified to multi-track only with track selection support - Added selectedTrackId state for track-specific effect chain - Reduced from ~1500 lines to ~290 lines **SidePanel Changes:** - Complete rewrite with 2 tabs: Tracks and Effect Chain - Tracks tab shows all tracks with inline controls (volume, pan, solo, mute) - Click tracks to select/deselect - Effect Chain tab shows effects for selected track - Removed old file/history/info/effects tabs - Sidebar now wider (320px) to accommodate inline track controls **TrackList/Track Changes:** - Added track selection support (isSelected/onSelect props) - Visual feedback with ring border when track is selected - Click anywhere on track to select it **Workflow:** 1. Import or add audio tracks 2. Select a track in the sidebar or main view 3. Apply effects to selected track via Effect Chain tab 4. Adjust track controls (volume, pan, solo, mute) in Tracks tab 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,43 +2,34 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
FileAudio,
|
||||
History,
|
||||
Info,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Upload,
|
||||
Download,
|
||||
X,
|
||||
Sparkles,
|
||||
Plus,
|
||||
Trash2,
|
||||
Link2,
|
||||
FolderOpen,
|
||||
Trash2,
|
||||
Layers,
|
||||
Plus,
|
||||
Volume2,
|
||||
Music2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Slider } from '@/components/ui/Slider';
|
||||
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 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';
|
||||
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;
|
||||
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;
|
||||
|
||||
// Effect chain
|
||||
effectChain: EffectChain;
|
||||
@@ -51,44 +42,18 @@ export interface SidePanelProps {
|
||||
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,
|
||||
tracks,
|
||||
selectedTrackId,
|
||||
onSelectTrack,
|
||||
onAddTrack,
|
||||
onImportTracks,
|
||||
onUpdateTrack,
|
||||
onRemoveTrack,
|
||||
onClearTracks,
|
||||
effectChain,
|
||||
effectPresets,
|
||||
onToggleEffect,
|
||||
@@ -98,47 +63,13 @@ export function SidePanel({
|
||||
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 [activeTab, setActiveTab] = React.useState<'tracks' | 'chain'>('tracks');
|
||||
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);
|
||||
}
|
||||
};
|
||||
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
@@ -161,17 +92,17 @@ export function SidePanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('w-64 bg-card border-r border-border flex flex-col', className)}>
|
||||
<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 === 'file' ? 'secondary' : 'ghost'}
|
||||
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
|
||||
size="icon-sm"
|
||||
onClick={() => setActiveTab('file')}
|
||||
title="File"
|
||||
onClick={() => setActiveTab('tracks')}
|
||||
title="Tracks"
|
||||
>
|
||||
<FileAudio className="h-4 w-4" />
|
||||
<Music2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'chain' ? 'secondary' : 'ghost'}
|
||||
@@ -181,38 +112,6 @@ export function SidePanel({
|
||||
>
|
||||
<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"
|
||||
@@ -226,62 +125,172 @@ export function SidePanel({
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
|
||||
{activeTab === 'file' && (
|
||||
{activeTab === 'tracks' && (
|
||||
<>
|
||||
{/* Track Actions */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Audio File
|
||||
Multi-Track Editor
|
||||
</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 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 */}
|
||||
{tracks.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Tracks ({tracks.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{tracks.map((track) => {
|
||||
const isSelected = selectedTrackId === track.id;
|
||||
return (
|
||||
<div
|
||||
key={track.id}
|
||||
className={cn(
|
||||
'p-3 rounded-lg border transition-colors cursor-pointer',
|
||||
isSelected
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'bg-secondary/30 border-border hover:border-primary/50'
|
||||
)}
|
||||
onClick={() => onSelectTrack(isSelected ? null : track.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-foreground truncate">
|
||||
{track.name}
|
||||
</div>
|
||||
{track.audioBuffer && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{formatDuration(track.audioBuffer.duration)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveTrack(track.id);
|
||||
}}
|
||||
title="Remove track"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Track Controls - Always visible */}
|
||||
<div className="space-y-2">
|
||||
{/* Volume */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Volume2 className="h-3 w-3" />
|
||||
Volume
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{Math.round(track.volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={track.volume}
|
||||
onChange={(value) => onUpdateTrack(track.id, { volume: value })}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pan */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground">Pan</label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{track.pan === 0
|
||||
? 'C'
|
||||
: track.pan < 0
|
||||
? `L${Math.round(Math.abs(track.pan) * 100)}`
|
||||
: `R${Math.round(track.pan * 100)}`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={track.pan}
|
||||
onChange={(value) => onUpdateTrack(track.id, { pan: value })}
|
||||
min={-1}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Solo / Mute */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={track.solo ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTrack(track.id, { solo: !track.solo });
|
||||
}}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
Solo
|
||||
</Button>
|
||||
<Button
|
||||
variant={track.mute ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTrack(track.id, { mute: !track.mute });
|
||||
}}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
Mute
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -290,6 +299,9 @@ export function SidePanel({
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Effect Chain
|
||||
{selectedTrack && (
|
||||
<span className="text-primary ml-2">({selectedTrack.name})</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
@@ -312,380 +324,37 @@ export function SidePanel({
|
||||
)}
|
||||
</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>
|
||||
{!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>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
No history available. Edit operations will appear here.
|
||||
</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 === '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>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface TrackProps {
|
||||
zoom: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isSelected?: boolean;
|
||||
onSelect?: () => void;
|
||||
onToggleMute: () => void;
|
||||
onToggleSolo: () => void;
|
||||
onToggleCollapse: () => void;
|
||||
@@ -25,6 +27,8 @@ export function Track({
|
||||
zoom,
|
||||
currentTime,
|
||||
duration,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onToggleMute,
|
||||
onToggleSolo,
|
||||
onToggleCollapse,
|
||||
@@ -121,7 +125,13 @@ export function Track({
|
||||
|
||||
if (track.collapsed) {
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<div
|
||||
className={cn(
|
||||
'border-b border-border cursor-pointer',
|
||||
isSelected && 'ring-2 ring-primary ring-inset'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<TrackHeader
|
||||
track={track}
|
||||
onToggleMute={onToggleMute}
|
||||
@@ -140,9 +150,10 @@ export function Track({
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'border-b border-border',
|
||||
track.selected && 'ring-2 ring-primary ring-inset'
|
||||
'border-b border-border cursor-pointer',
|
||||
isSelected && 'ring-2 ring-primary ring-inset'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<TrackHeader
|
||||
track={track}
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface TrackListProps {
|
||||
zoom: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
selectedTrackId?: string | null;
|
||||
onSelectTrack?: (trackId: string | null) => void;
|
||||
onAddTrack: () => void;
|
||||
onImportTrack?: (buffer: AudioBuffer, name: string) => void;
|
||||
onRemoveTrack: (trackId: string) => void;
|
||||
@@ -24,6 +26,8 @@ export function TrackList({
|
||||
zoom,
|
||||
currentTime,
|
||||
duration,
|
||||
selectedTrackId,
|
||||
onSelectTrack,
|
||||
onAddTrack,
|
||||
onImportTrack,
|
||||
onRemoveTrack,
|
||||
@@ -78,6 +82,8 @@ export function TrackList({
|
||||
zoom={zoom}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isSelected={selectedTrackId === track.id}
|
||||
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||
onToggleMute={() =>
|
||||
onUpdateTrack(track.id, { mute: !track.mute })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user