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>
This commit is contained in:
2025-11-17 21:57:31 +01:00
parent 83127b3116
commit 832a18dd9c
2 changed files with 207 additions and 1 deletions

View File

@@ -11,6 +11,8 @@ import type { CommandAction } from '@/components/ui/CommandPalette';
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
import { useHistory } from '@/lib/hooks/useHistory';
import { useEffectChain } from '@/lib/hooks/useEffectChain';
import { useMultiTrack } from '@/lib/hooks/useMultiTrack';
import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer';
import { useToast } from '@/components/ui/Toast';
import { Slider } from '@/components/ui/Slider';
import { cn } from '@/lib/utils/cn';
@@ -52,6 +54,8 @@ import { EffectParameterDialog, type FilterParameters } from '@/components/effec
import { DynamicsParameterDialog, type DynamicsParameters, type DynamicsType } from '@/components/effects/DynamicsParameterDialog';
import { TimeBasedParameterDialog, type TimeBasedParameters, type TimeBasedType } from '@/components/effects/TimeBasedParameterDialog';
import { AdvancedParameterDialog, type AdvancedParameters, type AdvancedType } from '@/components/effects/AdvancedParameterDialog';
import { TrackList } from '@/components/tracks/TrackList';
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
const EFFECT_LABELS: Record<string, string> = {
lowpass: 'Low-Pass Filter',
@@ -76,6 +80,10 @@ const EFFECT_LABELS: Record<string, string> = {
};
export function AudioEditor() {
// View mode state
const [viewMode, setViewMode] = React.useState<'waveform' | 'tracks'>('waveform');
const [importDialogOpen, setImportDialogOpen] = React.useState(false);
// Zoom and scroll state
const [zoom, setZoom] = React.useState(1);
const [scrollOffset, setScrollOffset] = React.useState(0);
@@ -141,6 +149,27 @@ export function AudioEditor() {
} = useEffectChain();
const { addToast } = useToast();
// Multi-track hooks
const {
tracks,
addTrack,
addTrackFromBuffer,
removeTrack,
updateTrack,
clearTracks,
} = useMultiTrack();
const {
isPlaying: isMultiTrackPlaying,
currentTime: multiTrackCurrentTime,
duration: multiTrackDuration,
play: playMultiTrack,
pause: pauseMultiTrack,
stop: stopMultiTrack,
seek: seekMultiTrack,
togglePlayPause: toggleMultiTrackPlayPause,
} = useMultiTrackPlayer(tracks);
const handleFileSelect = async (file: File) => {
try {
await loadFile(file);
@@ -176,6 +205,41 @@ export function AudioEditor() {
});
};
// Multi-track handlers
const handleConvertToTrack = () => {
if (!audioBuffer) return;
const trackName = fileName || 'Audio Track';
addTrackFromBuffer(audioBuffer, trackName);
setViewMode('tracks');
addToast({
title: 'Converted to Track',
description: `"${trackName}" added to tracks`,
variant: 'success',
duration: 2000,
});
};
const handleImportTracks = () => {
setImportDialogOpen(true);
};
const handleImportTrack = (buffer: AudioBuffer, name: string) => {
addTrackFromBuffer(buffer, name);
setViewMode('tracks');
};
const handleClearTracks = () => {
clearTracks();
setViewMode('waveform');
addToast({
title: 'Tracks Cleared',
description: 'All tracks have been removed',
variant: 'info',
duration: 2000,
});
};
// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
@@ -1321,6 +1385,11 @@ export function AudioEditor() {
onTimeStretch={handleTimeStretch}
onDistortion={handleDistortion}
onBitcrusher={handleBitcrusher}
tracks={tracks}
onAddTrack={addTrack}
onImportTracks={handleImportTracks}
onConvertToTrack={handleConvertToTrack}
onClearTracks={handleClearTracks}
/>
{/* Main canvas area */}
@@ -1332,6 +1401,41 @@ export function AudioEditor() {
<p className="text-sm text-muted-foreground">Loading audio file...</p>
</div>
</div>
) : viewMode === 'tracks' ? (
<>
{/* Multi-Track View */}
<div className="flex-1 flex flex-col overflow-hidden">
<TrackList
tracks={tracks}
zoom={zoom}
currentTime={multiTrackCurrentTime}
duration={multiTrackDuration}
onAddTrack={addTrack}
onImportTrack={handleImportTrack}
onRemoveTrack={removeTrack}
onUpdateTrack={updateTrack}
onSeek={seekMultiTrack}
/>
</div>
{/* Multi-Track Playback Controls */}
<div className="border-t border-border bg-card p-3">
<PlaybackControls
isPlaying={isMultiTrackPlaying}
isPaused={!isMultiTrackPlaying}
currentTime={multiTrackCurrentTime}
duration={multiTrackDuration}
volume={1}
onPlay={playMultiTrack}
onPause={pauseMultiTrack}
onStop={stopMultiTrack}
onSeek={seekMultiTrack}
onVolumeChange={() => {}}
currentTimeFormatted={`${Math.floor(multiTrackCurrentTime / 60)}:${String(Math.floor(multiTrackCurrentTime % 60)).padStart(2, '0')}`}
durationFormatted={`${Math.floor(multiTrackDuration / 60)}:${String(Math.floor(multiTrackDuration % 60)).padStart(2, '0')}`}
/>
</div>
</>
) : audioBuffer ? (
<>
{/* Waveform - takes maximum space */}
@@ -1450,6 +1554,13 @@ export function AudioEditor() {
effectType={advancedDialogType}
onApply={handleAdvancedApply}
/>
{/* Import Track Dialog */}
<ImportTrackDialog
open={importDialogOpen}
onClose={() => setImportDialogOpen(false)}
onImportTrack={handleImportTrack}
/>
</>
);
}

View File

@@ -14,6 +14,8 @@ import {
Link2,
FolderOpen,
Trash2,
Layers,
Plus,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
@@ -23,6 +25,7 @@ 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
@@ -69,6 +72,13 @@ export interface SidePanelProps {
onDistortion: () => void;
onBitcrusher: () => void;
// Multi-track
tracks?: Track[];
onAddTrack?: () => void;
onImportTracks?: () => void;
onConvertToTrack?: () => void;
onClearTracks?: () => void;
className?: string;
}
@@ -107,10 +117,15 @@ export function SidePanel({
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'>('file');
const [activeTab, setActiveTab] = React.useState<'file' | 'chain' | 'history' | 'info' | 'effects' | 'tracks'>('file');
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
const fileInputRef = React.useRef<HTMLInputElement>(null);
@@ -174,6 +189,14 @@ export function SidePanel({
>
<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"
@@ -591,6 +614,78 @@ export function SidePanel({
</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>
);