Passed
Push — master ( d927b7...2e96db )
by Rafael S.
02:26
created

index.js (3 issues)

1
/*!
2
 * wavefile
3
 * Read & write wave files with 4, 8, 11, 12, 16, 20, 24, 32 & 64-bit data.
4
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
5
 * https://github.com/rochars/wavefile
6
 *
7
 */
8
9
/** @private */
10
const bitDepth_ = require("bitdepth");
11
/** @private */
12
const riffChunks_ = require("riff-chunks");
13
/** @private */
14
const imaadpcm_ = require("imaadpcm");
15
/** @private */
16
const alawmulaw_ = require("alawmulaw");
17
/** @private */
18
const byteData_ = require("byte-data");
19
/** @private */
20
const encodeBase64 = require("base64-arraybuffer").encode;
21
/** @private */
22
const uInt8_ = {"bits": 8};
23
/** @private */
24
const uInt16_ = {"bits": 16};
25
/** @private */
26
const uInt32_ = {"bits": 32};
27
/** @private */
28
const fourCC_ = {"bits": 32, "char": true};
29
/** @private */
30
const chr_ = {"bits": 8, "char": true};
31
32
/**
33
 * Class representing a wav file.
34
 */
35
class WaveFile {
36
37
    /**
38
     * @param {Uint8Array|Array<!number>} bytes A wave file buffer.
39
     * @throws {Error} If no "RIFF" chunk is found.
40
     * @throws {Error} If no "fmt " chunk is found.
41
     * @throws {Error} If no "fact" chunk is found and "fact" is needed.
42
     * @throws {Error} If no "data" chunk is found.
43
     */
44
    constructor(bytes) {
45
        /**
46
         * The container identifier.
47
         * Only "RIFF" and "RIFX" are supported.
48
         * @type {!string}
49
         */
50
        this.container = "";
51
        /**
52
         * @type {!number}
53
         */
54
        this.chunkSize = 0;
55
        /**
56
         * The format.
57
         * Always "WAVE".
58
         * @type {!string}
59
         */
60
        this.format = "";
61
        /**
62
         * The data of the "fmt" chunk.
63
         * @type {!Object<string, *>}
64
         */
65
        this.fmt = {
66
            /** @type {!string} */
67
            "chunkId": "",
68
            /** @type {!number} */
69
            "chunkSize": 0,
70
            /** @type {!number} */
71
            "audioFormat": 0,
72
            /** @type {!number} */
73
            "numChannels": 0,
74
            /** @type {!number} */
75
            "sampleRate": 0,
76
            /** @type {!number} */
77
            "byteRate": 0,
78
            /** @type {!number} */
79
            "blockAlign": 0,
80
            /** @type {!number} */
81
            "bitsPerSample": 0,
82
            /** @type {!number} */
83
            "cbSize": 0,
84
            /** @type {!number} */
85
            "validBitsPerSample": 0,
86
            /** @type {!number} */
87
            "dwChannelMask": 0,
88
            /**
89
             * 4 32-bit values representing a 128-bit ID
90
             * @type {!Array<number>} 
91
             */
92
            "subformat": []
93
        };
94
        /**
95
         * The data of the "fact" chunk.
96
         * @type {!Object<string, *>} 
97
         */
98
        this.fact = {
99
            /** @type {!string} */
100
            "chunkId": "",
101
            /** @type {!number} */
102
            "chunkSize": 0,
103
            /** @type {!number} */
104
            "dwSampleLength": 0
105
        };
106
        /**
107
         * The data of the "cue " chunk.
108
         * @type {!Object<string, *>} 
109
         */
110
        this.cue = {
111
            /** @type {!string} */
112
            "chunkId": "",
113
            /** @type {!number} */
114
            "chunkSize": 0,
115
            /** @type {!number} */
116
            "dwCuePoints": 0,
117
            /** @type {!Array<Object>} */
118
            "points": [],
119
        };
120
        /**
121
         * The data of the "bext" chunk.
122
         * @type {!Object<string, *>}
123
         */
124
        this.bext = {
125
            /** @type {!string} */
126
            "chunkId": "",
127
            /** @type {!number} */
128
            "chunkSize": 0,
129
            /** @type {!string} */
130
            "description": "", //256
131
            /** @type {!string} */
132
            "originator": "", //32
133
            /** @type {!string} */
134
            "originatorReference": "", //32
135
            /** @type {!string} */
136
            "originationDate": "", //10
137
            /** @type {!string} */
138
            "originationTime": "", //8
139
            /**
140
             * 2 32-bit values, timeReference high and low
141
             * @type {!Array<number>} 
142
             */
143
            "timeReference": [],
144
            /** @type {number} */
145
            "version": 0, //WORD
146
            /** @type {!string} */
147
            "UMID": "", // 64 chars
148
            /** @type {!number} */
149
            "loudnessValue": 0, //WORD
150
            /** @type {!number} */
151
            "loudnessRange": 0, //WORD
152
            /** @type {!number} */
153
            "maxTruePeakLevel": 0, //WORD
154
            /** @type {!number} */
155
            "maxMomentaryLoudness": 0, //WORD
156
            /** @type {!number} */
157
            "maxShortTermLoudness": 0, //WORD
158
            /** @type {!string} */
159
            "reserved": "", //180
160
            /** @type {!string} */
161
            "codingHistory": "" // string, unlimited
162
        };
163
        /**
164
         * The data of the "ds64" chunk.
165
         * Used only with RF64 files.
166
         * @type {!Object<string, *>}
167
         */
168
        this.ds64 = {
169
            /** @type {!string} */
170
            "chunkId": "",
171
            /** @type {!number} */
172
            "chunkSize": 0,
173
            /** @type {!number} */
174
            "riffSizeHigh": 0, // DWORD
175
            /** @type {!number} */
176
            "riffSizeLow": 0, // DWORD
177
            /** @type {!number} */
178
            "dataSizeHigh": 0, // DWORD
179
            /** @type {!number} */
180
            "dataSizeLow": 0, // DWORD
181
            /** @type {!number} */
182
            "originationTime": 0, // DWORD
183
            /** @type {!number} */
184
            "sampleCountHigh": 0, // DWORD
185
            /** @type {!number} */
186
            "sampleCountLow": 0, // DWORD
187
            /** @type {!number} */
188
            "tableLength": 0, // DWORD
189
            /** @type {!Array<number>} */
190
            "table": []
191
        };
192
        /**
193
         * The data of the "data" chunk.
194
         * @type {Object}
195
         */
196
        this.data = {
197
            /** @type {!string} */
198
            "chunkId": "",
199
            /** @type {!number} */
200
            "chunkSize": 0,
201
            /** @type {!Array<number>} */
202
            "samples": []
203
        };
204
        /**
205
         * If the data in data.samples is interleaved or not.
206
         * @type {!boolean}
207
         */
208
        this.isInterleaved = true;
209
        /**
210
         * @type {!string}
211
         */
212
        this.bitDepth = "0";
213
        /**
214
         * Audio formats.
215
         * Formats not listed here will be set to 65534
216
         * and treated as WAVE_FORMAT_EXTENSIBLE
217
         * @enum {!number}
218
         * @private
219
         */
220
        this.audioFormats_ = {
221
            "4": 17,
222
            "8": 1,
223
            "8a": 6,
224
            "8m": 7,
225
            "16": 1,
226
            "24": 1,
227
            "32": 1,
228
            "32f": 3,
229
            "40": 65534,
230
            "48": 65534,
231
            "64": 3
232
        };
233
        /**
234
         * @type {!number}
235
         * @private
236
         */
237
        this.head_ = 0;
238
        /**
239
         * If the "fact" chunk should be enforced or not.
240
         * @type {!boolean}
241
         * @private 
242
         */
243
        this.enforceFact_ = false;
244
        // Load a file from the buffer if one was passed
245
        // when creating the object
246
        if(bytes) {
247
            this.fromBuffer(bytes);
248
        }
249
    }
250
251
    /**
252
     * Set up a WaveFile object based on the arguments passed.
253
     * @param {!number} numChannels The number of channels
254
     *     (Integer numbers: 1 for mono, 2 stereo and so on).
255
     * @param {!number} sampleRate The sample rate.
256
     *     Integer numbers like 8000, 44100, 48000, 96000, 192000.
257
     * @param {!string} bitDepth The audio bit depth.
258
     *     One of "4", "8", "8a", "8m", "16", "24", "32", "32f", "64"
259
     *     or any value between "8" and "32".
260
     * @param {!Array<number>} samples Array of samples to be written.
261
     *     The samples must be in the correct range according to the
262
     *     bit depth.
263
     * @throws {Error} If any argument does not meet the criteria.
264
     */
265
    fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) {
266
        if (!options["container"]) {
267
            options["container"] = "RIFF";
268
        }
269
        // closest nuber of bytes if not / 8
270
        let numBytes = (((parseInt(bitDepth, 10) - 1) | 7) + 1) / 8;
271
        this.clearHeader_();
272
        this.bitDepth = bitDepth;
273
        // Normal PCM file header
274
        this.container = options["container"];
275
        this.chunkSize = 36 + samples.length * numBytes;
276
        this.format = "WAVE";
277
        this.fmt.chunkId = "fmt ";
278
        this.fmt.chunkSize = 16;
279
        this.fmt.byteRate = (numChannels * numBytes) * sampleRate;
280
        this.fmt.blockAlign = numChannels * numBytes;
281
        this.fmt.audioFormat = this.audioFormats_[bitDepth] ?
282
            this.audioFormats_[bitDepth] : 65534;
283
        this.fmt.numChannels = numChannels;
284
        this.fmt.sampleRate = sampleRate;
285
        this.fmt.bitsPerSample = parseInt(bitDepth, 10);
286
        this.data.chunkId = "data";
287
        this.data.samples = samples;
288
        // interleave the samples if they were passed de-interleaved
289
        if (samples.length > 0) {
290
            if (samples[0].constructor === Array) {
291
                this.isInterleaved = false;
292
                this.interleave();
293
            }
294
        }
295
        this.data.chunkSize = samples.length * numBytes;
296
        // IMA ADPCM header
297
        if (bitDepth == "4") {
298
            this.chunkSize = 44 + samples.length;
299
            this.fmt.chunkSize = 20;
300
            this.fmt.byteRate = 4055;
301
            this.fmt.blockAlign = 256;
302
            this.fmt.bitsPerSample = 4;
303
            this.fmt.cbSize = 2;
304
            this.fmt.validBitsPerSample = 505;
305
            this.fact.chunkId = "fact";
306
            this.fact.chunkSize = 4;
307
            this.fact.dwSampleLength = samples.length * 2;
308
            this.data.chunkSize = samples.length;
309
        }
310
        // A-Law and mu-Law header
311
        if (bitDepth == "8a" || bitDepth == "8m") {
312
            this.chunkSize = 44 + samples.length;
313
            this.fmt.chunkSize = 20;
314
            this.fmt.cbSize = 2;
315
            this.fmt.validBitsPerSample = 8;
316
            this.fact.chunkId = "fact";
317
            this.fact.chunkSize = 4;
318
            this.fact.dwSampleLength = samples.length;
319
        }
320
        // WAVE_FORMAT_EXTENSIBLE
321
        if (this.fmt.audioFormat == 65534) {
322
            this.chunkSize = 36 + 24 + samples.length * numBytes;
323
            this.fmt.chunkSize = 40;
324
            this.fmt.bitsPerSample = ((parseInt(bitDepth, 10) - 1) | 7) + 1;
325
            this.fmt.cbSize = 22;
326
            this.fmt.validBitsPerSample = parseInt(bitDepth, 10);
327
            this.fmt.dwChannelMask = 0;
328
            // subformat 128-bit GUID as 4 32-bit values
329
            // only supports uncompressed integer PCM samples
330
            this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
331
        }
332
        this.checkWriteInput_();
333
    }
