| /*! | |
| * 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/lame'; | |
| import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared'; | |
| type ExtendedEmscriptenModule = EmscriptenModule & { | |
| cwrap: typeof cwrap; | |
| }; | |
| let module: ExtendedEmscriptenModule; | |
| let lamePtr: number; | |
| let storedNumberOfChannels: number; | |
| let storedSampleRate: number; | |
| let storedBitrate: number; | |
| let needsReset = false; | |
| let initLame: ( | |
| numberOfChannels: number, | |
| sampleRate: number, | |
| bitrate: number | |
| ) => number; | |
| let encodeSamples: ( | |
| lame: number, | |
| leftPtr: number, | |
| rightPtr: number, | |
| sampleCount: number, | |
| outputPtr: number, | |
| outputSize: number | |
| ) => number; | |
| let flushLame: (lame: number, outputPtr: number, outputSize: number) => number; | |
| let closeLame: (lame: number) => void; | |
| let inputSlice: Slice | null = null; | |
| let outputSlice: Slice | null = null; | |
| const init = async (numberOfChannels: number, sampleRate: number, bitrate: number) => { | |
| storedNumberOfChannels = numberOfChannels; | |
| storedSampleRate = sampleRate; | |
| storedBitrate = bitrate; | |
| module = (await createModule()) as ExtendedEmscriptenModule; | |
| // Set up the functions | |
| initLame = module.cwrap('init_lame', 'number', ['number', 'number', 'number']); | |
| encodeSamples = module.cwrap( | |
| 'encode_samples', | |
| 'number', | |
| ['number', 'number', 'number', 'number', 'number', 'number'], | |
| ); | |
| flushLame = module.cwrap('flush_lame', 'number', ['number', 'number', 'number']); | |
| closeLame = module.cwrap('close_lame', null, ['number']); | |
| lamePtr = initLame(numberOfChannels, sampleRate, bitrate); | |
| }; | |
| const reset = () => { | |
| closeLame(lamePtr); | |
| lamePtr = initLame(storedNumberOfChannels, storedSampleRate, storedBitrate); | |
| }; | |
| const encode = (audioData: ArrayBuffer, numberOfFrames: number) => { | |
| if (needsReset) { | |
| reset(); | |
| needsReset = false; | |
| } | |
| const audioBytes = new Uint8Array(audioData); | |
| const sizePerChannel = audioBytes.length / storedNumberOfChannels; | |
| inputSlice = maybeGrowSlice(inputSlice, audioBytes.length); | |
| module.HEAPU8.set(audioBytes, inputSlice.ptr); | |
| const requiredOutputSize = Math.ceil(1.25 * numberOfFrames + 7200); | |
| outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize); | |
| const bytesWritten = encodeSamples( | |
| lamePtr, | |
| inputSlice.ptr, | |
| inputSlice.ptr + (storedNumberOfChannels - 1) * sizePerChannel, | |
| numberOfFrames, | |
| outputSlice.ptr, | |
| requiredOutputSize, | |
| ); | |
| const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten); | |
| return result.buffer; | |
| }; | |
| const flush = () => { | |
| if (needsReset) { | |
| reset(); | |
| needsReset = false; | |
| } | |
| const requiredOutputSize = 7200; | |
| outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize); | |
| const bytesWritten = flushLame(lamePtr, outputSlice.ptr, requiredOutputSize); | |
| const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten); | |
| needsReset = true; // After a flush, the encoder must be prepared to start a new encoding process | |
| return result.buffer; | |
| }; | |
| /** A "fat pointer" type thing. */ | |
| type Slice = { | |
| ptr: number; | |
| size: number; | |
| }; | |
| /** Either returns the existing slice, or allocates a new one if there's no existing slice or it was too small. */ | |
| const maybeGrowSlice = (slice: Slice | null, requiredSize: number) => { | |
| if (!slice || slice.size < requiredSize) { | |
| if (slice) { | |
| module._free(slice.ptr); | |
| } | |
| return { | |
| ptr: module._malloc(requiredSize), | |
| size: requiredSize, | |
| }; | |
| } | |
| return slice; | |
| }; | |
| 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': { | |
| await init( | |
| command.data.numberOfChannels, | |
| command.data.sampleRate, | |
| command.data.bitrate, | |
| ); | |
| result = { | |
| type: command.type, | |
| success: true, | |
| }; | |
| }; break; | |
| case 'encode': { | |
| const encodedData = encode( | |
| command.data.audioData, | |
| command.data.numberOfFrames, | |
| ); | |
| result = { | |
| type: command.type, | |
| encodedData, | |
| }; | |
| transferables.push(encodedData); | |
| }; break; | |
| case 'flush': { | |
| const flushedData = flush(); | |
| result = { | |
| type: command.type, | |
| flushedData, | |
| }; | |
| transferables.push(flushedData); | |
| }; break; | |
| } | |
| const response: WorkerResponse = { | |
| id, | |
| success: true, | |
| data: result, | |
| }; | |
| sendMessage(response, transferables); | |
| } catch (error) { | |
| 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-require-imports, @typescript-eslint/no-unsafe-assignment, @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:
- 5.99 kB
- Xet hash:
- 27a58aabfb74e5087bcfd016e019da2fbc73949f8f84591f69bf1d123c51b010
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.