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}
/>
</>
);
}