@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/LICENSE +373 -0
- package/README.md +140 -0
- package/dist/bundles/mediabunny-flac-encoder.js +235 -0
- package/dist/bundles/mediabunny-flac-encoder.min.js +1365 -0
- package/dist/bundles/mediabunny-flac-encoder.min.mjs +1364 -0
- package/dist/bundles/mediabunny-flac-encoder.mjs +194 -0
- package/dist/mediabunny-flac-encoder.d.ts +22 -0
- package/dist/modules/build/flac.d.ts +3 -0
- package/dist/modules/build/flac.d.ts.map +1 -0
- package/dist/modules/build/flac.js +0 -0
- package/dist/modules/src/encode.worker.d.ts +9 -0
- package/dist/modules/src/encode.worker.d.ts.map +1 -0
- package/dist/modules/src/encode.worker.js +165 -0
- package/dist/modules/src/encoder.d.ts +27 -0
- package/dist/modules/src/encoder.d.ts.map +1 -0
- package/dist/modules/src/encoder.js +160 -0
- package/dist/modules/src/index.d.ts +9 -0
- package/dist/modules/src/index.d.ts.map +1 -0
- package/dist/modules/src/index.js +20 -0
- package/dist/modules/src/shared.d.ts +51 -0
- package/dist/modules/src/shared.d.ts.map +1 -0
- package/dist/modules/src/shared.js +9 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +54 -0
- package/src/bridge.c +255 -0
- package/src/encode.worker.ts +193 -0
- package/src/encoder.ts +200 -0
- package/src/index.ts +20 -0
- package/src/shared.ts +54 -0
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
|
+
}
|