/*! * Copyright (c) 2026-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import createModule from '../build/flac'; import type { PacketInfo, WorkerCommand, WorkerResponse, WorkerResponseData } from './shared'; type ExtendedEmscriptenModule = EmscriptenModule & { cwrap: typeof cwrap; }; let module: ExtendedEmscriptenModule; let modulePromise: Promise | null = null; let initEncoderFn: (channels: number, sampleRate: number, bitsPerSample: number) => number; let getEncodeInputPtr: (ctx: number, size: number) => number; let sendSamplesFn: (ctx: number, numSamples: number) => number; let getOutputData: (ctx: number) => number; let getFrameCount: (ctx: number) => number; let getFrameSize: (ctx: number, index: number) => number; let getFrameSamples: (ctx: number, index: number) => number; let getHeaderData: (ctx: number) => number; let getHeaderSize: (ctx: number) => number; let finishEncoderFn: (ctx: number) => number; const ensureModule = async () => { if (!module) { if (modulePromise) { return modulePromise; } modulePromise = createModule() as Promise; module = await modulePromise; modulePromise = null; initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']); getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']); sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']); getOutputData = module.cwrap('get_output_data', 'number', ['number']); getFrameCount = module.cwrap('get_frame_count', 'number', ['number']); getFrameSize = module.cwrap('get_frame_size', 'number', ['number', 'number']); getFrameSamples = module.cwrap('get_frame_samples', 'number', ['number', 'number']); getHeaderData = module.cwrap('get_header_data', 'number', ['number']); getHeaderSize = module.cwrap('get_header_size', 'number', ['number']); finishEncoderFn = module.cwrap('finish_encoder', 'number', ['number']); } }; const initEncoder = async (numberOfChannels: number, sampleRate: number, bitsPerSample: 16 | 24) => { await ensureModule(); const ctx = initEncoderFn(numberOfChannels, sampleRate, bitsPerSample); if (ctx === 0) { throw new Error('Failed to initialize FLAC encoder.'); } const headerPtr = getHeaderData(ctx); const headerSize = getHeaderSize(ctx); const header = module.HEAPU8.slice(headerPtr, headerPtr + headerSize).buffer; return { ctx, header }; }; const readPackets = (ctx: number) => { const packets: PacketInfo[] = []; const frameCount = getFrameCount(ctx); const outputPtr = getOutputData(ctx); let offset = 0; for (let i = 0; i < frameCount; i++) { const size = getFrameSize(ctx, i); const samples = getFrameSamples(ctx, i); const encodedData = module.HEAPU8.slice(outputPtr + offset, outputPtr + offset + size).buffer; packets.push({ encodedData, samples }); offset += size; } return packets; }; const encode = (ctx: number, audioData: ArrayBuffer, numSamples: number) => { const audioBytes = new Uint8Array(audioData); const inputPtr = getEncodeInputPtr(ctx, audioBytes.length); if (inputPtr === 0) { throw new Error('Failed to allocate encoder input buffer.'); } module.HEAPU8.set(audioBytes, inputPtr); const ret = sendSamplesFn(ctx, numSamples); if (ret < 0) { throw new Error(`Encode failed with error code ${ret}.`); } return readPackets(ctx); }; const flush = (ctx: number) => { const ret = finishEncoderFn(ctx); if (ret < 0) { throw new Error('Flush failed.'); } return readPackets(ctx); }; const onMessage = (data: { id: number; command: WorkerCommand }) => { const { id, command } = data; const handleCommand = async (): Promise => { try { let result: WorkerResponseData; const transferables: Transferable[] = []; switch (command.type) { case 'init': { const { ctx, header } = await initEncoder( command.data.numberOfChannels, command.data.sampleRate, command.data.bitsPerSample, ); result = { type: command.type, ctx, header }; transferables.push(header); }; break; case 'encode': { const packets = encode( command.data.ctx, command.data.audioData, command.data.numSamples, ); for (const p of packets) { transferables.push(p.encodedData); } result = { type: command.type, packets }; }; break; case 'flush': { const packets = flush(command.data.ctx); for (const p of packets) { transferables.push(p.encodedData); } result = { type: command.type, packets }; }; break; } const response: WorkerResponse = { id, success: true, data: result, }; sendMessage(response, transferables); } catch (error: unknown) { const response: WorkerResponse = { id, success: false, error, }; sendMessage(response); } }; void handleCommand(); }; const sendMessage = (data: unknown, transferables?: Transferable[]) => { if (parentPort) { parentPort.postMessage(data, transferables ?? []); } else { self.postMessage(data, { transfer: transferables ?? [] }); } }; let parentPort: { postMessage: (data: unknown, transferables?: Transferable[]) => void; on: (event: string, listener: (data: never) => void) => void; } | null = null; if (typeof self === 'undefined') { const workerModule = 'worker_threads'; // eslint-disable-next-line @stylistic/max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access parentPort = require(workerModule).parentPort; } if (parentPort) { parentPort.on('message', onMessage); } else { self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand })); } // Prevents the worker for being randomly closed by Firefox // https://github.com/Vanilagy/mediabunny/issues/435 setInterval(() => {}, 1000);