26 |
27 |
28 | /**
29 | * Loads the `mp3.wasm` and returns the main class `Encoder`.
30 | * @module @etercast/mp3
31 | *
32 | * @func instantiate
33 | * @param {string|URL|Request} [wasm='mp3.wasm'] WASM file URL.
34 | * @returns Promise<class Encoder|Error>
35 | */
36 | export function instantiate(wasm = './mp3.wasm') {
37 | return fetch(wasm)
38 | .then((response) => response.arrayBuffer())
39 | .then((arrayBuffer) => WebAssembly.instantiate(arrayBuffer, {
40 | // TODO: We really don't need these imports because mp3.wasm
41 | // does not handle file descriptors.
42 | wasi_unstable: {
43 | fd_close() { console.log('fd_close') },
44 | fd_seek() { console.log('fd_seek') },
45 | fd_write() { console.log('fd_write') },
46 | proc_exit() { console.log('proc_exit') }
47 | }
48 | }))
49 | .then(({ instance }) => {
50 | // TODO: There's an extra export called _start that's not used
51 | // but is defined in WASI. Look for ways to remove this unnecessary
52 | // export.
53 | const {
54 | memory,
55 | mp3_create,
56 | mp3_init,
57 | mp3_encode,
58 | mp3_destroy
59 | } = instance.exports
60 |
61 | /**
62 | * Returns the error codes from the MP3 encoder.
63 | * @param {number} errorCode
64 | * @return {string}
65 | */
66 | function getEncoderError(errorCode) {
67 | switch (errorCode) {
68 | default:
69 | return `Unknown error: ${errorCode}`
70 | case -1:
71 | return 'Invalid input sample rate'
72 | case -2:
73 | return 'Invalid input channels (mono and stereo only)'
74 | case -3:
75 | return 'Invalid quality'
76 | case -4:
77 | return 'Error calling lame_init'
78 | case -5:
79 | return 'Error calling lame_init_params'
80 | case -6:
81 | return 'Error reallocating buffers'
82 | case -7:
83 | return 'Too much input samples'
84 | case -8:
85 | return 'Error calling lame_encode_buffer_ieee_float'
86 | case -9:
87 | return 'Error calling lame_encode_flush'
88 | case -10:
89 | return 'Invalid number of samples passed'
90 | case -11:
91 | return 'Invalid input samples'
92 | case -100:
93 | return 'Ok'
94 | case -101:
95 | return 'Error calling lame_encode_buffer_ieee_float: Buffer was too small'
96 | case -102:
97 | return 'Error calling lame_encode_buffer_ieee_float: malloc() problem'
98 | case -103:
99 | return 'Error calling lame_encode_buffer_ieee_float: lame_init_params not called'
100 | case -104:
101 | return 'Error calling lame_encode_buffer_ieee_float: Psycho acoustic problems'
102 | case -110:
103 | return 'Error calling lame_encode_buffer_ieee_float: No memory'
104 | case -111:
105 | return 'Error calling lame_encode_buffer_ieee_float: Bad bitrate'
106 | case -112:
107 | return 'Error calling lame_encode_buffer_ieee_float: Bad samplefreq'
108 | case -113:
109 | return 'Error calling lame_encode_buffer_ieee_float: Internal error'
110 | }
111 | }
112 |
113 | /**
114 | * Encoder states.
115 | * @readonly
116 | * @enum {string}
117 | */
118 | const EncoderState = {
119 | /** Indicates that the encoder is running and is capable of encode MP3 frames. */
120 | RUNNING: 'running',
121 | /** Indicates that the encoder was closed and no longer can encode MP3 frames. */
122 | CLOSED: 'closed',
123 | /** Indicates that something went wrong. */
124 | ERROR: 'error'
125 | }
126 |
127 | /**
128 | * Encoder mode.
129 | * @readonly
130 | * @enum {number}
131 | */
132 | const EncoderMode = {
133 | /** Constant bit-rate */
134 | CBR: 0,
135 | /** Variable bit-rate */
136 | VBR: 1
137 | }
138 |
139 | /**
140 | * MP3 encoder options
141 | * @typedef {Object} EncoderOptions
142 | * @property {number} sampleRate Input/output sample rate. Usually this is the value from an AudioContext.
143 | * @property {number} numChannels Number of input/output channels. MP3 supports 1 (mono) or 2 (stereo) channels
144 | * @property {number} quality Encoding quality (0 - lowest, 9 - highest).
145 | * In VBR (Variable Bit-Rate) this quality indicates an average kbps but in
146 | * CBR (Constant Bit-Rate) 0 is equal to 32kbps and 9 is equal to 320kbps.
147 | * @property {number} samples Number of samples that will be encoded each time `encode` is called.
148 | * @property {EncoderMode} mode Encoding mode (0 - CBR, 1 - VBR).
149 | */
150 |
151 | /**
152 | * Not exported by default in the module, you need
153 | * to call `instantiate` to retrieve this class.
154 | *
155 | * @example
156 | * import instantiate from '@etercast/mp3'
157 | *
158 | * const Encoder = await instantiate()
159 | * const encoder = new Encoder(encoderOptions)
160 | * encoder.encode(leftChannelData, rightChannelData)
161 | * encoder.close()
162 | */
163 | class Encoder {
164 | /**
165 | * Creates a new MP3 encoder. This is equivalent to calling
166 | * the constructor using the `new` keyword. It's useful
167 | * if you need a function that instantiates the Encoder.
168 | * @param {EncoderOptions} options
169 | * @example
170 | * import instantiate from '@etercast/mp3'
171 | *
172 | * const Encoder = await instantiate()
173 | * const encoder = Encoder.create(encoderOptions)
174 | * @returns {Encoder}
175 | */
176 | static create(options) {
177 | return new Encoder(options)
178 | }
179 |
180 | /**
181 | * Internally this calls the exported method `mp3_create` to
182 | * make WASM module create a new structure to hold all the LAME
183 | * encoding data. It also calls `mp3_init` to establish encoder
184 | * options like number of channels, quality, sampleRate, etc.
185 | * @param {EncoderOptions} options
186 | */
187 | constructor(options) {
188 | const {
189 | sampleRate,
190 | numChannels,
191 | quality,
192 | samples,
193 | mode,
194 | } = {
195 | sampleRate: 44100,
196 | numChannels: 1,
197 | quality: 9,
198 | samples: 2048,
199 | mode: 0,
200 | ...options
201 | }
202 | this._error = null
203 | this._state = EncoderState.RUNNING
204 | this._pointer = null
205 | const pointer = mp3_create()
206 | if (!pointer) {
207 | return this._throw(new Error('Cannot create mp3 encoder'))
208 | }
209 | this._pointer = pointer
210 | const internalRegisters = 10
211 | const internal = new Int32Array(memory.buffer, pointer, internalRegisters)
212 | this._internal = internal
213 | const errorCode = mp3_init(this._pointer, sampleRate, numChannels, quality, samples, mode)
214 | if (errorCode < 0) {
215 | return this._throw(new Error(getEncoderError(errorCode)))
216 | }
217 |
218 | const [, outputBufferMaxSize, , , inputSamples, , , inputBufferLeftPointer, inputBufferRightPointer, outputBufferPointer] = internal
219 | this._inputBufferLeft = new Float32Array(memory.buffer, inputBufferLeftPointer, inputSamples)
220 | this._inputBufferRight = new Float32Array(memory.buffer, inputBufferRightPointer, inputSamples)
221 | this._outputBuffer = new Uint8Array(memory.buffer, outputBufferPointer, outputBufferMaxSize)
222 | }
223 |
224 | /**
225 | * Encoder state
226 | * @type {EncoderState}
227 | */
228 | get state() {
229 | return this._state
230 | }
231 |
232 | /**
233 | * Error
234 | * @type {null|Error}
235 | */
236 | get error() {
237 | return this._error
238 | }
239 |
240 | /**
241 | * Current output buffer size.
242 | * @type {number}
243 | */
244 | get outputBufferSize() {
245 | return this._internal[0]
246 | }
247 |
248 | /**
249 | * Max. output buffer size.
250 | * @type {number}
251 | */
252 | get outputBufferMaxSize() {
253 | return this._internal[1]
254 | }
255 |
256 | /**
257 | * Output sample rate
258 | * @type {number}
259 | */
260 | get outputSampleRate() {
261 | return this._internal[2]
262 | }
263 |
264 | /**
265 | * Output quality
266 | * @type {number}
267 | */
268 | get outputQuality() {
269 | return this._internal[3]
270 | }
271 |
272 | /**
273 | * Input samples
274 | * @type {number}
275 | */
276 | get inputSamples() {
277 | return this._internal[4]
278 | }
279 |
280 | /**
281 | * Input sample rate
282 | * @type {number}
283 | */
284 | get inputSampleRate() {
285 | return this._internal[5]
286 | }
287 |
288 | /**
289 | * Input number of channels (1 - mono, 2 - stereo)
290 | * @type {number}
291 | */
292 | get inputChannels() {
293 | return this._internal[6]
294 | }
295 |
296 | /**
297 | * Indicates that the encoder is running.
298 | * @type {boolean}
299 | */
300 | get isRunning() {
301 | return this._state === EncoderState.RUNNING
302 | }
303 |
304 | /**
305 | * Indicates that the encoder is running.
306 | * @type {boolean}
307 | */
308 | get isClosed() {
309 | return this._state === EncoderState.CLOSED
310 | }
311 |
312 | /**
313 | * Throws an error
314 | * @private
315 | * @param {Error} error
316 | */
317 | _throw(error) {
318 | this._state = EncoderState.ERROR
319 | this._error = error
320 | throw error
321 | }
322 |
323 | /**
324 | * Encodes raw Float 32-bit audio data into MP3 frames. An MP3
325 | * frame consist of a header and payload, this makes MP3 streamable
326 | * and implies that you can merge two files easily by concatenating them.
327 | *
328 | * If you need to merge two channels then you should use a `ChannelMergeNode`
329 | * and then reencode the audio.
330 | *
331 | * @example
332 | * import instantiate from '@etercast/mp3'
333 | *
334 | * const Encoder = await instantiate()
335 | * const encoder = new Encoder({
336 | * sampleRate,
337 | * samples,
338 | * numChannels,
339 | * quality,
340 | * mode
341 | * })
342 | * encoder.encode(leftChannelData, rightChannelData)
343 | *
344 | * @param {Float32Array} left Left channel (mono)
345 | * @param {Float32Array} [right] Right channel (stereo)
346 | * @returns {Uint8Array} Returns a bunch of encoded frames
347 | */
348 | encode(left, right) {
349 | if (this._state !== EncoderState.RUNNING) {
350 | this._throw(new Error('Encoder already closed'))
351 | }
352 | let samples = 0
353 | if (left) {
354 | samples = left.length
355 | this._inputBufferLeft.set(left)
356 | if (right) {
357 | if (samples !== right.length) {
358 | this._throw(new Error('Encoder channels have different lengths'))
359 | }
360 | this._inputBufferRight.set(right)
361 | }
362 | }
363 | // Codifica el MP3.
364 | const errorCode = mp3_encode(this._pointer, samples)
365 | if (errorCode < 0) {
366 | this._throw(new Error(getEncoderError(errorCode)))
367 | }
368 | return this._outputBuffer.slice(0, this.outputBufferSize)
369 | }
370 |
371 | /**
372 | * Closes the encoder. After this method any call to `encode`
373 | * will throw an Error. If you need to append more data after
374 | * closing an encoder you can instantiate a new Encoder and then
375 | * concatenate all the new data with the old data.
376 | *
377 | * @example
378 | * import instantiate from '@etercast/mp3'
379 | *
380 | * const Encoder = await instantiate()
381 | * const encoder = new Encoder()
382 | *
383 | * // Do something with the encoder...
384 | *
385 | * encoder.close()
386 | *
387 | * // `encoder` is no longer usable and all the
388 | * // memory reserved is freed.
389 | */
390 | close() {
391 | if (this._state !== EncoderState.RUNNING) {
392 | this._throw(new Error('Encoder already closed'))
393 | }
394 |
395 | const errorCode = mp3_destroy(this._pointer)
396 | if (errorCode < 0) {
397 | this._throw(new Error(getEncoderError(errorCode)))
398 | }
399 | this._inputBufferLeft = null
400 | this._inputBufferRight = null
401 | this._outputBuffer = null
402 | this._pointer = null
403 | this._internal = null
404 | this._state = EncoderState.CLOSED
405 | }
406 | }
407 |
408 | return Encoder
409 | })
410 | }
411 |
412 | export default instantiate
413 |
414 |
415 |
416 |
417 |