334
335
    /**
336
     * Init a WaveFile object from a byte buffer.
337
     * @param {!Uint8Array|!Array<number>} bytes The buffer.
338
     * @throws {Error} If container is not RIFF or RIFX.
339
     * @throws {Error} If no "fmt " chunk is found.
340
     * @throws {Error} If no "fact" chunk is found and "fact" is needed.
341
     * @throws {Error} If no "data" chunk is found.
342
     */
343
    fromBuffer(bytes) {
344
        this.readRIFFChunk_(bytes);
345
        let bigEndian = this.container == "RIFX";
346
        let chunk = riffChunks_.read(bytes, bigEndian);
347
        this.readDs64Chunk_(chunk["subChunks"]);
348
        this.readFmtChunk_(chunk["subChunks"]);
349
        this.readFactChunk_(chunk["subChunks"]);
350
        this.readBextChunk_(chunk["subChunks"]);
351
        this.readCueChunk_(chunk["subChunks"]);
352
        this.readDataChunk_(chunk["subChunks"], {"be": bigEndian});
353
        this.bitDepthFromFmt_();
354
    }
355
356
    /**
357
     * Return a byte buffer representig the WaveFile object as a wav file.
358
     * The return value of this method can be written straight to disk.
359
     * @return {!Uint8Array} A .wav file.
360
     * @throws {Error} If any property of the object appears invalid.
361
     */
