| /*! | |
| * 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 { | |
| CustomAudioEncoder, | |
| AudioCodec, | |
| AudioSample, | |
| EncodedPacket, | |
| registerEncoder, | |
| } from 'mediabunny'; | |
| import type { PacketInfo, WorkerCommand, WorkerResponse, WorkerResponseData } from './shared'; | |
| // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand | |
| import createWorker from './encode.worker'; | |
| const FLAC_SAMPLE_RATES = [ | |
| 8000, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000, | |
| ]; | |
| class FlacEncoder extends CustomAudioEncoder { | |
| private worker: Worker | null = null; | |
| private workerError: Error | null = null; | |
| private nextMessageId = 0; | |
| private pendingMessages = new Map<number, { | |
| resolve: (value: WorkerResponseData) => void; | |
| reject: (reason?: unknown) => void; | |
| }>(); | |
| private ctx: number | null = null; | |
| private chunkMetadata: EncodedAudioChunkMetadata = {}; | |
| private description: Uint8Array | null = null; | |
| private nextTimestampInSamples: number | null = null; | |
| static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean { | |
| return codec === 'flac' | |
| && config.numberOfChannels >= 1 | |
| && config.numberOfChannels <= 8 | |
| && FLAC_SAMPLE_RATES.includes(config.sampleRate); | |
| } | |
| async init() { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |
| this.worker = (await createWorker()) as Worker; | |
| const onMessage = (data: WorkerResponse) => { | |
| const pending = this.pendingMessages.get(data.id); | |
| assert(pending !== undefined); | |
| this.pendingMessages.delete(data.id); | |
| if (data.success) { | |
| pending.resolve(data.data); | |
| } else { | |
| pending.reject(data.error); | |
| } | |
| }; | |
| // Treats any worker error (like CSP errors) as fatal, rejecting pending and future commands | |
| const onError = (error: Error) => { | |
| this.workerError = error; | |
| for (const pending of this.pendingMessages.values()) { | |
| pending.reject(error); | |
| } | |
| this.pendingMessages.clear(); | |
| this.worker?.terminate(); | |
| this.worker = null; | |
| }; | |
| if (this.worker.addEventListener) { | |
| this.worker.addEventListener('message', event => onMessage(event.data as WorkerResponse)); | |
| this.worker.addEventListener('error', (event) => { | |
| onError(new Error(event.message || 'FLAC encoder worker failed to load or crashed.')); | |
| }); | |
| } else { | |
| const nodeWorker = this.worker as unknown as { | |
| on: (event: string, listener: (data: never) => void) => void; | |
| }; | |
| nodeWorker.on('message', onMessage); | |
| nodeWorker.on('error', onError); | |
| } | |
| } | |
| private resetInternalState() { | |
| this.nextTimestampInSamples = null; | |
| this.chunkMetadata = { | |
| decoderConfig: { | |
| codec: 'flac', | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| description: this.description!, | |
| }, | |
| }; | |
| } | |
| async encode(audioSample: AudioSample) { | |
| if (this.ctx === null) { | |
| // This is the first sample, let's do some init | |
| let bitsPerSample: 16 | 24; | |
| switch (audioSample.format) { | |
| case 'u8': | |
| case 'u8-planar': | |
| case 's16': | |
| case 's16-planar': | |
| bitsPerSample = 16; | |
| break; | |
| case 's32': | |
| case 's32-planar': | |
| case 'f32': | |
| case 'f32-planar': | |
| bitsPerSample = 24; | |
| break; | |
| default: | |
| assertNever(audioSample.format); | |
| assert(false); | |
| } | |
| const result = await this.sendCommand({ | |
| type: 'init', | |
| data: { | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| bitsPerSample, | |
| }, | |
| }); | |
| this.ctx = result.ctx; | |
| this.description = new Uint8Array(result.header); | |
| this.resetInternalState(); | |
| } | |
| if (this.nextTimestampInSamples === null) { | |
| this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate); | |
| } | |
| const totalBytes = audioSample.allocationSize({ format: 's32', planeIndex: 0 }); | |
| const audioData = new ArrayBuffer(totalBytes); | |
| audioSample.copyTo(audioData, { format: 's32', planeIndex: 0 }); | |
| const result = await this.sendCommand({ | |
| type: 'encode', | |
| data: { | |
| ctx: this.ctx, | |
| audioData, | |
| numSamples: audioSample.numberOfFrames, | |
| }, | |
| }, [audioData]); | |
| this.emitPackets(result.packets); | |
| } | |
| async flush() { | |
| if (this.ctx === null) { | |
| return; | |
| } | |
| const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } }); | |
| this.emitPackets(result.packets); | |
| this.resetInternalState(); | |
| } | |
| close() { | |
| this.worker?.terminate(); | |
| } | |
| private emitPackets(packets: PacketInfo[]) { | |
| assert(this.nextTimestampInSamples !== null); | |
| for (const p of packets) { | |
| const data = new Uint8Array(p.encodedData); | |
| const packet = new EncodedPacket( | |
| data, | |
| 'key', | |
| this.nextTimestampInSamples / this.config.sampleRate, | |
| p.samples / this.config.sampleRate, | |
| ); | |
| this.nextTimestampInSamples += p.samples; | |
| this.onPacket( | |
| packet, | |
| this.chunkMetadata, | |
| ); | |
| this.chunkMetadata = {}; | |
| } | |
| } | |
| private sendCommand<T extends string>( | |
| command: WorkerCommand & { type: T }, | |
| transferables?: Transferable[], | |
| ) { | |
| return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => { | |
| if (this.workerError) { | |
| reject(this.workerError); | |
| return; | |
| } | |
| const id = this.nextMessageId++; | |
| this.pendingMessages.set(id, { | |
| resolve: resolve as (value: WorkerResponseData) => void, | |
| reject, | |
| }); | |
| assert(this.worker); | |
| if (transferables) { | |
| this.worker.postMessage({ id, command }, transferables); | |
| } else { | |
| this.worker.postMessage({ id, command }); | |
| } | |
| }); | |
| } | |
| } | |
| let registered = false; | |
| /** | |
| * Registers the FLAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this | |
| * function before starting any encoding task. The FLAC encoder will automatically determine the output bit depth | |
| * (16 or 24) based on the sample format of incoming `AudioSample` instances. | |
| * | |
| * Preferably, wrap the call in a condition to avoid overriding any native FLAC encoder: | |
| * | |
| * ```ts | |
| * import { canEncodeAudio } from 'mediabunny'; | |
| * import { registerFlacEncoder } from '@mediabunny/flac-encoder'; | |
| * | |
| * if (!(await canEncodeAudio('flac'))) { | |
| * registerFlacEncoder(); | |
| * } | |
| * ``` | |
| * | |
| * @group \@mediabunny/flac-encoder | |
| * @public | |
| */ | |
| export const registerFlacEncoder = () => { | |
| if (registered) { | |
| return; | |
| } | |
| registered = true; | |
| registerEncoder(FlacEncoder); | |
| }; | |
| function assert(x: unknown): asserts x { | |
| if (!x) { | |
| throw new Error('Assertion failed.'); | |
| } | |
| } | |
| export const assertNever = (x: never) => { | |
| // eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
| throw new Error(`Unexpected value: ${x}`); | |
| }; | |
Xet Storage Details
- Size:
- 6.8 kB
- Xet hash:
- 6d506df1ce34f61b4ca90b58e0251e9794d9045b78f8205d0d1c2fc91e16289b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.