feat: Initialize k-convert application with core structure, UI components, and conversion logic.
This commit is contained in:
207
src/workers/audio.worker.ts
Normal file
207
src/workers/audio.worker.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
// =============================================================================
|
||||
// Audio Worker
|
||||
// Handles audio conversions using FFmpeg WASM
|
||||
// =============================================================================
|
||||
|
||||
import * as Comlink from 'comlink';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { toBlobURL } from '@ffmpeg/util';
|
||||
|
||||
type AudioFormat = 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac';
|
||||
|
||||
let ffmpeg: FFmpeg | null = null;
|
||||
let ffmpegLoading: Promise<void> | null = null;
|
||||
|
||||
async function ensureFFmpegLoaded(): Promise<FFmpeg> {
|
||||
if (ffmpeg?.loaded) return ffmpeg;
|
||||
|
||||
if (ffmpegLoading) {
|
||||
await ffmpegLoading;
|
||||
return ffmpeg!;
|
||||
}
|
||||
|
||||
ffmpegLoading = (async () => {
|
||||
ffmpeg = new FFmpeg();
|
||||
|
||||
// Load FFmpeg core from CDN with proper CORS
|
||||
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
||||
|
||||
await ffmpeg.load({
|
||||
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
||||
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
||||
});
|
||||
})();
|
||||
|
||||
await ffmpegLoading;
|
||||
return ffmpeg!;
|
||||
}
|
||||
|
||||
function getFFmpegArgs(
|
||||
_inputFormat: AudioFormat,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number
|
||||
): string[] {
|
||||
const args: string[] = [];
|
||||
|
||||
// Codec selection based on output format
|
||||
switch (outputFormat) {
|
||||
case 'mp3':
|
||||
args.push('-c:a', 'libmp3lame');
|
||||
args.push('-b:a', `${bitrate || 192}k`);
|
||||
break;
|
||||
case 'wav':
|
||||
args.push('-c:a', 'pcm_s16le');
|
||||
break;
|
||||
case 'ogg':
|
||||
args.push('-c:a', 'libvorbis');
|
||||
args.push('-q:a', '4'); // Quality level 0-10
|
||||
break;
|
||||
case 'm4a':
|
||||
args.push('-c:a', 'aac');
|
||||
args.push('-b:a', `${bitrate || 192}k`);
|
||||
break;
|
||||
case 'flac':
|
||||
args.push('-c:a', 'flac');
|
||||
args.push('-compression_level', '5');
|
||||
break;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function getExtension(format: AudioFormat): string {
|
||||
return format;
|
||||
}
|
||||
|
||||
function getMimeType(format: AudioFormat): string {
|
||||
const mimeMap: Record<AudioFormat, string> = {
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
ogg: 'audio/ogg',
|
||||
m4a: 'audio/mp4',
|
||||
flac: 'audio/flac',
|
||||
};
|
||||
return mimeMap[format];
|
||||
}
|
||||
|
||||
const audioConverter = {
|
||||
/**
|
||||
* Convert audio file using FFmpeg
|
||||
*/
|
||||
async convert(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
inputFormat: AudioFormat,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> {
|
||||
const ff = await ensureFFmpegLoaded();
|
||||
|
||||
const inputExt = getExtension(inputFormat);
|
||||
const outputExt = getExtension(outputFormat);
|
||||
const inputFileName = `input.${inputExt}`;
|
||||
const outputFileName = `output.${outputExt}`;
|
||||
|
||||
// Register progress callback if provided
|
||||
if (onProgress) {
|
||||
ff.on('progress', ({ progress }) => {
|
||||
onProgress(Math.round(progress * 100));
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Write input file to FFmpeg's virtual filesystem
|
||||
await ff.writeFile(inputFileName, new Uint8Array(arrayBuffer));
|
||||
|
||||
// Build FFmpeg command
|
||||
const codecArgs = getFFmpegArgs(inputFormat, outputFormat, bitrate);
|
||||
|
||||
await ff.exec([
|
||||
'-i', inputFileName,
|
||||
...codecArgs,
|
||||
'-y', // Overwrite output
|
||||
outputFileName,
|
||||
]);
|
||||
|
||||
// Read output file
|
||||
const data = await ff.readFile(outputFileName);
|
||||
|
||||
// Cleanup
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
|
||||
// Convert to regular ArrayBuffer to avoid SharedArrayBuffer issues
|
||||
const buffer = new ArrayBuffer((data as Uint8Array).length);
|
||||
new Uint8Array(buffer).set(data as Uint8Array);
|
||||
return new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
} catch (error) {
|
||||
// Attempt cleanup on error
|
||||
try {
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract audio from video file (useful for iPhone MOV files)
|
||||
*/
|
||||
async extractAudio(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
inputExtension: string,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> {
|
||||
const ff = await ensureFFmpegLoaded();
|
||||
|
||||
const inputFileName = `input.${inputExtension}`;
|
||||
const outputExt = getExtension(outputFormat);
|
||||
const outputFileName = `output.${outputExt}`;
|
||||
|
||||
if (onProgress) {
|
||||
ff.on('progress', ({ progress }) => {
|
||||
onProgress(Math.round(progress * 100));
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await ff.writeFile(inputFileName, new Uint8Array(arrayBuffer));
|
||||
|
||||
const codecArgs = getFFmpegArgs('m4a', outputFormat, bitrate);
|
||||
|
||||
await ff.exec([
|
||||
'-i', inputFileName,
|
||||
'-vn', // No video
|
||||
...codecArgs,
|
||||
'-y',
|
||||
outputFileName,
|
||||
]);
|
||||
|
||||
const data = await ff.readFile(outputFileName);
|
||||
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
|
||||
// Convert to regular ArrayBuffer
|
||||
const buffer = new ArrayBuffer((data as Uint8Array).length);
|
||||
new Uint8Array(buffer).set(data as Uint8Array);
|
||||
return new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
} catch (error) {
|
||||
try {
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Comlink.expose(audioConverter);
|
||||
|
||||
export type AudioConverterWorker = typeof audioConverter;
|
||||
Reference in New Issue
Block a user