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 * as React from 'react';
|
||||||
import {
|
import {
|
||||||
FileAudio,
|
|
||||||
History,
|
|
||||||
Info,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Upload,
|
Upload,
|
||||||
Download,
|
Plus,
|
||||||
X,
|
Trash2,
|
||||||
Sparkles,
|
|
||||||
Link2,
|
Link2,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Trash2,
|
Volume2,
|
||||||
Layers,
|
Music2,
|
||||||
Plus,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Slider } from '@/components/ui/Slider';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { formatDuration } from '@/lib/audio/decoder';
|
import { formatDuration } from '@/lib/audio/decoder';
|
||||||
import type { Selection } from '@/types/selection';
|
import type { Track } from '@/types/track';
|
||||||
import type { HistoryState } from '@/lib/history/history-manager';
|
import type { EffectChain, EffectPreset } from '@/lib/audio/effects/chain';
|
||||||
import type { EffectChain, ChainEffect, EffectPreset } from '@/lib/audio/effects/chain';
|
|
||||||
import { EffectRack } from '@/components/effects/EffectRack';
|
import { EffectRack } from '@/components/effects/EffectRack';
|
||||||
import { PresetManager } from '@/components/effects/PresetManager';
|
import { PresetManager } from '@/components/effects/PresetManager';
|
||||||
import type { Track } from '@/types/track';
|
|
||||||
|
|
||||||
export interface SidePanelProps {
|
export interface SidePanelProps {
|
||||||
// File info
|
tracks: Track[];
|
||||||
fileName: string | null;
|
selectedTrackId: string | null;
|
||||||
audioBuffer: AudioBuffer | null;
|
onSelectTrack: (trackId: string | null) => void;
|
||||||
onFileSelect: (file: File) => void;
|
onAddTrack: () => void;
|
||||||
onClear: () => void;
|
onImportTracks: () => void;
|
||||||
|
onUpdateTrack: (trackId: string, updates: Partial<Track>) => void;
|
||||||
// Selection info
|
onRemoveTrack: (trackId: string) => void;
|
||||||
selection: Selection | null;
|
onClearTracks: () => void;
|
||||||
|
|
||||||
// History info
|
|
||||||
historyState: HistoryState;
|
|
||||||
|
|
||||||
// Effect chain
|
// Effect chain
|
||||||
effectChain: EffectChain;
|
effectChain: EffectChain;
|
||||||
@@ -51,44 +42,18 @@ export interface SidePanelProps {
|
|||||||
onDeletePreset: (presetId: string) => void;
|
onDeletePreset: (presetId: string) => void;
|
||||||
onClearChain: () => 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;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidePanel({
|
export function SidePanel({
|
||||||
fileName,
|
tracks,
|
||||||
audioBuffer,
|
selectedTrackId,
|
||||||
onFileSelect,
|
onSelectTrack,
|
||||||
onClear,
|
onAddTrack,
|
||||||
selection,
|
onImportTracks,
|
||||||
historyState,
|
onUpdateTrack,
|
||||||
|
onRemoveTrack,
|
||||||
|
onClearTracks,
|
||||||
effectChain,
|
effectChain,
|
||||||
effectPresets,
|
effectPresets,
|
||||||
onToggleEffect,
|
onToggleEffect,
|
||||||
@@ -98,47 +63,13 @@ export function SidePanel({
|
|||||||
onLoadPreset,
|
onLoadPreset,
|
||||||
onDeletePreset,
|
onDeletePreset,
|
||||||
onClearChain,
|
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,
|
className,
|
||||||
}: SidePanelProps) {
|
}: SidePanelProps) {
|
||||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
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 [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
|
||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleFileClick = () => {
|
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
onFileSelect(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
return (
|
return (
|
||||||
@@ -161,17 +92,17 @@ export function SidePanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-2 border-b border-border">
|
<div className="flex items-center justify-between p-2 border-b border-border">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'file' ? 'secondary' : 'ghost'}
|
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => setActiveTab('file')}
|
onClick={() => setActiveTab('tracks')}
|
||||||
title="File"
|
title="Tracks"
|
||||||
>
|
>
|
||||||
<FileAudio className="h-4 w-4" />
|
<Music2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'chain' ? 'secondary' : 'ghost'}
|
variant={activeTab === 'chain' ? 'secondary' : 'ghost'}
|
||||||
@@ -181,38 +112,6 @@ export function SidePanel({
|
|||||||
>
|
>
|
||||||
<Link2 className="h-4 w-4" />
|
<Link2 className="h-4 w-4" />
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -226,62 +125,172 @@ export function SidePanel({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
|
<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">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
Audio File
|
Multi-Track Editor
|
||||||
</h3>
|
</h3>
|
||||||
{audioBuffer ? (
|
<div className="flex gap-2">
|
||||||
<div className="space-y-2">
|
<Button
|
||||||
<div className="p-2 bg-secondary/30 rounded text-xs">
|
variant="outline"
|
||||||
<div className="font-medium text-foreground truncate" title={fileName || 'Unknown'}>
|
size="sm"
|
||||||
{fileName || 'Unknown'}
|
onClick={onAddTrack}
|
||||||
</div>
|
className="flex-1"
|
||||||
<div className="text-muted-foreground mt-1">
|
>
|
||||||
Duration: {formatDuration(audioBuffer.duration)}
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||||
</div>
|
Add Track
|
||||||
<div className="text-muted-foreground">
|
</Button>
|
||||||
Channels: {audioBuffer.numberOfChannels}
|
<Button
|
||||||
</div>
|
variant="outline"
|
||||||
<div className="text-muted-foreground">
|
size="sm"
|
||||||
Sample Rate: {audioBuffer.sampleRate} Hz
|
onClick={onImportTracks}
|
||||||
</div>
|
className="flex-1"
|
||||||
</div>
|
>
|
||||||
<Button
|
<Upload className="h-3.5 w-3.5 mr-1.5" />
|
||||||
variant="outline"
|
Import
|
||||||
size="sm"
|
</Button>
|
||||||
onClick={onClear}
|
</div>
|
||||||
className="w-full"
|
{tracks.length > 0 && (
|
||||||
>
|
<Button
|
||||||
<X className="h-3.5 w-3.5 mr-1.5" />
|
variant="outline"
|
||||||
Clear File
|
size="sm"
|
||||||
</Button>
|
onClick={onClearTracks}
|
||||||
</div>
|
className="w-full"
|
||||||
) : (
|
>
|
||||||
<div className="space-y-2">
|
<Trash2 className="h-3.5 w-3.5 mr-1.5 text-destructive" />
|
||||||
<input
|
Clear All Tracks
|
||||||
ref={fileInputRef}
|
</Button>
|
||||||
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>
|
</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">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
Effect Chain
|
Effect Chain
|
||||||
|
{selectedTrack && (
|
||||||
|
<span className="text-primary ml-2">({selectedTrack.name})</span>
|
||||||
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -312,380 +324,37 @@ export function SidePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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' && (
|
{!selectedTrack ? (
|
||||||
<div className="space-y-2">
|
<div className="text-center py-8">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<Link2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-2" />
|
||||||
Edit History
|
<p className="text-sm text-muted-foreground">
|
||||||
</h3>
|
Select a track to apply effects
|
||||||
{historyState.historySize > 0 ? (
|
</p>
|
||||||
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-muted-foreground">
|
<>
|
||||||
No history available. Edit operations will appear here.
|
<EffectRack
|
||||||
</div>
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface TrackProps {
|
|||||||
zoom: number;
|
zoom: number;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelect?: () => void;
|
||||||
onToggleMute: () => void;
|
onToggleMute: () => void;
|
||||||
onToggleSolo: () => void;
|
onToggleSolo: () => void;
|
||||||
onToggleCollapse: () => void;
|
onToggleCollapse: () => void;
|
||||||
@@ -25,6 +27,8 @@ export function Track({
|
|||||||
zoom,
|
zoom,
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
onToggleMute,
|
onToggleMute,
|
||||||
onToggleSolo,
|
onToggleSolo,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
@@ -121,7 +125,13 @@ export function Track({
|
|||||||
|
|
||||||
if (track.collapsed) {
|
if (track.collapsed) {
|
||||||
return (
|
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
|
<TrackHeader
|
||||||
track={track}
|
track={track}
|
||||||
onToggleMute={onToggleMute}
|
onToggleMute={onToggleMute}
|
||||||
@@ -140,9 +150,10 @@ export function Track({
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b border-border',
|
'border-b border-border cursor-pointer',
|
||||||
track.selected && 'ring-2 ring-primary ring-inset'
|
isSelected && 'ring-2 ring-primary ring-inset'
|
||||||
)}
|
)}
|
||||||
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<TrackHeader
|
<TrackHeader
|
||||||
track={track}
|
track={track}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface TrackListProps {
|
|||||||
zoom: number;
|
zoom: number;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
selectedTrackId?: string | null;
|
||||||
|
onSelectTrack?: (trackId: string | null) => void;
|
||||||
onAddTrack: () => void;
|
onAddTrack: () => void;
|
||||||
onImportTrack?: (buffer: AudioBuffer, name: string) => void;
|
onImportTrack?: (buffer: AudioBuffer, name: string) => void;
|
||||||
onRemoveTrack: (trackId: string) => void;
|
onRemoveTrack: (trackId: string) => void;
|
||||||
@@ -24,6 +26,8 @@ export function TrackList({
|
|||||||
zoom,
|
zoom,
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
|
selectedTrackId,
|
||||||
|
onSelectTrack,
|
||||||
onAddTrack,
|
onAddTrack,
|
||||||
onImportTrack,
|
onImportTrack,
|
||||||
onRemoveTrack,
|
onRemoveTrack,
|
||||||
@@ -78,6 +82,8 @@ export function TrackList({
|
|||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
duration={duration}
|
duration={duration}
|
||||||
|
isSelected={selectedTrackId === track.id}
|
||||||
|
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
|
||||||
onToggleMute={() =>
|
onToggleMute={() =>
|
||||||
onUpdateTrack(track.id, { mute: !track.mute })
|
onUpdateTrack(track.id, { mute: !track.mute })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user