From 832a18dd9c1e0301a67a80c58c64f448651faf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 21:57:31 +0100 Subject: [PATCH] feat: integrate multi-track functionality into main AudioEditor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/editor/AudioEditor.tsx | 111 ++++++++++++++++++++++++++++++ components/layout/SidePanel.tsx | 97 +++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index d6caff5..106f6a4 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -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 = { lowpass: 'Low-Pass Filter', @@ -76,6 +80,10 @@ const EFFECT_LABELS: Record = { }; 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() {

Loading audio file...

+ ) : viewMode === 'tracks' ? ( + <> + {/* Multi-Track View */} +
+ +
+ + {/* Multi-Track Playback Controls */} +
+ {}} + 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')}`} + /> +
+ ) : audioBuffer ? ( <> {/* Waveform - takes maximum space */} @@ -1450,6 +1554,13 @@ export function AudioEditor() { effectType={advancedDialogType} onApply={handleAdvancedApply} /> + + {/* Import Track Dialog */} + setImportDialogOpen(false)} + onImportTrack={handleImportTrack} + /> ); } diff --git a/components/layout/SidePanel.tsx b/components/layout/SidePanel.tsx index 12b90b8..05bf75d 100644 --- a/components/layout/SidePanel.tsx +++ b/components/layout/SidePanel.tsx @@ -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(null); @@ -174,6 +189,14 @@ export function SidePanel({ > + + )} + + ) : ( +
+
+ Add tracks to work with multiple audio files simultaneously. +
+ {audioBuffer && onConvertToTrack && ( + + )} + {onAddTrack && ( + + )} + {onImportTracks && ( + + )} +
+ )} + + + )} );