From 83127b311617cb86af53a5d2bcfe6e35ef9c9b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 21:28:38 +0100 Subject: [PATCH] feat: add multi-track audio import functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive multi-file audio import system: - Created ImportTrackDialog component with drag-and-drop and file browser support - Updated TrackList to integrate import functionality with dedicated buttons - Added multi-track-demo page to test and demonstrate import features - Sequential file processing with automatic track naming from filenames - Error handling for non-audio files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/multi-track-demo/page.tsx | 127 +++++++++++++++++++++ components/tracks/ImportTrackDialog.tsx | 145 ++++++++++++++++++++++++ components/tracks/TrackList.tsx | 70 ++++++++++-- 3 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 app/multi-track-demo/page.tsx create mode 100644 components/tracks/ImportTrackDialog.tsx diff --git a/app/multi-track-demo/page.tsx b/app/multi-track-demo/page.tsx new file mode 100644 index 0000000..b1cce01 --- /dev/null +++ b/app/multi-track-demo/page.tsx @@ -0,0 +1,127 @@ +'use client'; + +import * as React from 'react'; +import { TrackList } from '@/components/tracks/TrackList'; +import { useMultiTrack } from '@/lib/hooks/useMultiTrack'; +import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer'; +import { Button } from '@/components/ui/Button'; +import { Play, Pause, Square, SkipBack } from 'lucide-react'; +import { formatDuration } from '@/lib/audio/decoder'; + +export default function MultiTrackDemoPage() { + const { + tracks, + addTrack, + addTrackFromBuffer, + removeTrack, + updateTrack, + clearTracks, + } = useMultiTrack(); + + const { + isPlaying, + currentTime, + duration, + play, + pause, + stop, + seek, + togglePlayPause, + } = useMultiTrackPlayer(tracks); + + const [zoom, setZoom] = React.useState(1); + + return ( +
+ {/* Header */} +
+

Multi-Track Demo

+
+ + + +
+
+ + {/* Track List */} +
+ addTrack()} + onImportTrack={(buffer, name) => addTrackFromBuffer(buffer, name)} + onRemoveTrack={removeTrack} + onUpdateTrack={updateTrack} + onSeek={seek} + /> +
+ + {/* Playback Controls */} +
+
+ + + +
+ +
+ + {formatDuration(currentTime)} / {formatDuration(duration)} + + + {tracks.length} {tracks.length === 1 ? 'track' : 'tracks'} + +
+
+
+ ); +} diff --git a/components/tracks/ImportTrackDialog.tsx b/components/tracks/ImportTrackDialog.tsx new file mode 100644 index 0000000..a0fdbb3 --- /dev/null +++ b/components/tracks/ImportTrackDialog.tsx @@ -0,0 +1,145 @@ +'use client'; + +import * as React from 'react'; +import { Upload, Plus } from 'lucide-react'; +import { Modal } from '@/components/ui/Modal'; +import { Button } from '@/components/ui/Button'; +import { decodeAudioFile } from '@/lib/audio/decoder'; + +export interface ImportTrackDialogProps { + open: boolean; + onClose: () => void; + onImportTrack: (buffer: AudioBuffer, name: string) => void; +} + +export function ImportTrackDialog({ + open, + onClose, + onImportTrack, +}: ImportTrackDialogProps) { + const [isDragging, setIsDragging] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + const fileInputRef = React.useRef(null); + + const handleFiles = async (files: FileList) => { + setIsLoading(true); + + try { + // Process files sequentially + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + if (!file.type.startsWith('audio/')) { + console.warn(`Skipping non-audio file: ${file.name}`); + continue; + } + + try { + const buffer = await decodeAudioFile(file); + const trackName = file.name.replace(/\.[^/.]+$/, ''); // Remove extension + onImportTrack(buffer, trackName); + } catch (error) { + console.error(`Failed to import ${file.name}:`, error); + } + } + + onClose(); + } finally { + setIsLoading(false); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (files && files.length > 0) { + handleFiles(files); + } + // Reset input + e.target.value = ''; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + handleFiles(files); + } + }; + + return ( + +
+ {/* Drag and Drop Area */} +
+ +

+ {isLoading ? 'Importing files...' : 'Drag and drop audio files here'} +

+

+ or +

+ + +
+ + {/* Supported Formats */} +
+

Supported formats:

+

MP3, WAV, OGG, FLAC, M4A, AAC, and more

+

+ 💡 Tip: Select multiple files at once or drag multiple files to import them all as separate tracks +

+
+ + {/* Actions */} +
+ +
+
+
+ ); +} diff --git a/components/tracks/TrackList.tsx b/components/tracks/TrackList.tsx index ceea51d..bfcdaa9 100644 --- a/components/tracks/TrackList.tsx +++ b/components/tracks/TrackList.tsx @@ -1,9 +1,10 @@ 'use client'; import * as React from 'react'; -import { Plus } from 'lucide-react'; +import { Plus, Upload } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Track } from './Track'; +import { ImportTrackDialog } from './ImportTrackDialog'; import type { Track as TrackType } from '@/types/track'; export interface TrackListProps { @@ -12,6 +13,7 @@ export interface TrackListProps { currentTime: number; duration: number; onAddTrack: () => void; + onImportTrack?: (buffer: AudioBuffer, name: string) => void; onRemoveTrack: (trackId: string) => void; onUpdateTrack: (trackId: string, updates: Partial) => void; onSeek?: (time: number) => void; @@ -23,19 +25,45 @@ export function TrackList({ currentTime, duration, onAddTrack, + onImportTrack, onRemoveTrack, onUpdateTrack, onSeek, }: TrackListProps) { + const [importDialogOpen, setImportDialogOpen] = React.useState(false); + + const handleImportTrack = (buffer: AudioBuffer, name: string) => { + if (onImportTrack) { + onImportTrack(buffer, name); + } + }; + if (tracks.length === 0) { return ( -
-

No tracks yet. Add a track to get started.

- -
+ <> +
+

No tracks yet. Add a track to get started.

+
+ + {onImportTrack && ( + + )} +
+
+ {onImportTrack && ( + setImportDialogOpen(false)} + onImportTrack={handleImportTrack} + /> + )} + ); } @@ -74,13 +102,33 @@ export function TrackList({ ))} - {/* Add Track Button */} -
+ {/* Add Track Buttons */} +
+ {onImportTrack && ( + + )}
+ + {/* Import Dialog */} + {onImportTrack && ( + setImportDialogOpen(false)} + onImportTrack={handleImportTrack} + /> + )}
); }