362
    toBuffer() {
363
        this.checkWriteInput_();
364
        this.assureInterleaved_();
365
        return this.createWaveFile_();
366
    }
367
368
    /**
369
     * Return a base64 string representig the WaveFile object as a wav file.
370
     * @return {string} A .wav file as a base64 string.
371
     * @throws {Error} If any property of the object appears invalid.
372
     */
373
    toBase64() {
374
        return encodeBase64(this.toBuffer());
375
    }
376
377
    /**
378
     * Return a base64 string representig the WaveFile object as a wav file.
379
     * The return value of this method can be used to load the audio in browsers.
380
     * @return {string} A .wav file as a DataURI.
381
     * @throws {Error} If any property of the object appears invalid.
382
     */
383
    toDataURI() {
384
        return "data:audio/wav;base64," + this.toBase64();
385
    }
386
387
    /**
388
     * Force a file as RIFF.
389
     */
390
    toRIFF() {
391
        if (this.container == "RF64") {
392
            this.fromScratch(
393
                this.fmt.numChannels,
394
                this.fmt.sampleRate,
395
                this.bitDepth,
396
                this.data.samples);
397
        } else {
398
            this.container = "RIFF";
399
            this.LEorBE_();
400
        }
401
    }
402
403
    /**
404
     * Force a file as RIFX.
405
     */
406
    toRIFX() {
407
        if (this.container == "RF64") {
408
            this.fromScratch(
409
                this.fmt.numChannels,
410
                this.fmt.sampleRate,
411
                this.bitDepth,
412
                this.data.samples,
413
                {"container": "RIFX"});
414
        } else {
415
            this.container = "RIFX";
416
            this.LEorBE_();
417
        }
418
    }
419
420
    /**
421
     * Change the bit depth of the samples.
422
     * @param {!string} bitDepth The new bit depth of the samples.
423
     *      One of "8" ... "32" (integers), "32f" or "64" (floats)
424
     * @param {!boolean} changeResolution A boolean indicating if the
425
     *      resolution of samples should be actually changed or not.
426
     * @throws {Error} If the bit depth is not valid.
427
     */
428
    toBitDepth(bitDepth, changeResolution=true) {
429
        let toBitDepth = bitDepth;
430
        let thisBitDepth = this.bitDepth;
431
        if (!changeResolution) {
432
            toBitDepth = this.realBitDepth_(bitDepth);
433
            thisBitDepth = this.realBitDepth_(this.bitDepth);
434
        }
435
        this.assureInterleaved_();
436
        bitDepth_.toBitDepth(this.data.samples, thisBitDepth, toBitDepth);
437
        this.fromScratch(
438
            this.fmt.numChannels,
439
            this.fmt.sampleRate,
440
            bitDepth,
441
            this.data.samples,
442
            {"container": this.correctContainer_()});
443
    }
444
445
    /**
446
     * Interleave multi-channel samples.
447
     */
448
    interleave() {
449
        if (!this.isInterleaved) {
450
            let finalSamples = [];
451
            let numChannels = this.data.samples[0].length;
452
            for (let i = 0; i < numChannels; i++) {
453
                for (let j = 0; j < this.data.samples.length; j++) {
454
                    finalSamples.push(this.data.samples[j][i]);
455
                }
456
            }
457
            this.data.samples = finalSamples;
458
            this.isInterleaved = true;    
459
        }
460
    }
461
462
    /**
463
     * De-interleave samples into multiple channels.
464
     */
465
    deInterleave() {
466
        if (this.isInterleaved) {
467
            let finalSamples = [];
468
            let i;
469
            for (i = 0; i < this.fmt.numChannels; i++) {
470
                finalSamples[i] = [];
471
            }
472
            i = 0;
0 ignored issues
show
Complexity Coding Style introduced by
You seem to be assigning a new value to the loop variable i here. Please check if this was indeed your intention. Even if it was, consider using another kind of loop instead.
Loading history...
473
            let j;
474
            while (i < this.data.samples.length) {
475
                for (j = 0; j < this.fmt.numChannels; j++) {
476
                    finalSamples[j].push(this.data.samples[i+j]);
477
                }
478
                i += j;
479
            }
480
            this.data.samples = finalSamples;
481
            this.isInterleaved = false;
482
        }
483
    }
484
485
    /**
486
     * Encode a 16-bit wave file as 4-bit IMA ADPCM.
487
     * @throws {Error} If sample rate is not 8000.
488
     * @throws {Error} If number of channels is not 1.
489
     */
490
    toIMAADPCM() {
491
        if (this.fmt.sampleRate != 8000) {
492
            throw new Error(
493
                "Only 8000 Hz files can be compressed as IMA-ADPCM.");
494
        } else if(this.fmt.numChannels != 1) {
0 ignored issues
show
Comparing this.fmt.numChannels to 1 using the != operator is not safe. Consider using !== instead.
Loading history...
495
            throw new Error(
496
                "Only mono files can be compressed as IMA-ADPCM.");
497
        } else {
498
            this.assure16Bit_();
499
            this.fromScratch(
500
                this.fmt.numChannels,
501
                this.fmt.sampleRate,
502
                "4",
503
                imaadpcm_.encode(this.data.samples),
504
                {"container": this.correctContainer_()});
505
        }
506
    }
