| /*! | |
| * 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<ExtendedEmscriptenModule> | 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<ExtendedEmscriptenModule>; | |
| 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<void> => { | |
| 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; | |
| try { | |
| // We're in Node.js (or a runtime that mimics it) | |
| 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; | |
| } catch { | |
| // Web worker (browser, Deno), no require available | |
| } | |
| 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); | |
Xet Storage Details
- Size:
- 6.23 kB
- Xet hash:
- d3e33ae874662b217c13157d32037b366a0bce2da852885094a06b81745b0095
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.