feat: add export scope options (project/selection/tracks)

Implemented Phase 11.3 - Export Regions:
- Added export scope selector in ExportDialog
  - Entire Project: Mix all tracks into single file
  - Selected Region: Export only the selected region (disabled if no selection)
  - Individual Tracks: Export each track as separate file
- Updated ExportSettings interface with scope property
- Refactored handleExport to support all three export modes:
  - Project mode: Mix all tracks (existing behavior)
  - Selection mode: Extract selection from all tracks and mix
  - Tracks mode: Loop through tracks and export separately with sanitized filenames
- Added hasSelection prop to ExportDialog to enable/disable selection option
- Created helper function convertAndDownload to reduce code duplication
- Selection mode uses extractBufferSegment for precise region extraction
- Track names are sanitized for filenames (remove special characters)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 07:47:56 +01:00
parent c6ff313050
commit 38a2b2962d
2 changed files with 126 additions and 42 deletions

View File

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