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:
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user