@mediabunny/flac-encoder 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@mediabunny/flac-encoder",
3
+ "author": "Vanilagy",
4
+ "version": "1.36.0",
5
+ "description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
6
+ "main": "./dist/bundles/mediabunny-flac-encoder.mjs",
7
+ "module": "./dist/bundles/mediabunny-flac-encoder.mjs",
8
+ "types": "./dist/modules/src/index.d.ts",
9
+ "exports": {
10
+ "types": "./dist/modules/src/index.d.ts",
11
+ "import": "./dist/bundles/mediabunny-flac-encoder.mjs",
12
+ "require": "./dist/bundles/mediabunny-flac-encoder.mjs"
13
+ },
14
+ "files": [
15
+ "README.md",
16
+ "package.json",
17
+ "LICENSE",
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "browser": {
22
+ "worker_threads": false
23
+ },
24
+ "sideEffects": false,
25
+ "license": "MPL-2.0",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Vanilagy/mediabunny.git",
29
+ "directory": "packages/flac-encoder"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/Vanilagy/mediabunny/issues"
33
+ },
34
+ "homepage": "https://mediabunny.dev/guide/extensions/flac-encoder",
35
+ "funding": {
36
+ "type": "individual",
37
+ "url": "https://github.com/sponsors/Vanilagy"
38
+ },
39
+ "peerDependencies": {
40
+ "mediabunny": "^1.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/emscripten": "^1.40.1"
44
+ },
45
+ "keywords": [
46
+ "flac",
47
+ "encoding",
48
+ "codec",
49
+ "mediabunny",
50
+ "lossless",
51
+ "browser",
52
+ "wasm"
53
+ ]
54
+ }
package/src/bridge.c ADDED
@@ -0,0 +1,255 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ #include <emscripten.h>
10
+ #include <FLAC/stream_encoder.h>
11
+ #include <stdbool.h>
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+
15
+ #define BITS_PER_SAMPLE 16
16
+ #define COMPRESSION_LEVEL 5
17
+
18
+ typedef struct {
19
+ int size;
20
+ int samples;
21
+ } FrameInfo;
22
+
23
+ typedef struct {
24
+ FLAC__StreamEncoder *encoder;
25
+
26
+ // Input buffer for interleaved int16 samples from JS
27
+ int16_t *input_buffer;
28
+ int input_buffer_size;
29
+
30
+ // Widened to int32 for libFLAC
31
+ FLAC__int32 *int32_buffer;
32
+ int int32_buffer_size;
33
+
34
+ // Contiguous output buffer for encoded frame data
35
+ uint8_t *output_buffer;
36
+ int output_size;
37
+ int output_capacity;
38
+
39
+ // Per-frame metadata so JS can split the output buffer into individual packets
40
+ FrameInfo *frames;
41
+ int frame_count;
42
+ int frames_capacity;
43
+
44
+ // Stream header captured during init (fLaC + metadata blocks)
45
+ uint8_t *header_buffer;
46
+ int header_size;
47
+ int header_capacity;
48
+ bool header_done;
49
+
50
+ int channels;
51
+ } EncoderContext;
52
+
53
+ static void ensure_output_capacity(EncoderContext *ctx, int needed) {
54
+ if (needed <= ctx->output_capacity) {
55
+ return;
56
+ }
57
+
58
+ int new_capacity = ctx->output_capacity;
59
+ if (new_capacity < 4096) {
60
+ new_capacity = 4096;
61
+ }
62
+ while (new_capacity < needed) {
63
+ new_capacity *= 2;
64
+ }
65
+
66
+ ctx->output_buffer = realloc(ctx->output_buffer, new_capacity);
67
+ ctx->output_capacity = new_capacity;
68
+ }
69
+
70
+ static FLAC__StreamEncoderWriteStatus write_callback(
71
+ const FLAC__StreamEncoder *encoder,
72
+ const FLAC__byte buffer[],
73
+ size_t bytes,
74
+ uint32_t samples,
75
+ uint32_t current_frame,
76
+ void *client_data
77
+ ) {
78
+ EncoderContext *ctx = (EncoderContext *)client_data;
79
+
80
+ // samples == 0 means this is metadata (stream header)
81
+ if (samples == 0) {
82
+ if (!ctx->header_done) {
83
+ int needed = ctx->header_size + bytes;
84
+ if (needed > ctx->header_capacity) {
85
+ int new_cap = ctx->header_capacity < 256 ? 256 : ctx->header_capacity;
86
+ while (new_cap < needed) { new_cap *= 2; }
87
+ ctx->header_buffer = realloc(ctx->header_buffer, new_cap);
88
+ ctx->header_capacity = new_cap;
89
+ }
90
+ memcpy(ctx->header_buffer + ctx->header_size, buffer, bytes);
91
+ ctx->header_size += bytes;
92
+ }
93
+
94
+ return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
95
+ }
96
+
97
+ ctx->header_done = true;
98
+
99
+ // Append encoded data
100
+ ensure_output_capacity(ctx, ctx->output_size + bytes);
101
+ memcpy(ctx->output_buffer + ctx->output_size, buffer, bytes);
102
+ ctx->output_size += bytes;
103
+
104
+ // Record frame metadata
105
+ if (ctx->frame_count >= ctx->frames_capacity) {
106
+ int new_cap = ctx->frames_capacity < 16 ? 16 : ctx->frames_capacity * 2;
107
+ ctx->frames = realloc(ctx->frames, new_cap * sizeof(FrameInfo));
108
+ ctx->frames_capacity = new_cap;
109
+ }
110
+ ctx->frames[ctx->frame_count].size = bytes;
111
+ ctx->frames[ctx->frame_count].samples = samples;
112
+ ctx->frame_count++;
113
+
114
+ return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
115
+ }
116
+
117
+ static void reset_output(EncoderContext *ctx) {
118
+ ctx->output_size = 0;
119
+ ctx->frame_count = 0;
120
+ }
121
+
122
+ EMSCRIPTEN_KEEPALIVE
123
+ int init_encoder(int channels, int sample_rate) {
124
+ EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
125
+ if (!ctx) {
126
+ return 0;
127
+ }
128
+
129
+ ctx->channels = channels;
130
+
131
+ ctx->encoder = FLAC__stream_encoder_new();
132
+ if (!ctx->encoder) {
133
+ free(ctx);
134
+ return 0;
135
+ }
136
+
137
+ FLAC__stream_encoder_set_channels(ctx->encoder, channels);
138
+ FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
139
+ FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, BITS_PER_SAMPLE);
140
+ FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
141
+ FLAC__stream_encoder_set_verify(ctx->encoder, false);
142
+
143
+ FLAC__StreamEncoderInitStatus status = FLAC__stream_encoder_init_stream(
144
+ ctx->encoder,
145
+ write_callback,
146
+ NULL, // seek callback
147
+ NULL, // tell callback
148
+ NULL, // metadata callback
149
+ ctx
150
+ );
151
+
152
+ if (status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) {
153
+ FLAC__stream_encoder_delete(ctx->encoder);
154
+ free(ctx);
155
+ return 0;
156
+ }
157
+
158
+ return (int)ctx;
159
+ }
160
+
161
+ EMSCRIPTEN_KEEPALIVE
162
+ uint8_t *get_encode_input_ptr(int ctx_ptr, int size) {
163
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
164
+
165
+ if (size > ctx->input_buffer_size) {
166
+ ctx->input_buffer = realloc(ctx->input_buffer, size);
167
+ ctx->input_buffer_size = size;
168
+ }
169
+
170
+ return (uint8_t *)ctx->input_buffer;
171
+ }
172
+
173
+ EMSCRIPTEN_KEEPALIVE
174
+ int send_samples(int ctx_ptr, int num_samples) {
175
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
176
+
177
+ // Widen int16 to int32 for libFLAC
178
+ int total = num_samples * ctx->channels;
179
+ if (total > ctx->int32_buffer_size) {
180
+ ctx->int32_buffer = realloc(ctx->int32_buffer, total * sizeof(FLAC__int32));
181
+ ctx->int32_buffer_size = total;
182
+ }
183
+ for (int i = 0; i < total; i++) {
184
+ ctx->int32_buffer[i] = ctx->input_buffer[i];
185
+ }
186
+
187
+ reset_output(ctx);
188
+
189
+ FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->int32_buffer, num_samples);
190
+ return ok ? 0 : -1;
191
+ }
192
+
193
+ EMSCRIPTEN_KEEPALIVE
194
+ uint8_t *get_output_data(int ctx_ptr) {
195
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
196
+ return ctx->output_buffer;
197
+ }
198
+
199
+ EMSCRIPTEN_KEEPALIVE
200
+ int get_frame_count(int ctx_ptr) {
201
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
202
+ return ctx->frame_count;
203
+ }
204
+
205
+ EMSCRIPTEN_KEEPALIVE
206
+ int get_frame_size(int ctx_ptr, int index) {
207
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
208
+ return ctx->frames[index].size;
209
+ }
210
+
211
+ EMSCRIPTEN_KEEPALIVE
212
+ int get_frame_samples(int ctx_ptr, int index) {
213
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
214
+ return ctx->frames[index].samples;
215
+ }
216
+
217
+ EMSCRIPTEN_KEEPALIVE
218
+ uint8_t *get_header_data(int ctx_ptr) {
219
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
220
+ return ctx->header_buffer;
221
+ }
222
+
223
+ EMSCRIPTEN_KEEPALIVE
224
+ int get_header_size(int ctx_ptr) {
225
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
226
+ return ctx->header_size;
227
+ }
228
+
229
+ EMSCRIPTEN_KEEPALIVE
230
+ int finish_encoder(int ctx_ptr) {
231
+ EncoderContext *ctx = (EncoderContext *)ctx_ptr;
232
+
233
+ reset_output(ctx);
234
+
235
+ FLAC__bool ok = FLAC__stream_encoder_finish(ctx->encoder);
236
+ if (!ok) {
237
+ return -1;
238
+ }
239
+
240
+ // finish() leaves the encoder uninitialized but retains configuration (channels, sample rate,
241
+ // etc.), so we just re-init the stream to be ready for the next batch of samples.
242
+ ctx->header_size = 0;
243
+ ctx->header_done = false;
244
+
245
+ FLAC__StreamEncoderInitStatus status = FLAC__stream_encoder_init_stream(
246
+ ctx->encoder,
247
+ write_callback,
248
+ NULL,
249
+ NULL,
250
+ NULL,
251
+ ctx
252
+ );
253
+
254
+ return status == FLAC__STREAM_ENCODER_INIT_STATUS_OK ? 0 : -1;
255
+ }
@@ -0,0 +1,193 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import createModule from '../build/flac';
10
+ import type { PacketInfo, WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
11
+
12
+ type ExtendedEmscriptenModule = EmscriptenModule & {
13
+ cwrap: typeof cwrap;
14
+ };
15
+
16
+ let module: ExtendedEmscriptenModule;
17
+ let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
18
+
19
+ let initEncoderFn: (channels: number, sampleRate: number) => number;
20
+ let getEncodeInputPtr: (ctx: number, size: number) => number;
21
+ let sendSamplesFn: (ctx: number, numSamples: number) => number;
22
+ let getOutputData: (ctx: number) => number;
23
+ let getFrameCount: (ctx: number) => number;
24
+ let getFrameSize: (ctx: number, index: number) => number;
25
+ let getFrameSamples: (ctx: number, index: number) => number;
26
+ let getHeaderData: (ctx: number) => number;
27
+ let getHeaderSize: (ctx: number) => number;
28
+ let finishEncoderFn: (ctx: number) => number;
29
+
30
+ const ensureModule = async () => {
31
+ if (!module) {
32
+ if (modulePromise) {
33
+ return modulePromise;
34
+ }
35
+
36
+ modulePromise = createModule() as Promise<ExtendedEmscriptenModule>;
37
+ module = await modulePromise;
38
+ modulePromise = null;
39
+
40
+ initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number']);
41
+ getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
42
+ sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
43
+ getOutputData = module.cwrap('get_output_data', 'number', ['number']);
44
+ getFrameCount = module.cwrap('get_frame_count', 'number', ['number']);
45
+ getFrameSize = module.cwrap('get_frame_size', 'number', ['number', 'number']);
46
+ getFrameSamples = module.cwrap('get_frame_samples', 'number', ['number', 'number']);
47
+ getHeaderData = module.cwrap('get_header_data', 'number', ['number']);
48
+ getHeaderSize = module.cwrap('get_header_size', 'number', ['number']);
49
+ finishEncoderFn = module.cwrap('finish_encoder', 'number', ['number']);
50
+ }
51
+ };
52
+
53
+ const initEncoder = async (numberOfChannels: number, sampleRate: number) => {
54
+ await ensureModule();
55
+
56
+ const ctx = initEncoderFn(numberOfChannels, sampleRate);
57
+ if (ctx === 0) {
58
+ throw new Error('Failed to initialize FLAC encoder.');
59
+ }
60
+
61
+ const headerPtr = getHeaderData(ctx);
62
+ const headerSize = getHeaderSize(ctx);
63
+ const header = module.HEAPU8.slice(headerPtr, headerPtr + headerSize).buffer;
64
+
65
+ return { ctx, header };
66
+ };
67
+
68
+ const readPackets = (ctx: number) => {
69
+ const packets: PacketInfo[] = [];
70
+ const frameCount = getFrameCount(ctx);
71
+ const outputPtr = getOutputData(ctx);
72
+
73
+ let offset = 0;
74
+ for (let i = 0; i < frameCount; i++) {
75
+ const size = getFrameSize(ctx, i);
76
+ const samples = getFrameSamples(ctx, i);
77
+ const encodedData = module.HEAPU8.slice(outputPtr + offset, outputPtr + offset + size).buffer;
78
+ packets.push({ encodedData, samples });
79
+ offset += size;
80
+ }
81
+
82
+ return packets;
83
+ };
84
+
85
+ const encode = (ctx: number, audioData: ArrayBuffer, numSamples: number) => {
86
+ const audioBytes = new Uint8Array(audioData);
87
+
88
+ const inputPtr = getEncodeInputPtr(ctx, audioBytes.length);
89
+ if (inputPtr === 0) {
90
+ throw new Error('Failed to allocate encoder input buffer.');
91
+ }
92
+ module.HEAPU8.set(audioBytes, inputPtr);
93
+
94
+ const ret = sendSamplesFn(ctx, numSamples);
95
+ if (ret < 0) {
96
+ throw new Error(`Encode failed with error code ${ret}.`);
97
+ }
98
+
99
+ return readPackets(ctx);
100
+ };
101
+
102
+ const flush = (ctx: number) => {
103
+ const ret = finishEncoderFn(ctx);
104
+ if (ret < 0) {
105
+ throw new Error('Flush failed.');
106
+ }
107
+
108
+ return readPackets(ctx);
109
+ };
110
+
111
+ const onMessage = (data: { id: number; command: WorkerCommand }) => {
112
+ const { id, command } = data;
113
+
114
+ const handleCommand = async (): Promise<void> => {
115
+ try {
116
+ let result: WorkerResponseData;
117
+ const transferables: Transferable[] = [];
118
+
119
+ switch (command.type) {
120
+ case 'init': {
121
+ const { ctx, header } = await initEncoder(
122
+ command.data.numberOfChannels,
123
+ command.data.sampleRate,
124
+ );
125
+ result = { type: command.type, ctx, header };
126
+ transferables.push(header);
127
+ }; break;
128
+
129
+ case 'encode': {
130
+ const packets = encode(
131
+ command.data.ctx,
132
+ command.data.audioData,
133
+ command.data.numSamples,
134
+ );
135
+ for (const p of packets) {
136
+ transferables.push(p.encodedData);
137
+ }
138
+ result = { type: command.type, packets };
139
+ }; break;
140
+
141
+ case 'flush': {
142
+ const packets = flush(command.data.ctx);
143
+ for (const p of packets) {
144
+ transferables.push(p.encodedData);
145
+ }
146
+ result = { type: command.type, packets };
147
+ }; break;
148
+ }
149
+
150
+ const response: WorkerResponse = {
151
+ id,
152
+ success: true,
153
+ data: result,
154
+ };
155
+ sendMessage(response, transferables);
156
+ } catch (error: unknown) {
157
+ const response: WorkerResponse = {
158
+ id,
159
+ success: false,
160
+ error,
161
+ };
162
+ sendMessage(response);
163
+ }
164
+ };
165
+
166
+ void handleCommand();
167
+ };
168
+
169
+ const sendMessage = (data: unknown, transferables?: Transferable[]) => {
170
+ if (parentPort) {
171
+ parentPort.postMessage(data, transferables ?? []);
172
+ } else {
173
+ self.postMessage(data, { transfer: transferables ?? [] });
174
+ }
175
+ };
176
+
177
+ let parentPort: {
178
+ postMessage: (data: unknown, transferables?: Transferable[]) => void;
179
+ on: (event: string, listener: (data: never) => void) => void;
180
+ } | null = null;
181
+
182
+ if (typeof self === 'undefined') {
183
+ const workerModule = 'worker_threads';
184
+ // eslint-disable-next-line @stylistic/max-len
185
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access
186
+ parentPort = require(workerModule).parentPort;
187
+ }
188
+
189
+ if (parentPort) {
190
+ parentPort.on('message', onMessage);
191
+ } else {
192
+ self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
193
+ }
package/src/encoder.ts ADDED
@@ -0,0 +1,200 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import {
10
+ CustomAudioEncoder,
11
+ AudioCodec,
12
+ AudioSample,
13
+ EncodedPacket,
14
+ registerEncoder,
15
+ } from 'mediabunny';
16
+ import type { PacketInfo, WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
17
+ // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
18
+ import createWorker from './encode.worker';
19
+
20
+ const FLAC_SAMPLE_RATES = [
21
+ 8000, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000,
22
+ ];
23
+
24
+ class FlacEncoder extends CustomAudioEncoder {
25
+ private worker: Worker | null = null;
26
+ private nextMessageId = 0;
27
+ private pendingMessages = new Map<number, {
28
+ resolve: (value: WorkerResponseData) => void;
29
+ reject: (reason?: unknown) => void;
30
+ }>();
31
+
32
+ private ctx = 0;
33
+ private chunkMetadata: EncodedAudioChunkMetadata = {};
34
+ private description: Uint8Array | null = null;
35
+ private nextTimestampInSamples: number | null = null;
36
+
37
+ static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean {
38
+ return codec === 'flac'
39
+ && config.numberOfChannels >= 1
40
+ && config.numberOfChannels <= 8
41
+ && FLAC_SAMPLE_RATES.includes(config.sampleRate);
42
+ }
43
+
44
+ async init() {
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
46
+ this.worker = (await createWorker()) as Worker;
47
+
48
+ const onMessage = (data: WorkerResponse) => {
49
+ const pending = this.pendingMessages.get(data.id);
50
+ assert(pending !== undefined);
51
+
52
+ this.pendingMessages.delete(data.id);
53
+ if (data.success) {
54
+ pending.resolve(data.data);
55
+ } else {
56
+ pending.reject(data.error);
57
+ }
58
+ };
59
+
60
+ if (this.worker.addEventListener) {
61
+ this.worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
62
+ } else {
63
+ const nodeWorker = this.worker as unknown as {
64
+ on: (event: string, listener: (data: never) => void) => void;
65
+ };
66
+ nodeWorker.on('message', onMessage);
67
+ }
68
+
69
+ const result = await this.sendCommand({
70
+ type: 'init',
71
+ data: {
72
+ numberOfChannels: this.config.numberOfChannels,
73
+ sampleRate: this.config.sampleRate,
74
+ },
75
+ });
76
+
77
+ this.ctx = result.ctx;
78
+
79
+ this.description = new Uint8Array(result.header);
80
+ this.resetInternalState();
81
+ }
82
+
83
+ private resetInternalState() {
84
+ this.nextTimestampInSamples = null;
85
+
86
+ this.chunkMetadata = {
87
+ decoderConfig: {
88
+ codec: 'flac',
89
+ numberOfChannels: this.config.numberOfChannels,
90
+ sampleRate: this.config.sampleRate,
91
+ description: this.description!,
92
+ },
93
+ };
94
+ }
95
+
96
+ async encode(audioSample: AudioSample) {
97
+ if (this.nextTimestampInSamples === null) {
98
+ this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
99
+ }
100
+
101
+ const totalBytes = audioSample.allocationSize({ format: 's16', planeIndex: 0 });
102
+ const audioBytes = new Uint8Array(totalBytes);
103
+ audioSample.copyTo(audioBytes, { format: 's16', planeIndex: 0 });
104
+
105
+ const audioData = audioBytes.buffer;
106
+ const result = await this.sendCommand({
107
+ type: 'encode',
108
+ data: {
109
+ ctx: this.ctx,
110
+ audioData,
111
+ numSamples: audioSample.numberOfFrames,
112
+ },
113
+ }, [audioData]);
114
+
115
+ this.emitPackets(result.packets);
116
+ }
117
+
118
+ async flush() {
119
+ const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
120
+ this.emitPackets(result.packets);
121
+
122
+ this.resetInternalState();
123
+ }
124
+
125
+ close() {
126
+ this.worker?.terminate();
127
+ }
128
+
129
+ private emitPackets(packets: PacketInfo[]) {
130
+ assert(this.nextTimestampInSamples !== null);
131
+
132
+ for (const p of packets) {
133
+ const data = new Uint8Array(p.encodedData);
134
+
135
+ const packet = new EncodedPacket(
136
+ data,
137
+ 'key',
138
+ this.nextTimestampInSamples / this.config.sampleRate,
139
+ p.samples / this.config.sampleRate,
140
+ );
141
+ this.nextTimestampInSamples += p.samples;
142
+
143
+ this.onPacket(
144
+ packet,
145
+ this.chunkMetadata,
146
+ );
147
+
148
+ this.chunkMetadata = {};
149
+ }
150
+ }
151
+
152
+ private sendCommand<T extends string>(
153
+ command: WorkerCommand & { type: T },
154
+ transferables?: Transferable[],
155
+ ) {
156
+ return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
157
+ const id = this.nextMessageId++;
158
+ this.pendingMessages.set(id, {
159
+ resolve: resolve as (value: WorkerResponseData) => void,
160
+ reject,
161
+ });
162
+
163
+ assert(this.worker);
164
+
165
+ if (transferables) {
166
+ this.worker.postMessage({ id, command }, transferables);
167
+ } else {
168
+ this.worker.postMessage({ id, command });
169
+ }
170
+ });
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Registers the FLAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
176
+ * function before starting any encoding task.
177
+ *
178
+ * Preferably, wrap the call in a condition to avoid overriding any native FLAC encoder:
179
+ *
180
+ * ```ts
181
+ * import { canEncodeAudio } from 'mediabunny';
182
+ * import { registerFlacEncoder } from '@mediabunny/flac-encoder';
183
+ *
184
+ * if (!(await canEncodeAudio('flac'))) {
185
+ * registerFlacEncoder();
186
+ * }
187
+ * ```
188
+ *
189
+ * @group \@mediabunny/flac-encoder
190
+ * @public
191
+ */
192
+ export const registerFlacEncoder = () => {
193
+ registerEncoder(FlacEncoder);
194
+ };
195
+
196
+ function assert(x: unknown): asserts x {
197
+ if (!x) {
198
+ throw new Error('Assertion failed.');
199
+ }
200
+ }