507
508
    /**
509
     * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
510
     * @param {!string} bitDepth The new bit depth of the samples.
511
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
512
     *      Optional. Default is 16.
513
     */
514
    fromIMAADPCM(bitDepth="16") {
515
        this.fromScratch(
516
            this.fmt.numChannels,
517
            this.fmt.sampleRate,
518
            "16",
519
            imaadpcm_.decode(this.data.samples, this.fmt.blockAlign),
520
            {"container": this.correctContainer_()});
521
        this.toBitDepth(bitDepth);
522
    }
523
524
    /**
525
     * Encode 16-bit wave file as 8-bit A-Law.
526
     */
527
    toALaw() {
528
        this.assure16Bit_();
529
        this.assureInterleaved_();
530
        this.fromScratch(
531
            this.fmt.numChannels,
532
            this.fmt.sampleRate,
533
            "8a",
534
            alawmulaw_.alaw.encode(this.data.samples),
535
            {"container": this.correctContainer_()});
536
    }
537
538
    /**
539
     * Decode a 8-bit A-Law wave file into a 16-bit wave file.
540
     * @param {!string} bitDepth The new bit depth of the samples.
541
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
542
     *      Optional. Default is 16.
543
     */
544
    fromALaw(bitDepth="16") {
545
        this.fromScratch(
546
            this.fmt.numChannels,
547
            this.fmt.sampleRate,
548
            "16",
549
            alawmulaw_.alaw.decode(this.data.samples),
550
            {"container": this.correctContainer_()});
551
        this.toBitDepth(bitDepth);
552
    }
553
554
    /**
555
     * Encode 16-bit wave file as 8-bit mu-Law.
556
     */
557
    toMuLaw() {
558
        this.assure16Bit_();
559
        this.assureInterleaved_();
560
        this.fromScratch(
561
            this.fmt.numChannels,
562
            this.fmt.sampleRate,
563
            "8m",
564
            alawmulaw_.mulaw.encode(this.data.samples),
565
            {"container": this.correctContainer_()});
566
    }
567
568
    /**
569
     * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
570
     * @param {!string} bitDepth The new bit depth of the samples.
571
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
572
     *      Optional. Default is 16.
573
     */
574
    fromMuLaw(bitDepth="16") {
575
        this.fromScratch(
576
            this.fmt.numChannels,
577
            this.fmt.sampleRate,
578
            "16",
579
            alawmulaw_.mulaw.decode(this.data.samples),
580
            {"container": this.correctContainer_()});
581
        this.toBitDepth(bitDepth);
582
    }
583
584
    /**
585
     * Return the closest greater number of bits for a number of bits that
586
     * do not fill a full sequence of bytes.
587
     * @param {!string} bitDepth The bit depth.
588
     * @return {!string}
589
     */
590
    realBitDepth_(bitDepth) {
591
        if (bitDepth != "32f") {
592
            bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1).toString();
593
        }
594
        return bitDepth;
595
    }
596
597
    /**
598
     * Validate the input for wav writing.
599
     * @throws {Error} If any property of the object appears invalid.
600
     * @private
601
     */
602
    checkWriteInput_() {
603
        this.validateBitDepth_();
604
        this.validateNumChannels_();
605
        this.validateSampleRate_();
606
    }
607
608
    /**
609
     * Validate the bit depth.
610
     * @return {!boolean} True is the bit depth is valid.
611
     * @throws {Error} If bit depth is invalid.
612
     * @private
613
     */
614
    validateBitDepth_() {
615
        if (!this.audioFormats_[this.bitDepth]) {
616
            if (parseInt(this.bitDepth, 10) > 8 &&
617
                    parseInt(this.bitDepth, 10) < 54) {
618
                return true;
619
            }
620
            throw new Error("Invalid bit depth.");
621
        }
622
        return true;
623
    }
624
625
    /**
626
     * Validate the number of channels.
627
     * @return {!boolean} True is the number of channels is valid.
628
     * @throws {Error} If the number of channels is invalid.
629
     * @private
630
     */
631
    validateNumChannels_() {
632
        let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
633
        if (this.fmt.numChannels < 1 || blockAlign > 65535) {
634
            throw new Error("Invalid number of channels.");
635
        }
636
        return true;
637
    }
638
639
    /**
640
     * Validate the sample rate value.
641
     * @return {!boolean} True is the sample rate is valid.
642
     * @throws {Error} If the sample rate is invalid.
643
     * @private
644
     */
645
    validateSampleRate_() {
646
        let byteRate = this.fmt.numChannels *
647
            (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
648
        if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
649
            throw new Error("Invalid sample rate.");
650
        }
651
        return true;
652
    }
653
654
    /**
655
     * Reset attributes that should emptied when a file is
656
     * created with the fromScratch() method.
657
     * @private
658
     */
659
    clearHeader_() {
660
        this.fmt.cbSize = 0;
661
        this.fmt.validBitsPerSample = 0;
662
        this.fact.chunkId = "";
663
        this.ds64.chunkId = "";
664
    }
665
666
    /**
667
     * Make the file 16-bit if it is not.
668
     * @private
669
     */
670
    assure16Bit_() {
671
        this.assureUncompressed_();
672
        if (this.bitDepth != "16") {
673
            this.toBitDepth("16");
674
        }
675
    }
676
677
    /**
678
     * Uncompress the samples in case of a compressed file.
679
     * @private
680
     */
681
    assureUncompressed_() {
682
        if (this.bitDepth == "8a") {
683
            this.fromALaw();
684
        } else if(this.bitDepth == "8m") {
685
            this.fromMuLaw();
686
        } else if (this.bitDepth == "4") {
687
            this.fromIMAADPCM();
688
        }
689
    }
690
691
    /**
692
     * Interleave the samples in case they are de-Interleaved.
693
     * @private
694
     */
