@mediabunny/ac3 0.1.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.
Files changed (37) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +103 -0
  3. package/dist/bundles/mediabunny-ac3.js +3829 -0
  4. package/dist/bundles/mediabunny-ac3.min.js +3510 -0
  5. package/dist/bundles/mediabunny-ac3.min.mjs +3509 -0
  6. package/dist/bundles/mediabunny-ac3.mjs +3792 -0
  7. package/dist/mediabunny-ac3.d.ts +20 -0
  8. package/dist/modules/build/ac3.d.ts +3 -0
  9. package/dist/modules/build/ac3.d.ts.map +1 -0
  10. package/dist/modules/build/ac3.js +0 -0
  11. package/dist/modules/src/codec.worker.d.ts +9 -0
  12. package/dist/modules/src/codec.worker.d.ts.map +1 -0
  13. package/dist/modules/src/codec.worker.js +269 -0
  14. package/dist/modules/src/decoder.d.ts +16 -0
  15. package/dist/modules/src/decoder.d.ts.map +1 -0
  16. package/dist/modules/src/decoder.js +61 -0
  17. package/dist/modules/src/encoder.d.ts +16 -0
  18. package/dist/modules/src/encoder.d.ts.map +1 -0
  19. package/dist/modules/src/encoder.js +151 -0
  20. package/dist/modules/src/index.d.ts +10 -0
  21. package/dist/modules/src/index.d.ts.map +1 -0
  22. package/dist/modules/src/index.js +22 -0
  23. package/dist/modules/src/shared.d.ts +96 -0
  24. package/dist/modules/src/shared.d.ts.map +1 -0
  25. package/dist/modules/src/shared.js +15 -0
  26. package/dist/modules/src/worker-client.d.ts +14 -0
  27. package/dist/modules/src/worker-client.d.ts.map +1 -0
  28. package/dist/modules/src/worker-client.js +61 -0
  29. package/dist/modules/tsconfig.tsbuildinfo +1 -0
  30. package/package.json +56 -0
  31. package/src/bridge.c +289 -0
  32. package/src/codec.worker.ts +306 -0
  33. package/src/decoder.ts +70 -0
  34. package/src/encoder.ts +191 -0
  35. package/src/index.ts +21 -0
  36. package/src/shared.ts +103 -0
  37. package/src/worker-client.ts +69 -0
