Passed
Push — master ( e3a958...4c9638 )
by Rafael S.
02:11
created

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