695
    assureInterleaved_() {
696
        if (!this.isInterleaved) {
697
            this.interleave();
698
        }
699
    }
700
701
    /**
702
     * Set up to work wih big-endian or little-endian files.
703
     * The types used are changed to LE or BE. If the
704
     * the file is big-endian (RIFX), true is returned.
705
     * @return {!boolean} True if the file is RIFX.
706
     * @private
707
     */
708
    LEorBE_() {
709
        let bigEndian = this.container === "RIFX";
710
        uInt8_["be"] = bigEndian;
711
        uInt16_["be"] = bigEndian;
712
        uInt32_["be"] = bigEndian;
713
        return bigEndian;
714
    }
715
716
    /**
717
     * Find a chunk by its fourCC_ in a array of RIFF chunks.
718
     * @param {!Array<!Object>} chunks The wav file chunks.
719
     * @param {!string} chunkId The chunk fourCC_.
720
     * @return {Object|null}
721
     * @private
722
     */
723
    findChunk_(chunks, chunkId) {
724
        for (let i = 0; i<chunks.length; i++) {
725
            if (chunks[i]["chunkId"] == chunkId) {
726
                return chunks[i];
727
            }
728
        }
729
        return null;
730
    }
731
732
    /**
733
     * Read the RIFF chunk a wave file.
734
     * @param {!Uint8Array|!Array<number>} bytes A wav buffer.
735
     * @throws {Error} If no "RIFF" chunk is found.
736
     * @private
737
     */
738
    readRIFFChunk_(bytes) {
739
        this.container = byteData_.unpack(
740
            bytes.slice(0, 4), fourCC_);
741
        if (["RIFF", "RIFX", "RF64"].indexOf(this.container) === -1) {
742
            throw Error("Not a supported format.");
743
        }
744
        this.LEorBE_();
745
        this.chunkSize = byteData_.unpack(bytes.slice(4, 8), uInt32_);
746
        this.format = byteData_.unpack(
747
            bytes.slice(8, 12), fourCC_);
748
        if (this.format != "WAVE") {
749
            throw Error("Could not find the 'WAVE' format identifier");
750
        }
751
    }
752
753
    /**
754
     * Read the "fmt " chunk of a wave file.
755
     * @param {!Array<!Object>} chunks The wav file chunks.
756
     * @throws {Error} If no "fmt " chunk is found.
757
     * @private
758
     */
759
    readFmtChunk_(chunks) {
760
        let chunk = this.findChunk_(chunks, "fmt ");
761
        if (chunk) {
762
            let chunkData = chunk["chunkData"];
763
            this.fmt.chunkId = "fmt ";
764
            this.fmt.chunkSize = chunk["chunkSize"];
765
            this.fmt.audioFormat = byteData_.unpack(
766
                chunkData.slice(0, 2), uInt16_);
767
            this.fmt.numChannels = byteData_.unpack(
768
                chunkData.slice(2, 4), uInt16_);
769
            this.fmt.sampleRate = byteData_.unpack(
770
                chunkData.slice(4, 8), uInt32_);
771
            this.fmt.byteRate = byteData_.unpack(
772
                chunkData.slice(8, 12), uInt32_);
773
            this.fmt.blockAlign = byteData_.unpack(
774
                chunkData.slice(12, 14), uInt16_);
775
            this.fmt.bitsPerSample = byteData_.unpack(
776
                    chunkData.slice(14, 16), uInt16_);
777
            this.readFmtExtension_(chunk);
778
        } else {
779
            throw Error("Could not find the 'fmt ' chunk");
780
        }
781
    }
782
783
    /**
784
     * Read the "fmt " chunk extension.
785
     * @param {!Object<string, *>} chunk The "fmt " chunk.
786
     * @private
787
     */
788
    readFmtExtension_(chunk) {
789
        let chunkData = chunk["chunkData"];
790
        if (this.fmt.chunkSize > 16) {
791
            this.fmt.cbSize = byteData_.unpack(
792
                chunkData.slice(16, 18), uInt16_);
793
            if (this.fmt.chunkSize > 18) {
794
                this.fmt.validBitsPerSample = byteData_.unpack(
795
                    chunkData.slice(18, 20), uInt16_);
796
                if (this.fmt.chunkSize > 20) {
797
                    this.fmt.dwChannelMask = byteData_.unpack(
798
                        chunkData.slice(20, 24), uInt32_);
799
                    this.fmt.subformat = [
800
                        byteData_.unpack(
801
                            chunkData.slice(24, 28), uInt32_),
802
                        byteData_.unpack(
803
                            chunkData.slice(28, 32), uInt32_),
804
                        byteData_.unpack(
805
                            chunkData.slice(32, 36), uInt32_),
806
                        byteData_.unpack(
807
                            chunkData.slice(36, 40), uInt32_)];
808
                }
809
            }
810
        }
811
    }
812
813
    /**
814
     * Read the "fact" chunk of a wav file.
815
     * @param {!Array<Object>} chunks The wav file chunks.
816
     * @throws {Error} If no "fact" chunk is found.
817
     * @private
818
     */
819
    readFactChunk_(chunks) {
820
        let chunk = this.findChunk_(chunks, "fact");
821
        if (chunk) {
822
            this.fact.chunkId = "fact";
823
            this.fact.chunkSize = chunk["chunkSize"];
824
            this.fact.dwSampleLength = byteData_.unpack(
825
                chunk["chunkData"].slice(0, 4), uInt32_);
826
        } else if (this.enforceFact_) {
827
            throw Error("Could not find the 'fact' chunk");
828
        }
829
    }
830
831
    /**
832
     * Read the "cue " chunk of a wave file.
833
     * @param {!Array<Object>} chunks The RIFF file chunks.
834
     * @private
835
     */
