Implements MP3 320kbps download functionality using ffmpeg.wasm for industry-standard encoding with libmp3lame. Features: - New MP3_320 quality option in download settings UI - ID3v2.3 metadata writing (title, artist, album, cover art, ISRC, etc.) - Non-blocking encoding via Web Worker to keep UI responsive - Proper UTF-16 with BOM text encoding for international characters - Album artist fallback to track artist (mirrors FLAC/M4A behavior) - Automatic format detection for downloaded audio - Year validation to prevent writing NaN to ID3 tags Implementation: - mp3-encoder.js: Main encoder module with worker orchestration - mp3-encoder.worker.js: FFmpeg Web Worker for async encoding - id3-writer.js: ID3v2.3 tag writer with synchsafe size encoding - Updates to api.js, metadata.js, utils.js for MP3 support - Vite config excludes @ffmpeg packages from dep optimization Technical details: - Uses @ffmpeg/ffmpeg (libmp3lame 320kbps CBR, 44.1kHz) - FFmpeg binary lazy-loaded from CDN (~25MB, cached) - Encoding runs in separate thread (non-blocking UI) - Proper error handling with distinct encoding vs network errors - Memory-efficient: transfers ArrayBuffer with zero-copy Dependencies: - @ffmpeg/ffmpeg ^0.12.10 - @ffmpeg/util ^0.12.1 - Removed: package-lock.json (project uses bun.lock) Closes maintainer request to use ffmpeg.wasm instead of lamejs.
47 lines
1.6 KiB
JavaScript
47 lines
1.6 KiB
JavaScript
async function encodeToMp3Worker(audioBlob, onProgress = null) {
|
|
const audioData = await audioBlob.arrayBuffer();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const worker = new Worker(new URL('./mp3-encoder.worker.js', import.meta.url), { type: 'module' });
|
|
|
|
worker.onmessage = (e) => {
|
|
const { type, blob, message, stage, progress } = e.data;
|
|
|
|
if (type === 'complete') {
|
|
worker.terminate();
|
|
resolve(blob);
|
|
} else if (type === 'error') {
|
|
worker.terminate();
|
|
reject(new Error(message));
|
|
} else if (type === 'progress' && onProgress) {
|
|
onProgress({ stage, message, progress });
|
|
} else if (type === 'log') {
|
|
console.log('[FFmpeg]', message);
|
|
}
|
|
};
|
|
|
|
worker.onerror = (error) => {
|
|
worker.terminate();
|
|
reject(new Error('Worker failed: ' + error.message));
|
|
};
|
|
|
|
// Transfer audio data to worker
|
|
worker.postMessage({
|
|
audioData
|
|
}, [audioData]);
|
|
});
|
|
}
|
|
|
|
export async function encodeToMp3(audioBlob, onProgress = null) {
|
|
try {
|
|
// Use Web Worker for non-blocking FFmpeg encoding
|
|
if (typeof Worker !== 'undefined') {
|
|
return await encodeToMp3Worker(audioBlob, onProgress);
|
|
}
|
|
|
|
throw new Error('Web Workers are required for MP3 encoding');
|
|
} catch (error) {
|
|
console.error('MP3 encoding failed:', error);
|
|
throw new Error('Failed to encode MP3: ' + error.message);
|
|
}
|
|
}
|