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:
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user