836
    readCueChunk_(chunks) {
837
        let chunk = this.findChunk_(chunks, "cue ");
838
        if (chunk) {
839
            let chunkData = chunk["chunkData"];
840
            this.cue.chunkId = "cue ";
841
            this.cue.chunkSize = chunk["chunkSize"];
842
            this.cue.dwCuePoints = byteData_.unpack(
843
                chunkData.slice(0, 4), uInt32_);
844
            for (let i=0; i<this.cue.dwCuePoints; i++) {
845
                let offset = 4 + (i * 24);
846
                this.cue.points.push({
847
                    "dwName": byteData_.unpack(
848
                        chunkData.slice(offset, offset + 4), uInt32_),
849
                    "dwPosition": byteData_.unpack(
850
                        chunkData.slice(offset + 4, offset + 8), uInt32_),
851
                    "fccChunk": byteData_.unpack(
852
                        chunkData.slice(offset + 8, offset + 12), fourCC_),
853
                    "dwChunkStart": byteData_.unpack(
854
                        chunkData.slice(offset + 12, offset + 16), uInt32_),
855
                    "dwBlockStart": byteData_.unpack(
856
                        chunkData.slice(offset + 16, offset + 20), uInt32_),
857
                    "dwSampleOffset": byteData_.unpack(
858
                        chunkData.slice(offset + 20, offset + 24), uInt32_),
859
                });
860
            }
861
        }
862
    }
863
864
    /**
865
     * Read the "data" chunk of a wave file.
866
     * @param {!Array<Object>} chunks The RIFF file chunks.
867
     * @param {!Object<string, *>} options The type definition.
868
     * @throws {Error} If no "data" chunk is found.
869
     * @private
870
     */
871
    readDataChunk_(chunks, options) {
872
        let chunk = this.findChunk_(chunks, "data");
873
        if (chunk) {
874
            this.data.chunkId = "data";
875
            this.data.chunkSize = chunk["chunkSize"];
876
            this.samplesFromBytes_(chunk["chunkData"], options);
877
        } else {
878
            throw Error("Could not find the 'data' chunk");
879
        }
880
    }
881
882
    /**
883
     * Read the "bext" chunk of a wav file.
884
     * @param {!Array<Object>} chunks The wav file chunks.
885
     * @private
886
     */
887
    readBextChunk_(chunks) {
888
        let chunk = this.findChunk_(chunks, "bext");
889
        if (chunk) {
890
            this.head_ = 0;
891
            let chunkData = chunk["chunkData"];
892
            this.bext = {
893
                "chunkId": "bext",
894
                "chunkSize": chunkData.length,
895
                "description": this.readString_(chunkData, 256),
896
                "originator": this.readString_(chunkData, 32),
897
                "originatorReference": this.readString_(chunkData, 32),
898
                "originationDate": this.readString_(chunkData, 10),
899
                "originationTime": this.readString_(chunkData, 8),
900
                "timeReference": [
901
                        this.readFromChunk_(chunkData, uInt32_),
902
                        this.readFromChunk_(chunkData, uInt32_)], 
903
                "version": this.readFromChunk_(
904
                    chunkData, uInt16_),
905
                "UMID": this.readString_(chunkData, 64),
906
                "loudnessValue": this.readFromChunk_(
907
                    chunkData, uInt16_),
908
                "loudnessRange": this.readFromChunk_(
909
                    chunkData, uInt16_),
910
                "maxTruePeakLevel": this.readFromChunk_(
911
                    chunkData, uInt16_),
912
                "maxMomentaryLoudness": this.readFromChunk_(
913
                    chunkData, uInt16_),
914
                "maxShortTermLoudness": this.readFromChunk_(
915
                    chunkData, uInt16_),
916
                "reserved": this.readString_(chunkData, 180),
917
                "codingHistory": this.readString_(
918
                    chunkData, chunkData.length - 602),
919
            };
920
        }
921
    }
922
923
    /**
924
     * Read the "fmt " chunk of a wave file.
925
     * @param {!Array<Object>} chunks The wav file chunks.
926
     * @throws {Error} If no "fmt " chunk is found.
927
     * @private
928
     */
929
    readDs64Chunk_(chunks) {
930
        let chunk = this.findChunk_(chunks, "ds64");
931
        if (chunk) {
932
            let chunkData = chunk["chunkData"];
933
            this.ds64.chunkId = chunk["chunkId"];
934
            this.ds64.chunkSize = chunk["chunkSize"];
935
            this.ds64.riffSizeHigh = byteData_.unpack(
936
                chunkData.slice(0, 4), uInt32_);
937
            this.ds64.riffSizeLow = byteData_.unpack(
938
                chunkData.slice(4, 8), uInt32_);
939
            this.ds64.dataSizeHigh = byteData_.unpack(
940
                chunkData.slice(8, 12), uInt32_);
941
            this.ds64.dataSizeLow = byteData_.unpack(
942
                chunkData.slice(12, 16), uInt32_);
943
            this.ds64.originationTime = byteData_.unpack(
944
                chunkData.slice(16, 20), uInt32_);
945
            this.ds64.sampleCountHigh = byteData_.unpack(
946
                chunkData.slice(20, 24), uInt32_);
947
            this.ds64.sampleCountLow = byteData_.unpack(
948
                chunkData.slice(24, 28), uInt32_);
949
            //if (this.ds64.chunkSize > 28) {
950
            //    this.ds64.tableLength = byteData_.unpack(
951
            //        chunkData.slice(28, 32), uInt32_);
952
            //    this.ds64.table = chunkData.slice(32, 32 + this.ds64.tableLength);    
953
            //}
954
        } else {
955
            if (this.container == "RF64") {
956
                throw Error("Could not find the 'ds64' chunk");    
957
            }
958
        }
959
    }
960
961
    /**
962
     * Read bytes as a string from a RIFF chunk.
963
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
964
     * @param {!number} maxSize the max size of the string.
965
     * @return {!string} The string.
966
     * @private
967
     */
968
    readString_(bytes, maxSize) {
969
        let str = "";
970
        for (let i=0; i<maxSize; i++) {
971
            str += byteData_.unpack([bytes[this.head_]], chr_);
972
            this.head_++;
973
        }
974
        return str;
975
    }
976
977
    /**
978
     * Read a number from a chunk.
979
     * @param {!Array<number>|!Uint8Array} bytes The chunk bytes.
980
     * @param {!Object} bdType The type definition.
981
     * @return {!number} The number.
982
     * @private
983
     */
