@mediabunny/mp3-encoder 1.6.1

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.
@@ -0,0 +1,9 @@
1
+ /*!
2
+ * Copyright (c) 2025-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
+ export {};
9
+ //# sourceMappingURL=encode.worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.worker.d.ts","sourceRoot":"","sources":["../../../src/encode.worker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /*!
3
+ * Copyright (c) 2025-present, Vanilagy and contributors
4
+ *
5
+ * This Source Code Form is subject to the terms of the Mozilla Public
6
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const lame_1 = __importDefault(require("../build/lame"));
14
+ let module;
15
+ let lamePtr;
16
+ let storedNumberOfChannels;
17
+ let storedSampleRate;
18
+ let storedBitrate;
19
+ let needsReset = false;
20
+ let initLame;
21
+ let encodeSamples;
22
+ let flushLame;
23
+ let closeLame;
24
+ let inputSlice = null;
25
+ let outputSlice = null;
26
+ const init = async (numberOfChannels, sampleRate, bitrate) => {
27
+ storedNumberOfChannels = numberOfChannels;
28
+ storedSampleRate = sampleRate;
29
+ storedBitrate = bitrate;
30
+ module = (await (0, lame_1.default)());
31
+ // Set up the functions
32
+ initLame = module.cwrap('init_lame', 'number', ['number', 'number', 'number']);
33
+ encodeSamples = module.cwrap('encode_samples', 'number', ['number', 'number', 'number', 'number', 'number', 'number']);
34
+ flushLame = module.cwrap('flush_lame', 'number', ['number', 'number', 'number']);
35
+ closeLame = module.cwrap('close_lame', null, ['number']);
36
+ lamePtr = initLame(numberOfChannels, sampleRate, bitrate);
37
+ };
38
+ const reset = () => {
39
+ closeLame(lamePtr);
40
+ lamePtr = initLame(storedNumberOfChannels, storedSampleRate, storedBitrate);
41
+ };
42
+ const encode = (audioData, numberOfFrames) => {
43
+ if (needsReset) {
44
+ reset();
45
+ needsReset = false;
46
+ }
47
+ const audioBytes = new Uint8Array(audioData);
48
+ const sizePerChannel = audioBytes.length / storedNumberOfChannels;
49
+ inputSlice = maybeGrowSlice(inputSlice, audioBytes.length);
50
+ module.HEAPU8.set(audioBytes, inputSlice.ptr);
51
+ const requiredOutputSize = Math.ceil(1.25 * numberOfFrames + 7200);
52
+ outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize);
53
+ const bytesWritten = encodeSamples(lamePtr, inputSlice.ptr, inputSlice.ptr + (storedNumberOfChannels - 1) * sizePerChannel, numberOfFrames, outputSlice.ptr, requiredOutputSize);
54
+ const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten);
55
+ return result.buffer;
56
+ };
57
+ const flush = () => {
58
+ if (needsReset) {
59
+ reset();
60
+ needsReset = false;
61
+ }
62
+ const requiredOutputSize = 7200;
63
+ outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize);
64
+ const bytesWritten = flushLame(lamePtr, outputSlice.ptr, requiredOutputSize);
65
+ const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten);
66
+ needsReset = true; // After a flush, the encoder must be prepared to start a new encoding process
67
+ return result.buffer;
68
+ };
69
+ /** Either returns the existing slice, or allocates a new one if there's no existing slice or it was too small. */
70
+ const maybeGrowSlice = (slice, requiredSize) => {
71
+ if (!slice || slice.size < requiredSize) {
72
+ if (slice) {
73
+ module._free(slice.ptr);
74
+ }
75
+ return {
76
+ ptr: module._malloc(requiredSize),
77
+ size: requiredSize,
78
+ };
79
+ }
80
+ return slice;
81
+ };
82
+ const onMessage = (data) => {
83
+ const { id, command } = data;
84
+ const handleCommand = async () => {
85
+ try {
86
+ let result;
87
+ const transferables = [];
88
+ switch (command.type) {
89
+ case 'init':
90
+ {
91
+ await init(command.data.numberOfChannels, command.data.sampleRate, command.data.bitrate);
92
+ result = { success: true };
93
+ }
94
+ ;
95
+ break;
96
+ case 'encode':
97
+ {
98
+ const encodedData = encode(command.data.audioData, command.data.numberOfFrames);
99
+ result = { encodedData };
100
+ transferables.push(encodedData);
101
+ }
102
+ ;
103
+ break;
104
+ case 'flush':
105
+ {
106
+ const flushedData = flush();
107
+ result = { flushedData };
108
+ transferables.push(flushedData);
109
+ }
110
+ ;
111
+ break;
112
+ }
113
+ const response = {
114
+ id,
115
+ success: true,
116
+ data: result,
117
+ };
118
+ sendMessage(response, transferables);
119
+ }
120
+ catch (error) {
121
+ const response = {
122
+ id,
123
+ success: false,
124
+ error,
125
+ };
126
+ sendMessage(response);
127
+ }
128
+ };
129
+ void handleCommand();
130
+ };
131
+ const sendMessage = (data, transferables) => {
132
+ if (parentPort) {
133
+ parentPort.postMessage(data, transferables ?? []);
134
+ }
135
+ else {
136
+ self.postMessage(data, { transfer: transferables ?? [] });
137
+ }
138
+ };
139
+ let parentPort = null;
140
+ if (typeof self === 'undefined') {
141
+ // We're in Node.js (or a runtime that mimics it)
142
+ const workerModule = 'worker_threads';
143
+ // eslint-disable-next-line @stylistic/max-len
144
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
145
+ parentPort = require(workerModule).parentPort;
146
+ }
147
+ if (parentPort) {
148
+ parentPort.on('message', onMessage);
149
+ }
150
+ else {
151
+ self.addEventListener('message', event => onMessage(event.data));
152
+ }
@@ -0,0 +1,26 @@
1
+ /*!
2
+ * Copyright (c) 2025-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
+ * Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
10
+ * function before starting any encoding task.
11
+ *
12
+ * Preferably, wrap the call in a condition to avoid overriding any native MP3 encoder:
13
+ *
14
+ * ```ts
15
+ * import { canEncodeAudio } from 'mediabunny';
16
+ * import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
17
+ *
18
+ * if (!(await canEncodeAudio('mp3'))) {
19
+ * registerMp3Encoder();
20
+ * }
21
+ * ```
22
+ *
23
+ * @public
24
+ */
25
+ export declare const registerMp3Encoder: () => void;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqLH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,YAE9B,CAAC"}
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /*!
3
+ * Copyright (c) 2025-present, Vanilagy and contributors
4
+ *
5
+ * This Source Code Form is subject to the terms of the Mozilla Public
6
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.registerMp3Encoder = void 0;
14
+ const mediabunny_1 = require("mediabunny");
15
+ const mp3_misc_1 = require("../../../shared/mp3-misc");
16
+ // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
17
+ const encode_worker_1 = __importDefault(require("./encode.worker"));
18
+ class Mp3Encoder extends mediabunny_1.CustomAudioEncoder {
19
+ constructor() {
20
+ super(...arguments);
21
+ this.worker = null;
22
+ this.nextMessageId = 0;
23
+ this.pendingMessages = new Map();
24
+ this.buffer = new Uint8Array(2 ** 16);
25
+ this.currentBufferOffset = 0;
26
+ this.currentTimestamp = 0;
27
+ this.chunkMetadata = {};
28
+ }
29
+ static supports(codec, config) {
30
+ return codec === 'mp3'
31
+ && (config.numberOfChannels === 1 || config.numberOfChannels === 2)
32
+ && (config.sampleRate === 32000 || config.sampleRate === 44100 || config.sampleRate === 48000);
33
+ }
34
+ async init() {
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
36
+ this.worker = (await (0, encode_worker_1.default)()); // The actual encoding takes place in this worker
37
+ const onMessage = (data) => {
38
+ const pending = this.pendingMessages.get(data.id);
39
+ assert(pending !== undefined);
40
+ this.pendingMessages.delete(data.id);
41
+ if (data.success) {
42
+ pending.resolve(data.data);
43
+ }
44
+ else {
45
+ pending.reject(data.error);
46
+ }
47
+ };
48
+ if (this.worker.addEventListener) {
49
+ this.worker.addEventListener('message', event => onMessage(event.data));
50
+ }
51
+ else {
52
+ const nodeWorker = this.worker;
53
+ nodeWorker.on('message', onMessage);
54
+ }
55
+ assert(this.config.bitrate);
56
+ await this.sendCommand({
57
+ type: 'init',
58
+ data: {
59
+ numberOfChannels: this.config.numberOfChannels,
60
+ sampleRate: this.config.sampleRate,
61
+ bitrate: this.config.bitrate,
62
+ },
63
+ });
64
+ this.chunkMetadata = {
65
+ decoderConfig: {
66
+ codec: 'mp3',
67
+ numberOfChannels: this.config.numberOfChannels,
68
+ sampleRate: this.config.sampleRate,
69
+ },
70
+ };
71
+ }
72
+ async encode(audioSample) {
73
+ const sizePerChannel = audioSample.allocationSize({
74
+ format: 's16-planar',
75
+ planeIndex: 0,
76
+ });
77
+ const requiredBytes = audioSample.numberOfChannels * sizePerChannel;
78
+ const audioData = new ArrayBuffer(requiredBytes);
79
+ const audioBytes = new Uint8Array(audioData);
80
+ for (let i = 0; i < audioSample.numberOfChannels; i++) {
81
+ audioSample.copyTo(audioBytes.subarray(i * sizePerChannel), {
82
+ format: 's16-planar', // LAME wants it in this format
83
+ planeIndex: i,
84
+ });
85
+ }
86
+ const result = await this.sendCommand({
87
+ type: 'encode',
88
+ data: {
89
+ audioData,
90
+ numberOfFrames: audioSample.numberOfFrames,
91
+ },
92
+ }, [audioData]);
93
+ assert('encodedData' in result);
94
+ this.digestOutput(new Uint8Array(result.encodedData));
95
+ }
96
+ async flush() {
97
+ const result = await this.sendCommand({ type: 'flush' });
98
+ assert('flushedData' in result);
99
+ this.digestOutput(new Uint8Array(result.flushedData));
100
+ }
101
+ close() {
102
+ this.worker?.terminate();
103
+ }
104
+ /**
105
+ * LAME returns data in chunks, but a chunk doesn't need to contain a full MP3 frame. Therefore, we must accumulate
106
+ * these chunks and extract the MP3 frames only when they're complete.
107
+ */
108
+ digestOutput(bytes) {
109
+ const requiredBufferSize = this.currentBufferOffset + bytes.length;
110
+ if (requiredBufferSize > this.buffer.length) {
111
+ // Grow the buffer to the required size
112
+ const newSize = 1 << Math.ceil(Math.log2(requiredBufferSize));
113
+ const newBuffer = new Uint8Array(newSize);
114
+ newBuffer.set(this.buffer);
115
+ this.buffer = newBuffer;
116
+ }
117
+ this.buffer.set(bytes, this.currentBufferOffset);
118
+ this.currentBufferOffset = requiredBufferSize;
119
+ let pos = 0;
120
+ while (pos <= this.currentBufferOffset - mp3_misc_1.FRAME_HEADER_SIZE) {
121
+ const word = new DataView(this.buffer.buffer).getUint32(pos, false);
122
+ const header = (0, mp3_misc_1.readFrameHeader)(word, { pos, fileSize: null });
123
+ if (!header) {
124
+ break;
125
+ }
126
+ const fits = header.totalSize <= this.currentBufferOffset - pos;
127
+ if (!fits) {
128
+ // The frame isn't complete yet
129
+ break;
130
+ }
131
+ const data = this.buffer.slice(pos, pos + header.totalSize);
132
+ const duration = header.audioSamplesInFrame / header.sampleRate;
133
+ this.onPacket(new mediabunny_1.EncodedPacket(data, 'key', this.currentTimestamp, duration), this.chunkMetadata);
134
+ if (this.currentTimestamp === 0) {
135
+ this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
136
+ }
137
+ this.currentTimestamp += duration;
138
+ pos += header.totalSize;
139
+ }
140
+ if (pos > 0) {
141
+ // Shift the data
142
+ this.buffer.set(this.buffer.subarray(pos, this.currentBufferOffset), 0);
143
+ this.currentBufferOffset -= pos;
144
+ }
145
+ }
146
+ sendCommand(command, transferables) {
147
+ return new Promise((resolve, reject) => {
148
+ const id = this.nextMessageId++;
149
+ this.pendingMessages.set(id, { resolve, reject });
150
+ assert(this.worker);
151
+ if (transferables) {
152
+ this.worker.postMessage({ id, command }, transferables);
153
+ }
154
+ else {
155
+ this.worker.postMessage({ id, command });
156
+ }
157
+ });
158
+ }
159
+ }
160
+ /**
161
+ * Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
162
+ * function before starting any encoding task.
163
+ *
164
+ * Preferably, wrap the call in a condition to avoid overriding any native MP3 encoder:
165
+ *
166
+ * ```ts
167
+ * import { canEncodeAudio } from 'mediabunny';
168
+ * import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
169
+ *
170
+ * if (!(await canEncodeAudio('mp3'))) {
171
+ * registerMp3Encoder();
172
+ * }
173
+ * ```
174
+ *
175
+ * @public
176
+ */
177
+ const registerMp3Encoder = () => {
178
+ (0, mediabunny_1.registerEncoder)(Mp3Encoder);
179
+ };
180
+ exports.registerMp3Encoder = registerMp3Encoder;
181
+ function assert(x) {
182
+ if (!x) {
183
+ throw new Error('Assertion failed.');
184
+ }
185
+ }
@@ -0,0 +1,40 @@
1
+ /*!
2
+ * Copyright (c) 2025-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
+ export type WorkerCommand = {
9
+ type: 'init';
10
+ data: {
11
+ numberOfChannels: number;
12
+ sampleRate: number;
13
+ bitrate: number;
14
+ };
15
+ } | {
16
+ type: 'encode';
17
+ data: {
18
+ audioData: ArrayBuffer;
19
+ numberOfFrames: number;
20
+ };
21
+ } | {
22
+ type: 'flush';
23
+ };
24
+ export type WorkerResponseData = {
25
+ success: boolean;
26
+ } | {
27
+ encodedData: ArrayBuffer;
28
+ } | {
29
+ flushedData: ArrayBuffer;
30
+ };
31
+ export type WorkerResponse = {
32
+ id: number;
33
+ } & ({
34
+ success: true;
35
+ data: WorkerResponseData;
36
+ } | {
37
+ success: false;
38
+ error: unknown;
39
+ });
40
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/shared.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QACL,gBAAgB,EAAE,MAAM,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CACF,GAAG;IACH,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE;QACL,SAAS,EAAE,WAAW,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;KACvB,CAAC;CACF,GAAG;IACH,IAAI,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAChC,OAAO,EAAE,OAAO,CAAC;CACjB,GAAG;IACH,WAAW,EAAE,WAAW,CAAC;CACzB,GAAG;IACH,WAAW,EAAE,WAAW,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;CACX,GAAG,CAAC;IACJ,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,kBAAkB,CAAC;CACzB,GAAG;IACH,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CACf,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /*!
3
+ * Copyright (c) 2025-present, Vanilagy and contributors
4
+ *
5
+ * This Source Code Form is subject to the terms of the Mozilla Public
6
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mediabunny/mp3-encoder",
3
+ "author": "Vanilagy",
4
+ "version": "1.6.1",
5
+ "description": "MP3 encoder extension for Mediabunny, based on LAME.",
6
+ "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
7
+ "module": "./dist/bundles/mediabunny-mp3-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-mp3-encoder.mjs",
12
+ "require": "./dist/bundles/mediabunny-mp3-encoder.mjs"
13
+ },
14
+ "files": [
15
+ "README.md",
16
+ "package.json",
17
+ "LICENSE",
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "sideEffects": false,
22
+ "license": "MPL-2.0",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/Vanilagy/mediabunny.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/Vanilagy/mediabunny/issues"
29
+ },
30
+ "homepage": "https://mediabunny.dev/",
31
+ "funding": {
32
+ "type": "individual",
33
+ "url": "https://github.com/sponsors/Vanilagy"
34
+ },
35
+ "peerDependencies": {
36
+ "mediabunny": "^1.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/emscripten": "^1.40.1"
40
+ },
41
+ "keywords": [
42
+ "mp3",
43
+ "encoding",
44
+ "codec",
45
+ "mediabunny",
46
+ "lame",
47
+ "browser",
48
+ "wasm",
49
+ "polyfill"
50
+ ]
51
+ }