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