984
    readFromChunk_(bytes, bdType) {
985
        let size = bdType["bits"] / 8;
986
        let value = byteData_.unpack(
987
            bytes.slice(this.head_, this.head_ + size), bdType);
988
        this.head_ += size;
989
        return value;
990
    }
991
992
    /**
993
     * Write a variable size string as bytes. If the string is smaller
994
     * than the max size the output array is filled with 0s.
995
     * @param {!string} str The string to be written as bytes.
996
     * @param {!number} maxSize the max size of the string.
997
     * @return {!Array<number>} The bytes.
998
     * @private
999
     */
1000
    writeString_(str, maxSize) {
1001
        let bytes = byteData_.packArray(str, chr_);
1002
        for (let i=bytes.length; i<maxSize; i++) {
1003
            bytes.push(0);
1004
        }
1005
        return bytes;
1006
    }
1007
1008
    /**
1009
     * Turn the samples to bytes.
1010
     * @param {!Object<string, *>} options Type options.
1011
     * @return {!Array<number>} The bytes.
1012
     * @private
1013
     */
1014
    samplesToBytes_(options) {
1015
        options["bits"] = this.fmt.bitsPerSample == 4 ?
1016
            8 : this.fmt.bitsPerSample;
1017
        options["signed"] = options["bits"] == 8 ? false : true;
1018
        options["float"] = (this.fmt.audioFormat == 3  ||
1019
                this.fmt.bitsPerSample == 64) ? true : false;
1020
        let bytes = byteData_.packArray(
1021
            this.data.samples, options);
1022
        if (bytes.length % 2) {
1023
            bytes.push(0);
1024
        }
1025
        return bytes;
1026
    }
1027
1028
    /**
1029
     * Turn bytes to samples and load them in the data.samples property.
1030
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1031
     * @param {!Object<string, *>} options The type definition.
1032
     * @private
1033
     */
1034
    samplesFromBytes_(bytes, options) {
1035
        options["bits"] = this.fmt.bitsPerSample == 4 ?
1036
            8 : this.fmt.bitsPerSample;
1037
        options["signed"] = options["bits"] == 8 ? false : true;
1038
        options["float"] = (this.fmt.audioFormat == 3 || 
1039
            this.fmt.bitsPerSample == 64) ? true : false;
1040
        this.data.samples = byteData_.unpackArray(
1041
            bytes, options);
1042
    }
1043
1044
    /**
1045
     * Return the bytes of the "bext" chunk.
1046
     * @return {!Array<number>} The "bext" chunk bytes.
1047
     * @private
1048
     */
1049
    getBextBytes_() {
1050
        if (this.bext.chunkId) {
1051
            return [].concat(
1052
                byteData_.pack(this.bext.chunkId, fourCC_),
1053
                byteData_.pack(this.bext.chunkSize, uInt32_),
1054
                this.writeString_(this.bext.description, 256),
1055
                this.writeString_(this.bext.originator, 32),
1056
                this.writeString_(this.bext.originatorReference, 32),
1057
                this.writeString_(this.bext.originationDate, 10),
1058
                this.writeString_(this.bext.originationTime, 8),
1059
                byteData_.pack(this.bext.timeReference[0], uInt32_),
1060
                byteData_.pack(this.bext.timeReference[1], uInt32_),
1061
                byteData_.pack(this.bext.version, uInt16_),
1062
                this.writeString_(this.bext.UMID, 64),
1063
                byteData_.pack(this.bext.loudnessValue, uInt16_),
1064
                byteData_.pack(this.bext.loudnessRange, uInt16_),
1065
                byteData_.pack(this.bext.maxTruePeakLevel, uInt16_),
1066
                byteData_.pack(this.bext.maxMomentaryLoudness, uInt16_),
1067
                byteData_.pack(this.bext.maxShortTermLoudness, uInt16_),
1068
                this.writeString_(this.bext.reserved, 180),
1069
                this.writeString_(
1070
                    this.bext.codingHistory,
1071
                    this.bext.chunkSize - 602));
1072
        }
1073
        return [];
1074
    }
1075
1076
    /**
1077
     * Return the bytes of the "ds64" chunk.
1078
     * @return {!Array<number>} The "ds64" chunk bytes.
1079
     * @private
1080
     */
1081
    getDs64Bytes_() {
1082
        let ds64Bytes = [];
1083
        if (this.ds64["chunkId"]) {
1084
            ds64Bytes = ds64Bytes.concat(
1085
                byteData_.pack(this.ds64.chunkId, fourCC_),
1086
                byteData_.pack(this.ds64.chunkSize, uInt32_),
1087
                byteData_.pack(this.ds64.riffSizeHigh, uInt32_),
1088
                byteData_.pack(this.ds64.riffSizeLow, uInt32_),
1089
                byteData_.pack(this.ds64.dataSizeHigh, uInt32_),
1090
                byteData_.pack(this.ds64.dataSizeLow, uInt32_),
1091
                byteData_.pack(this.ds64.originationTime, uInt32_),
1092
                byteData_.pack(this.ds64.sampleCountHigh, uInt32_),
1093
                byteData_.pack(this.ds64.sampleCountLow, uInt32_));          
1094
        }
1095
        //if (this.ds64.tableLength) {
1096
        //    ds64Bytes = ds64Bytes.concat(
1097
        //        byteData_.pack(this.ds64.tableLength, uInt32_),
1098
        //        this.ds64.table);
1099
        //}
1100
        return ds64Bytes;
1101
    }
1102
1103
    /**
1104
     * Return the bytes of the "cue " chunk.
1105
     * @return {!Array<number>} The "cue " chunk bytes.
1106
     * @private
1107
     */
1108
    getCueBytes_() {
1109
        if (this.cue.chunkId) {
1110
            return [].concat(
1111
                byteData_.pack(this.cue.chunkId, fourCC_),
1112
                byteData_.pack(this.cue.chunkSize, uInt32_),
1113
                byteData_.pack(this.cue.dwCuePoints, uInt32_),
1114
                this.getCuePointsBytes_());
1115
        }
1116
        return [];
1117
    }