package/src/encoder.ts ADDED
@@ -0,0 +1,191 @@
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 { sendCommand } from './worker-client';
17
+ import { assert } from './shared';
18
+ import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../../../shared/ac3-misc';
19
+
20
+ class Ac3Encoder extends CustomAudioEncoder {
21
+ private ctx = 0;
22
+ private encoderFrameSize = 0;
23
+ private sampleRate = 0;
24
+ private numberOfChannels = 0;
25
+ private chunkMetadata: EncodedAudioChunkMetadata = {};
26
+
27
+ // Accumulate interleaved f32 samples until we have a full frame
28
+ private pendingBuffer = new Float32Array(2 ** 16);
29
+ private pendingFrames = 0;
30
+ private nextSampleTimestampInSamples: number | null = null;
31
+ private nextPacketTimestampInSamples: number | null = null;
32
+
33
+ static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean {
34
+ const sampleRates = codec === 'eac3'
35
+ ? [...AC3_SAMPLE_RATES, ...EAC3_REDUCED_SAMPLE_RATES]
36
+ : AC3_SAMPLE_RATES;
37
+
38
+ return (codec === 'ac3' || codec === 'eac3')
39
+ && config.numberOfChannels >= 1
40
+ && config.numberOfChannels <= 8
41
+ && sampleRates.includes(config.sampleRate);
42
+ }
43
+
44
+ async init() {
45
+ assert(this.config.bitrate);
46
+ this.sampleRate = this.config.sampleRate;
47
+ this.numberOfChannels = this.config.numberOfChannels;
48
+
49
+ const result = await sendCommand({
50
+ type: 'init-encoder',
51
+ data: {
52
+ codec: this.codec,
53
+ numberOfChannels: this.config.numberOfChannels,
54
+ sampleRate: this.config.sampleRate,
55
+ bitrate: this.config.bitrate,
56
+ },
57
+ });
58
+
59
+ this.ctx = result.ctx;
60
+ this.encoderFrameSize = result.frameSize;
61
+
62
+ this.resetInternalState();
63
+ }
64
+
65
+ private resetInternalState() {
66
+ this.pendingFrames = 0;
67
+ this.nextSampleTimestampInSamples = null;
68
+ this.nextPacketTimestampInSamples = null;
69
+
70
+ this.chunkMetadata = {
71
+ decoderConfig: {
72
+ codec: this.codec === 'ac3' ? 'ac-3' : 'ec-3',
73
+ numberOfChannels: this.config.numberOfChannels,
74
+ sampleRate: this.config.sampleRate,
75
+ },
76
+ };
77
+ }
78
+
79
+ async encode(audioSample: AudioSample) {
80
+ if (this.nextSampleTimestampInSamples === null) {
81
+ this.nextSampleTimestampInSamples = Math.round(audioSample.timestamp * this.sampleRate);
82
+ this.nextPacketTimestampInSamples = this.nextSampleTimestampInSamples;
83
+ }
84
+
85
+ const channels = this.numberOfChannels;
86
+ const incomingFrames = audioSample.numberOfFrames;
87
+
88
+ // Extract interleaved f32 data
89
+ const totalBytes = audioSample.allocationSize({ format: 'f32', planeIndex: 0 });
90
+ const audioBytes = new Uint8Array(totalBytes);
91
+ audioSample.copyTo(audioBytes, { format: 'f32', planeIndex: 0 });
92
+ const incomingData = new Float32Array(audioBytes.buffer);
93
+
94
+ const requiredSamples = (this.pendingFrames + incomingFrames) * channels;
95
+ if (requiredSamples > this.pendingBuffer.length) {
96
+ let newSize = this.pendingBuffer.length;
97
+ while (newSize < requiredSamples) {
98
+ newSize *= 2;
99
+ }
100
+ const newBuffer = new Float32Array(newSize);
101
+ newBuffer.set(this.pendingBuffer.subarray(0, this.pendingFrames * channels));
102
+ this.pendingBuffer = newBuffer;
103
+ }
104
+ this.pendingBuffer.set(incomingData, this.pendingFrames * channels);
105
+ this.pendingFrames += incomingFrames;
106
+
107
+ while (this.pendingFrames >= this.encoderFrameSize) {
108
+ await this.encodeOneFrame();
109
+ }
110
+ }
111
+
112
+ async flush() {
113
+ // Pad remaining samples with silence to fill a full frame
114
+ if (this.pendingFrames > 0) {
115
+ const channels = this.numberOfChannels;
116
+ const frameSize = this.encoderFrameSize;
117
+ const usedSamples = this.pendingFrames * channels;
118
+ const frameSamples = frameSize * channels;
119
+
120
+ this.pendingBuffer.fill(0, usedSamples, frameSamples);
121
+ this.pendingFrames = frameSize;
122
+
123
+ await this.encodeOneFrame();
124
+ }
125
+
126
+ await sendCommand({ type: 'flush-encoder', data: { ctx: this.ctx } });
127
+
128
+ this.resetInternalState();
129
+ }
130
+
131
+ close() {
132
+ void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } });
133
+ }
134
+
135
+ private async encodeOneFrame() {
136
+ assert(this.nextSampleTimestampInSamples !== null);
137
+ assert(this.nextPacketTimestampInSamples !== null);
138
+
139
+ const channels = this.numberOfChannels;
140
+ const frameSize = this.encoderFrameSize;
141
+ const frameSamples = frameSize * channels;
142
+
143
+ const frameData = this.pendingBuffer.slice(0, frameSamples);
144
+
145
+ // Shift remaining using copyWithin
146
+ this.pendingFrames -= frameSize;
147
+ if (this.pendingFrames > 0) {
148
+ this.pendingBuffer.copyWithin(0, frameSamples, frameSamples + this.pendingFrames * channels);
149
+ }
150
+
151
+ const audioData = frameData.buffer;
152
+ const result = await sendCommand({
153
+ type: 'encode',
154
+ data: {
155
+ ctx: this.ctx,
156
+ audioData,
157
+ timestamp: this.nextSampleTimestampInSamples,
158
+ },
159
+ }, [audioData]);
160
+
161
+ this.nextSampleTimestampInSamples += frameSize;
162
+
163
+ // We always get exactly one packet because we encode the correct frame size
164
+ const packet = new EncodedPacket(
165
+ new Uint8Array(result.encodedData),
166
+ 'key',
167
+ this.nextPacketTimestampInSamples / this.sampleRate,
168
+ result.duration / this.sampleRate,
169
+ );
170
+
171
+ this.nextPacketTimestampInSamples += result.duration;
172
+
173
+ this.onPacket(
174
+ packet,
175
+ this.chunkMetadata,
176
+ );
177
+
178
+ this.chunkMetadata = {};
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Registers AC-3 and E-AC-3 encoders, which Mediabunny will then use automatically when applicable. Make sure to call
184
+ * this function before starting any encoding task.
185
+ *
186
+ * @group \@mediabunny/ac3
187
+ * @public
188
+ */
189
+ export const registerAc3Encoder = () => {
190
+ registerEncoder(Ac3Encoder);
191
+ };
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
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
+ const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
10
+ if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
11
+ console.error(
12
+ '[WARNING]\n@mediabunny/ac3 was loaded twice.'
13
+ + ' This will likely cause the encoder/decoder not to work correctly.'
14
+ + ' Check if multiple dependencies are importing different versions of @mediabunny/ac3,'
15
+ + ' or if something is being bundled incorrectly.',
16
+ );
17
+ }
18
+ (globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL] = true;
19
+
20
+ export { registerAc3Decoder } from './decoder';
21
+ export { registerAc3Encoder } from './encoder';
package/src/shared.ts ADDED
@@ -0,0 +1,103 @@
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
+ export type WorkerCommand = {
10
+ type: 'init-decoder';
11
+ data: {
12
+ codec: string;
13
+ };
14
+ } | {
15
+ type: 'decode';
16
+ data: {
17
+ ctx: number;
18
+ encodedData: ArrayBuffer;
19
+ timestamp: number;
20
+ };
21
+ } | {
22
+ type: 'flush-decoder';
23
+ data: {
24
+ ctx: number;
25
+ };
26
+ } | {
27
+ type: 'close-decoder';
28
+ data: {
29
+ ctx: number;
30
+ };
31
+ } | {
32
+ type: 'init-encoder';
33
+ data: {
34
+ codec: string;
35
+ numberOfChannels: number;
36
+ sampleRate: number;
37
+ bitrate: number;
38
+ };
39
+ } | {
40
+ type: 'encode';
41
+ data: {
42
+ ctx: number;
43
+ audioData: ArrayBuffer;
44
+ timestamp: number;
45
+ };
46
+ } | {
47
+ type: 'flush-encoder';
48
+ data: {
49
+ ctx: number;
50
+ };
51
+ } | {
52
+ type: 'close-encoder';
53
+ data: {
54
+ ctx: number;
55
+ };
56
+ };
57
+
58
+ export type WorkerResponseData = {
59
+ type: 'init-decoder';
60
+ ctx: number;
61
+ frameSize: number;
62
+ } | {
63
+ type: 'decode';
64
+ pcmData: ArrayBuffer;
65
+ format: AudioSampleFormat;
66
+ channels: number;
67
+ sampleRate: number;
68
+ sampleCount: number;
69
+ pts: number;
70
+ } | {
71
+ type: 'flush-decoder';
72
+ } | {
73
+ type: 'close-decoder';
74
+ } | {
75
+ type: 'init-encoder';
76
+ ctx: number;
77
+ frameSize: number;
78
+ } | {
79
+ type: 'encode';
80
+ encodedData: ArrayBuffer;
81
+ pts: number;
82
+ duration: number;
83
+ } | {
84
+ type: 'flush-encoder';
85
+ } | {
86
+ type: 'close-encoder';
87
+ };
88
+
89
+ export type WorkerResponse = {
90
+ id: number;
91
+ } & ({
92
+ success: true;
93
+ data: WorkerResponseData;
94
+ } | {
95
+ success: false;
96
+ error: unknown;
97
+ });
98
+
99
+ export function assert(x: unknown): asserts x {
100
+ if (!x) {
101
+ throw new Error('Assertion failed.');
102
+ }
103
+ }
@@ -0,0 +1,69 @@
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 { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseData } from './shared';
10
+ // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
11
+ import createWorker from './codec.worker';
12
+
13
+ let workerPromise: Promise<Worker> | null;
14
+ let nextMessageId = 0;
15
+ const pendingMessages = new Map<number, {
16
+ resolve: (value: WorkerResponseData) => void;
17
+ reject: (reason?: unknown) => void;
18
+ }>();
19
+
20
+ export const sendCommand = async <T extends string>(
21
+ command: WorkerCommand & { type: T },
22
+ transferables?: Transferable[],
23
+ ) => {
24
+ const worker = await ensureWorker();
25
+
26
+ return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
27
+ const id = nextMessageId++;
28
+ pendingMessages.set(id, {
29
+ resolve: resolve as (value: WorkerResponseData) => void,
30
+ reject,
31
+ });
32
+
33
+ if (transferables) {
34
+ worker.postMessage({ id, command }, transferables);
35
+ } else {
36
+ worker.postMessage({ id, command });
37
+ }
38
+ });
39
+ };
40
+
41
+ const ensureWorker = () => {
42
+ return workerPromise ??= (async () => {
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
44
+ const worker = (await createWorker()) as Worker;
45
+
46
+ const onMessage = (data: WorkerResponse) => {
47
+ const pending = pendingMessages.get(data.id);
48
+ assert(pending !== undefined);
49
+
50
+ pendingMessages.delete(data.id);
51
+ if (data.success) {
52
+ pending.resolve(data.data);
53
+ } else {
54
+ pending.reject(data.error);
55
+ }
56
+ };
57
+
58
+ if (worker.addEventListener) {
59
+ worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
60
+ } else {
61
+ const nodeWorker = worker as unknown as {
62
+ on: (event: string, listener: (data: never) => void) => void;
63
+ };
64
+ nodeWorker.on('message', onMessage);
65
+ }
66
+
67
+ return worker;
68
+ })();
69
+ };