diff --git a/components/dialogs/ExportDialog.tsx b/components/dialogs/ExportDialog.tsx index 795e539..8a232d3 100644 --- a/components/dialogs/ExportDialog.tsx +++ b/components/dialogs/ExportDialog.tsx @@ -7,6 +7,7 @@ import { cn } from '@/lib/utils/cn'; export interface ExportSettings { format: 'wav' | 'mp3' | 'flac'; + scope: 'project' | 'selection' | 'tracks'; // Export scope bitDepth: 16 | 24 | 32; bitrate: number; // For MP3: 128, 192, 256, 320 kbps quality: number; // For FLAC: 0-9 @@ -19,11 +20,13 @@ export interface ExportDialogProps { onClose: () => void; onExport: (settings: ExportSettings) => void; isExporting?: boolean; + hasSelection?: boolean; // Whether any track has a selection } -export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDialogProps) { +export function ExportDialog({ open, onClose, onExport, isExporting, hasSelection }: ExportDialogProps) { const [settings, setSettings] = React.useState({ format: 'wav', + scope: 'project', bitDepth: 16, bitrate: 192, // Default MP3 bitrate quality: 6, // Default FLAC quality @@ -88,6 +91,30 @@ export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDia + {/* Export Scope */} +
+ + +

+ {settings.scope === 'project' && 'Mix all tracks into a single file'} + {settings.scope === 'selection' && 'Export only the selected region'} + {settings.scope === 'tracks' && 'Export each track as a separate file'} +

+
+ {/* Bit Depth (WAV and FLAC only) */} {(settings.format === 'wav' || settings.format === 'flac') && (
diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index 8aa6a99..1bb2a29 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -730,57 +730,113 @@ export function AudioEditor() { setIsExporting(true); try { - // Get max duration and sample rate - const maxDuration = getMaxTrackDuration(tracks); const sampleRate = tracks[0]?.audioBuffer?.sampleRate || 44100; - // Mix all tracks into a single buffer - const mixedBuffer = mixTracks(tracks, sampleRate, maxDuration); + // Helper function to convert and download a buffer + const convertAndDownload = async (buffer: AudioBuffer, filename: string) => { + let exportedBuffer: ArrayBuffer; + let mimeType: string; + let fileExtension: string; - // Convert based on format - let exportedBuffer: ArrayBuffer; - let mimeType: string; - let fileExtension: string; + if (settings.format === 'mp3') { + exportedBuffer = await audioBufferToMp3(buffer, { + format: 'mp3', + bitrate: settings.bitrate, + normalize: settings.normalize, + }); + mimeType = 'audio/mpeg'; + fileExtension = 'mp3'; + } else if (settings.format === 'flac') { + exportedBuffer = await audioBufferToFlac(buffer, { + format: 'flac', + bitDepth: settings.bitDepth, + quality: settings.quality, + normalize: settings.normalize, + }); + mimeType = 'application/octet-stream'; + fileExtension = 'flac'; + } else { + exportedBuffer = audioBufferToWav(buffer, { + format: 'wav', + bitDepth: settings.bitDepth, + normalize: settings.normalize, + }); + mimeType = 'audio/wav'; + fileExtension = 'wav'; + } - if (settings.format === 'mp3') { - exportedBuffer = await audioBufferToMp3(mixedBuffer, { - format: 'mp3', - bitrate: settings.bitrate, - normalize: settings.normalize, + const fullFilename = `${filename}.${fileExtension}`; + downloadArrayBuffer(exportedBuffer, fullFilename, mimeType); + return fullFilename; + }; + + if (settings.scope === 'tracks') { + // Export each track individually + let exportedCount = 0; + for (const track of tracks) { + if (!track.audioBuffer) continue; + + const trackFilename = `${settings.filename}_${track.name.replace(/[^a-z0-9]/gi, '_')}`; + await convertAndDownload(track.audioBuffer, trackFilename); + exportedCount++; + } + + addToast({ + title: 'Export Complete', + description: `Exported ${exportedCount} track${exportedCount !== 1 ? 's' : ''}`, + variant: 'success', + duration: 3000, }); - mimeType = 'audio/mpeg'; - fileExtension = 'mp3'; - } else if (settings.format === 'flac') { - exportedBuffer = await audioBufferToFlac(mixedBuffer, { - format: 'flac', - bitDepth: settings.bitDepth, - quality: settings.quality, - normalize: settings.normalize, + } else if (settings.scope === 'selection') { + // Export selected region + const selectedTrack = tracks.find(t => t.selection); + if (!selectedTrack || !selectedTrack.selection) { + addToast({ + title: 'No Selection', + description: 'No region selected for export', + variant: 'warning', + duration: 3000, + }); + setIsExporting(false); + return; + } + + // Extract selection from all tracks and mix + const selectionStart = selectedTrack.selection.start; + const selectionEnd = selectedTrack.selection.end; + const selectionDuration = selectionEnd - selectionStart; + + // Create tracks with only the selected region + const selectedTracks = tracks.map(track => ({ + ...track, + audioBuffer: track.audioBuffer + ? extractBufferSegment(track.audioBuffer, selectionStart, selectionEnd) + : null, + })); + + const mixedBuffer = mixTracks(selectedTracks, sampleRate, selectionDuration); + const filename = await convertAndDownload(mixedBuffer, settings.filename); + + addToast({ + title: 'Export Complete', + description: `Exported ${filename}`, + variant: 'success', + duration: 3000, }); - mimeType = 'application/octet-stream'; // FLAC MIME type - fileExtension = 'flac'; } else { - // WAV (default) - exportedBuffer = audioBufferToWav(mixedBuffer, { - format: 'wav', - bitDepth: settings.bitDepth, - normalize: settings.normalize, + // Export entire project (mix all tracks) + const maxDuration = getMaxTrackDuration(tracks); + const mixedBuffer = mixTracks(tracks, sampleRate, maxDuration); + const filename = await convertAndDownload(mixedBuffer, settings.filename); + + addToast({ + title: 'Export Complete', + description: `Exported ${filename}`, + variant: 'success', + duration: 3000, }); - mimeType = 'audio/wav'; - fileExtension = 'wav'; } - // Download - const filename = `${settings.filename}.${fileExtension}`; - downloadArrayBuffer(exportedBuffer, filename, mimeType); - - addToast({ - title: 'Export Complete', - description: `Exported ${filename}`, - variant: 'success', - duration: 3000, - }); - setExportDialogOpen(false); } catch (error) { console.error('Export failed:', error); @@ -1219,6 +1275,7 @@ export function AudioEditor() { onClose={() => setExportDialogOpen(false)} onExport={handleExport} isExporting={isExporting} + hasSelection={tracks.some(t => t.selection !== null)} /> );