1118
1119
    /**
1120
     * Return the bytes of the "cue " points.
1121
     * @return {!Array<number>} The "cue " points as an array of bytes.
1122
     * @private
1123
     */
1124
    getCuePointsBytes_() {
1125
        let points = [];
1126
        for (let i=0; i<this.cue.dwCuePoints; i++) {
1127
            points = points.concat(
1128
                    byteData_.pack(this.cue.points[i]["dwName"], uInt32_),
1129
                    byteData_.pack(this.cue.points[i]["dwPosition"], uInt32_),
1130
                    byteData_.pack(this.cue.points[i]["fccChunk"], fourCC_),
1131
                    byteData_.pack(this.cue.points[i]["dwChunkStart"], uInt32_),
1132
                    byteData_.pack(this.cue.points[i]["dwBlockStart"], uInt32_),
1133
                    byteData_.pack(this.cue.points[i]["dwSampleOffset"], uInt32_)
1134
                );
1135
        }
1136
        return points;
1137
    }
1138
1139
    /**
1140
     * Return the bytes of the "fact" chunk.
1141
     * @return {!Array<number>} The "fact" chunk bytes.
1142
     * @private
1143
     */
1144
    getFactBytes_() {
1145
        if (this.fact.chunkId) {
1146
            return [].concat(
1147
                byteData_.pack(this.fact.chunkId, fourCC_),
1148
                byteData_.pack(this.fact.chunkSize, uInt32_),
1149
                byteData_.pack(this.fact.dwSampleLength, uInt32_));
1150
        }
1151
        return [];
1152
    }
1153
1154
    /**
1155
     * Return the bytes of the "fmt " chunk.
1156
     * @return {!Array<number>} The "fmt" chunk bytes.
1157
     * @private
1158
     * @throws {Error} if no "fmt " chunk is present.
1159
     */
1160
    getFmtBytes_() {
1161
        if (this.fmt.chunkId) {
1162
            return [].concat(
1163
                byteData_.pack(this.fmt.chunkId, fourCC_),
1164
                byteData_.pack(this.fmt.chunkSize, uInt32_),
1165
                byteData_.pack(this.fmt.audioFormat, uInt16_),
1166
                byteData_.pack(this.fmt.numChannels, uInt16_),
1167
                byteData_.pack(this.fmt.sampleRate, uInt32_),
1168
                byteData_.pack(this.fmt.byteRate, uInt32_),
1169
                byteData_.pack(this.fmt.blockAlign, uInt16_),
1170
                byteData_.pack(this.fmt.bitsPerSample, uInt16_),
1171
                this.getFmtExtensionBytes_()
1172
            );
1173
        } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
1174
            throw Error("Could not find the 'fmt ' chunk");
1175
        }
1176
    }
1177
1178
    /**
1179
     * Return the bytes of the fmt extension fields.
1180
     * @return {!Array<number>} The fmt extension bytes.
1181
     * @private
1182
     */
1183
    getFmtExtensionBytes_() {
1184
        let extension = [];
1185
        if (this.fmt.chunkSize > 16) {
1186
            extension = extension.concat(
1187
                byteData_.pack(this.fmt.cbSize, uInt16_));
1188
        }
1189
        if (this.fmt.chunkSize > 18) {
1190
            extension = extension.concat(
1191
                byteData_.pack(this.fmt.validBitsPerSample, uInt16_));
1192
        }
1193
        if (this.fmt.chunkSize > 20) {
1194
            extension = extension.concat(
1195
                byteData_.pack(this.fmt.dwChannelMask, uInt32_));
1196
        }
1197
        if (this.fmt.chunkSize > 24) {
1198
            extension = extension.concat(
1199
                byteData_.pack(this.fmt.subformat[0], uInt32_),
1200
                byteData_.pack(this.fmt.subformat[1], uInt32_),
1201
                byteData_.pack(this.fmt.subformat[2], uInt32_),
1202
                byteData_.pack(this.fmt.subformat[3], uInt32_));
1203
        }
1204
        return extension;
1205
    }
1206
1207
    /**
1208
     * Return "RIFF" if the container is "RF64", the current
1209
     * container name otherwise.
1210
     * Used to enforce "RIFF" as the container in operations where
1211
     * RF64 is not allowed.
1212
     * @private
1213
     */
1214
    correctContainer_() {
1215
        return this.container == "RF64" ? "RIFF" : this.container;
1216
    }
1217
1218
    /**
1219
     * Set the string code of the bit depth based on the "fmt " chunk.
1220
     * @private
1221
     */
1222
     bitDepthFromFmt_() {
1223
        if (this.fmt.audioFormat == 3 && this.fmt.bitsPerSample == 32) {
1224
            this.bitDepth = "32f";
1225
        } else if (this.fmt.audioFormat == 6) {
1226
            this.bitDepth = "8a";
1227
        } else if (this.fmt.audioFormat == 7) {
1228
            this.bitDepth = "8m";
1229
        } else {
1230
            this.bitDepth = this.fmt.bitsPerSample.toString();
1231
        }
1232
     }
1233
    
1234
    /**
1235
     * Return a .wav file byte buffer with the data from the WaveFile object.
1236
     * The return value of this method can be written straight to disk.
1237
     * @return {!Uint8Array} The wav file bytes.
1238
     * @private
1239
     */
1240
    createWaveFile_() {
1241
        let options = {"be": this.LEorBE_()};
1242
        return new Uint8Array([].concat(
1243
            byteData_.pack(this.container, fourCC_),
1244
            byteData_.pack(this.chunkSize, uInt32_),
1245
            byteData_.pack(this.format, fourCC_),
1246
            this.getDs64Bytes_(),
1247
            this.getBextBytes_(),
1248
            this.getFmtBytes_(),
1249
            this.getFactBytes_(),
1250
            byteData_.pack(this.data.chunkId, fourCC_),
1251
            byteData_.pack(this.data.chunkSize, uInt32_),
1252
            this.samplesToBytes_(options),
1253
            this.getCueBytes_()));
1254
    }
1255
}
1256
1257
module.exports = WaveFile;
1258