Passed
Push — master ( 1b0b84...83feb8 )
by Rafael S.
01:59
created

index.js (3 issues)

Severity
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha. MIT License.
3
 * https://github.com/rochars/wavefile
4
 *
5
 */
6
7
/** @private */
8
const bitDepth_ = require("bitdepth");
9
/** @private */
10
const riffChunks_ = require("riff-chunks");
11
/** @private */
12
const imaadpcm_ = require("imaadpcm");
13
/** @private */
14
const alawmulaw_ = require("alawmulaw");
15
/** @private */
16
const byteData_ = require("byte-data");
17
/** @private */
18
const encodeBase64_ = require("base64-arraybuffer").encode;
19
/** @private */
20
const decodeBase64_ = require("base64-arraybuffer").decode;
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} 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
            "64": 3
265
        };
266
        /**
267
         * @type {number}
268
         * @private
269
         */
270
        this.head_ = 0;
271
        // Load a file from the buffer if one was passed
272
        // when creating the object
273
        if(bytes) {
274
            this.fromBuffer(bytes);
275
        }
276
    }
277
278
    /**
279
     * Set up a WaveFile object based on the arguments passed.
280
     * @param {number} numChannels The number of channels
281
     *      (Integer numbers: 1 for mono, 2 stereo and so on).
282
     * @param {number} sampleRate The sample rate.
283
     *      Integer numbers like 8000, 44100, 48000, 96000, 192000.
284
     * @param {string} bitDepth The audio bit depth.
285
     *      One of "4", "8", "8a", "8m", "16", "24", "32", "32f", "64"
286
     *      or any value between "8" and "32".
287
     * @param {!Array<number>} samples Array of samples to be written.
288
     *      The samples must be in the correct range according to the
289
     *      bit depth.
290
     * @param {Object} options Optional. Used to force the container
291
     *      as RIFX with {"container": "RIFX"}
292
     * @throws {Error} If any argument does not meet the criteria.
293
     * @export
294
     */
295
    fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) {
296
        if (!options["container"]) {
297
            options["container"] = "RIFF";
298
        }
299
        this.bitDepth = bitDepth;
300
        // interleave the samples if they were passed de-interleaved
301
        this.data.samples = samples;
302
        if (samples.length > 0) {
303
            if (samples[0].constructor === Array) {
304
                this.isInterleaved = false;
305
                this.assureInterleaved_();
306
            }
307
        } 
308
        /** type {number} */
309
        let numBytes = (((parseInt(bitDepth, 10) - 1) | 7) + 1) / 8;
310
        this.createPCMHeader_(
311
            bitDepth, numChannels, sampleRate, numBytes, options);
312
        if (bitDepth == "4") {
313
            this.createADPCMHeader_(
314
                bitDepth, numChannels, sampleRate, numBytes, options);
315
        } else if (bitDepth == "8a" || bitDepth == "8m") {
316
            this.createALawMulawHeader_(
317
                bitDepth, numChannels, sampleRate, numBytes, options);
318
        } else if(Object.keys(this.audioFormats_).indexOf(bitDepth) == -1 ||
319
                this.fmt.numChannels > 2) {
320
            this.createExtensibleHeader_(
321
                bitDepth, numChannels, sampleRate, numBytes, options);
322
        }
323
        // the data chunk
324
        this.data.chunkId = "data";
325
        this.data.chunkSize = this.data.samples.length * numBytes;
326
        this.validateHeader_();
327
        this.LEorBE_();
328
    }
329
330
    /**
331
     * Init a WaveFile object from a byte buffer.
332
     * @param {!Uint8Array} bytes The buffer.
333
     * @throws {Error} If container is not RIFF or RIFX.
334
     * @throws {Error} If no "fmt " chunk is found.
335
     * @throws {Error} If no "fact" chunk is found and "fact" is needed.
336
     * @throws {Error} If no "data" chunk is found.
337
     * @export
338
     */
339
    fromBuffer(bytes) {
340
        this.clearHeader_();
341
        this.readRIFFChunk_(bytes);
342
        /** @type {!Object} */
343
        let chunk = riffChunks_.read(bytes);
344
        this.readDs64Chunk_(chunk["subChunks"]);
345
        this.readFmtChunk_(chunk["subChunks"]);
346
        this.readFactChunk_(chunk["subChunks"]);
347
        this.readBextChunk_(chunk["subChunks"]);
348
        this.readCueChunk_(chunk["subChunks"]);
349
        this.readDataChunk_(chunk["subChunks"]);
350
        this.readLISTChunk_(chunk["subChunks"]);
351
        this.readJunkChunk_(chunk["subChunks"]);
352
        this.bitDepthFromFmt_();
353
    }
354
355
    /**
356
     * Return a byte buffer representig the WaveFile object as a .wav file.
357
     * The return value of this method can be written straight to disk.
358
     * @return {!Uint8Array} A .wav file.
359
     * @throws {Error} If any property of the object appears invalid.
360
     * @export
361
     */
362
    toBuffer() {
363
        this.validateHeader_();
364
        this.assureInterleaved_();
365
        return this.createWaveFile_();
366
    }
367
368
    /**
369
     * Use a .wav file encoded as a base64 string to load the WaveFile object.
370
     * @param {string} base64String A .wav file as a base64 string.
371
     * @throws {Error} If any property of the object appears invalid.
372
     * @export
373
     */
374
    fromBase64(base64String) {
375
        this.fromBuffer(new Uint8Array(decodeBase64_(base64String)));
376
    }
377
378
    /**
379
     * Return a base64 string representig the WaveFile object as a .wav file.
380
     * @return {string} A .wav file as a base64 string.
381
     * @throws {Error} If any property of the object appears invalid.
382
     * @export
383
     */
384
    toBase64() {
385
        return encodeBase64_(this.toBuffer());
386
    }
387
388
    /**
389
     * Return a DataURI string representig the WaveFile object as a .wav file.
390
     * The return of this method can be used to load the audio in browsers.
391
     * @return {string} A .wav file as a DataURI.
392
     * @throws {Error} If any property of the object appears invalid.
393
     * @export
394
     */
395
    toDataURI() {
396
        return "data:audio/wav;base64," + this.toBase64();
397
    }
398
399
    /**
400
     * Use a .wav file encoded as a DataURI to load the WaveFile object.
401
     * @param {string} dataURI A .wav file as DataURI.
402
     * @throws {Error} If any property of the object appears invalid.
403
     * @export
404
     */
405
    fromDataURI(dataURI) {
406
        this.fromBase64(dataURI.replace("data:audio/wav;base64,", ""));
407
    }
408
409
    /**
410
     * Force a file as RIFF.
411
     * @export
412
     */
413
    toRIFF() {
414
        if (this.container == "RF64") {
415
            this.fromScratch(
416
                this.fmt.numChannels,
417
                this.fmt.sampleRate,
418
                this.bitDepth,
419
                this.data.samples);
420
        } else {
421
            this.container = "RIFF";
422
            this.LEorBE_();
423
        }
424
    }
425
426
    /**
427
     * Force a file as RIFX.
428
     * @export
429
     */
430
    toRIFX() {
431
        if (this.container == "RF64") {
432
            this.fromScratch(
433
                this.fmt.numChannels,
434
                this.fmt.sampleRate,
435
                this.bitDepth,
436
                this.data.samples,
437
                {"container": "RIFX"});
438
        } else {
439
            this.container = "RIFX";
440
            this.LEorBE_();
441
        }
442
    }
443
444
    /**
445
     * Change the bit depth of the samples.
446
     * @param {string} bitDepth The new bit depth of the samples.
447
     *      One of "8" ... "32" (integers), "32f" or "64" (floats)
448
     * @param {boolean} changeResolution A boolean indicating if the
449
     *      resolution of samples should be actually changed or not.
450
     * @throws {Error} If the bit depth is not valid.
451
     * @export
452
     */
453
    toBitDepth(bitDepth, changeResolution=true) {
454
        let toBitDepth = bitDepth;
455
        let thisBitDepth = this.bitDepth;
456
        if (!changeResolution) {
457
            toBitDepth = this.realBitDepth_(bitDepth);
458
            thisBitDepth = this.realBitDepth_(this.bitDepth);
459
        }
460
        this.assureInterleaved_();
461
        this.assureUncompressed_();
462
        bitDepth_.toBitDepth(this.data.samples, thisBitDepth, toBitDepth);
463
        this.fromScratch(
464
            this.fmt.numChannels,
465
            this.fmt.sampleRate,
466
            bitDepth,
467
            this.data.samples,
468
            {"container": this.correctContainer_()});
469
    }
470
471
    /**
472
     * Interleave multi-channel samples.
473
     * @export
474
     */
475
    interleave() {
476
        if (!this.isInterleaved) {
477
            /** @type {!Array<number>} */
478
            let finalSamples = [];
479
            for (let i=0; i < this.data.samples[0].length; i++) {
480
                for (let j=0; j < this.data.samples.length; j++) {
481
                    finalSamples.push(this.data.samples[j][i]);
482
                }
483
            }
484
            this.data.samples = finalSamples;
485
            this.isInterleaved = true;
486
        }
487
    }
488
489
    /**
490
     * De-interleave samples into multiple channels.
491
     * @export
492
     */
493
    deInterleave() {
494
        if (this.isInterleaved) {
495
            /** @type {!Array<!Array<number>>} */
496
            let finalSamples = [];
497
            for (let i=0; i < this.fmt.numChannels; i++) {
498
                finalSamples[i] = [];
499
            }
500
            /** @type {number} */
501
            let len = this.data.samples.length;
502
            for (let i=0; i < len; i+=this.fmt.numChannels) {
503
                for (let j=0; j < this.fmt.numChannels; j++) {
504
                    finalSamples[j].push(this.data.samples[i+j]);
505
                }
506
            }
507
            this.data.samples = finalSamples;
508
            this.isInterleaved = false;
509
        }
510
    }
511
512
    /**
513
     * Encode a 16-bit wave file as 4-bit IMA ADPCM.
514
     * @throws {Error} If sample rate is not 8000.
515
     * @throws {Error} If number of channels is not 1.
516
     * @export
517
     */
518
    toIMAADPCM() {
519
        if (this.fmt.sampleRate != 8000) {
520
            throw new Error(
521
                "Only 8000 Hz files can be compressed as IMA-ADPCM.");
522
        } 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...
523
            throw new Error(
524
                "Only mono files can be compressed as IMA-ADPCM.");
525
        } else {
526
            this.assure16Bit_();
527
            this.fromScratch(
528
                this.fmt.numChannels,
529
                this.fmt.sampleRate,
530
                "4",
531
                imaadpcm_.encode(this.data.samples),
532
                {"container": this.correctContainer_()});
533
        }
534
    }
535
536
    /**
537
     * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
538
     * @param {string} bitDepth The new bit depth of the samples.
539
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
540
     *      Optional. Default is 16.
541
     * @export
542
     */
543
    fromIMAADPCM(bitDepth="16") {
544
        this.fromScratch(
545
            this.fmt.numChannels,
546
            this.fmt.sampleRate,
547
            "16",
548
            imaadpcm_.decode(this.data.samples, this.fmt.blockAlign),
549
            {"container": this.correctContainer_()});
550
        if (bitDepth != "16") {
551
            this.toBitDepth(bitDepth);
552
        }
553
    }
554
555
    /**
556
     * Encode 16-bit wave file as 8-bit A-Law.
557
     * @export
558
     */
559
    toALaw() {
560
        this.assure16Bit_();
561
        this.assureInterleaved_();
562
        this.fromScratch(
563
            this.fmt.numChannels,
564
            this.fmt.sampleRate,
565
            "8a",
566
            alawmulaw_.alaw.encode(this.data.samples),
567
            {"container": this.correctContainer_()});
568
    }
569
570
    /**
571
     * Decode a 8-bit A-Law wave file into a 16-bit wave file.
572
     * @param {string} bitDepth The new bit depth of the samples.
573
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
574
     *      Optional. Default is 16.
575
     * @export
576
     */
577
    fromALaw(bitDepth="16") {
578
        this.fromScratch(
579
            this.fmt.numChannels,
580
            this.fmt.sampleRate,
581
            "16",
582
            alawmulaw_.alaw.decode(this.data.samples),
583
            {"container": this.correctContainer_()});
584
        if (bitDepth != "16") {
585
            this.toBitDepth(bitDepth);
586
        }
587
    }
588
589
    /**
590
     * Encode 16-bit wave file as 8-bit mu-Law.
591
     * @export
592
     */
593
    toMuLaw() {
594
        this.assure16Bit_();
595
        this.assureInterleaved_();
596
        this.fromScratch(
597
            this.fmt.numChannels,
598
            this.fmt.sampleRate,
599
            "8m",
600
            alawmulaw_.mulaw.encode(this.data.samples),
601
            {"container": this.correctContainer_()});
602
    }
603
604
    /**
605
     * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
606
     * @param {string} bitDepth The new bit depth of the samples.
607
     *      One of "8" ... "32" (integers), "32f" or "64" (floats).
608
     *      Optional. Default is 16.
609
     * @export
610
     */
611
    fromMuLaw(bitDepth="16") {
612
        this.fromScratch(
613
            this.fmt.numChannels,
614
            this.fmt.sampleRate,
615
            "16",
616
            alawmulaw_.mulaw.decode(this.data.samples),
617
            {"container": this.correctContainer_()});
618
        if (bitDepth != "16") {
619
            this.toBitDepth(bitDepth);
620
        }
621
    }
622
623
    /**
624
     * Write a RIFF tag in the INFO chunk. If the tag do not exist,
625
     * then it is created. It if exists, it is overwritten.
626
     * @param {string} tag The tag name.
627
     * @param {string} value The tag value.
628
     * @throws {Error} If the tag name is not valid.
629
     * @export
630
     */
631
    setTag(tag, value) {
632
        tag = this.fixTagName_(tag);
633
        /** @type {!Object} */
634
        let index = this.getTagIndex_(tag);
635
        if (index.TAG !== null) {
636
            this.LIST[index.LIST]["subChunks"][index.TAG]["chunkSize"] =
637
                value.length + 1;
638
            this.LIST[index.LIST]["subChunks"][index.TAG]["value"] = value;
639
        } else if (index.LIST !== null) {
640
            this.LIST[index.LIST]["subChunks"].push({
641
                "chunkId": tag,
642
                "chunkSize": value.length + 1,
643
                "value": value});
644
        } else {
645
            this.LIST.push({
646
                "chunkId": "LIST",
647
                "chunkSize": 8 + value.length + 1,
648
                "format": "INFO",
649
                "chunkData": [],
650
                "subChunks": []});
651
            this.LIST[this.LIST.length - 1]["subChunks"].push({
652
                "chunkId": tag,
653
                "chunkSize": value.length + 1,
654
                "value": value});
655
        }
656
    }
657
658
    /**
659
     * Return the value of a RIFF tag in the INFO chunk.
660
     * @param {string} tag The tag name.
661
     * @return {string|null} The value if the tag is found, null otherwise.
662
     * @export
663
     */
664
    getTag(tag) {
665
        /** @type {!Object} */
666
        let index = this.getTagIndex_(tag);
667
        if (index.TAG !== null) {
668
            return this.LIST[index.LIST]["subChunks"][index.TAG]["value"];
669
        }
670
        return null;
671
    }
672
673
    /**
674
     * Remove a RIFF tag in the INFO chunk.
675
     * @param {string} tag The tag name.
676
     * @return {boolean} True if a tag was deleted.
677
     * @export
678
     */
679
    deleteTag(tag) {
680
        /** @type {!Object} */
681
        let index = this.getTagIndex_(tag);
682
        if (index.TAG !== null) {
683
            this.LIST[index.LIST]["subChunks"].splice(index.TAG, 1);
684
            return true;
685
        }
686
        return false;
687
    }
688
689
    /**
690
     * Create a cue point in the wave file.
691
     * @param {number} position The cue point position in milliseconds.
692
     * @param {string} labl The LIST adtl labl text of the marker. Optional.
693
     * @export
694
     */
695
    setCuePoint(position, labl="") {
696
        this.cue.chunkId = "cue ";
697
        position = (position * this.fmt.sampleRate) / 1000;
698
        /** @type {!Array<Object>} */
699
        let existingPoints = this.getCuePoints_();
700
        this.clearLISTadtl_();
701
        /** @type {number} */
702
        let len = this.cue.points.length;
703
        this.cue.points = [];
704
        /** type {boolean} */
705
        let hasSet = false;
706
        if (len == 0) {
0 ignored issues
show
Comparing len to 0 using the == operator is not safe. Consider using === instead.
Loading history...
707
            this.setCuePoint_(position, 1, labl);
708
        } else {
709
            for (let i=0; i<len; i++) {
710
                if (existingPoints[i]["dwPosition"] > position && !hasSet) {
711
                    this.setCuePoint_(position, i + 1, labl);
712
                    this.setCuePoint_(
713
                        existingPoints[i]["dwPosition"],
714
                        i + 2,
715
                        existingPoints[i]["label"]);
716
                    hasSet = true;
717
                } else {
718
                    this.setCuePoint_(
719
                        existingPoints[i]["dwPosition"], 
720
                        i + 1, 
721
                        existingPoints[i]["label"]);
722
                }
723
            }
724
            if (!hasSet) {
725
                this.setCuePoint_(position, this.cue.points.length + 1, labl);
726
            }
727
        }
728
        this.cue.dwCuePoints = this.cue.points.length;
729
    }
730
731
    /**
732
     * Remove a cue point from a wave file.
733
     * @param {number} index the index of the point. First is 1,
734
     *      second is 2, and so on.
735
     * @export
736
     */
737
    deleteCuePoint(index) {
738
        this.cue.chunkId = "cue ";
739
        /** @type {!Array<Object>} */
740
        let existingPoints = this.getCuePoints_();
741
        this.clearLISTadtl_();
742
        /** type {number} */
743
        let len = this.cue.points.length;
744
        this.cue.points = [];
745
        for (let i=0; i<len; i++) {
746
            if (i + 1 != index) {
747
                this.setCuePoint_(
748
                    existingPoints[i]["dwPosition"],
749
                    i + 1,
750
                    existingPoints[i]["label"]);
751
            }
752
        }
753
        this.cue.dwCuePoints = this.cue.points.length;
754
        if (this.cue.dwCuePoints) {
755
            this.cue.chunkId = 'cue ';
756
        } else {
757
            this.cue.chunkId = '';
758
            this.clearLISTadtl_();
759
        }
760
    }
761
762
    /**
763
     * Update the label of a cue point.
764
     * @param {number} pointIndex The ID of the cue point.
765
     * @param {string} label The new text for the label.
766
     * @export
767
     */
768
    updateLabel(pointIndex, label) {
769
        /** @type {number|null} */
770
        let adtlIndex = this.getAdtlChunk_();
771
        if (adtlIndex !== null) {
772
            for (let i=0; i<this.LIST[adtlIndex]["subChunks"].length; i++) {
773
                if (this.LIST[adtlIndex]["subChunks"][i]["dwName"] ==
774
                        pointIndex) {
775
                    this.LIST[adtlIndex]["subChunks"][i]["value"] = label;
776
                }
777
            }
778
        }
779
    }
780
781
    /**
782
     * Push a new cue point in this.cue.points.
783
     * @param {number} position The position in milliseconds.
784
     * @param {number} dwName the dwName of the cue point
785
     * @private
786
     */
787
    setCuePoint_(position, dwName, label) {
788
        this.cue.points.push({
789
            "dwName": dwName,
790
            "dwPosition": position,
791
            "fccChunk": "data",
792
            "dwChunkStart": 0,
793
            "dwBlockStart": 0,
794
            "dwSampleOffset": position,
795
        });
796
        this.setLabl_(dwName, label);
797
    }
798
799
    /**
800
     * Return an array with the position of all cue points in the file.
801
     * @return {!Array<!Object>}
802
     * @private
803
     */
804
    getCuePoints_() {
805
        /** @type {!Array<Object>} */
806
        let points = [];
807
        for (let i=0; i<this.cue.points.length; i++) {
808
            points.push({
809
                'dwPosition': this.cue.points[i]["dwPosition"],
810
                'label': this.getLabelForCuePoint_(
811
                    this.cue.points[i]["dwName"])});
812
        }
813
        return points;
814
    }
815
816
    /**
817
     * Return the label of a cue point.
818
     * @param {number} pointDwName The ID of the cue point.
819
     * @return {string}
820
     * @private
821
     */
822
    getLabelForCuePoint_(pointDwName) {
823
        /** @type {number|null} */
824
        let adtlIndex = this.getAdtlChunk_();
825
        if (adtlIndex !== null) {
826
            for (let i=0; i<this.LIST[adtlIndex]["subChunks"].length; i++) {
827
                if (this.LIST[adtlIndex]["subChunks"][i]["dwName"] ==
828
                        pointDwName) {
829
                    return this.LIST[adtlIndex]["subChunks"][i]["value"];
830
                }
831
            }
832
        }
833
        return "";
834
    }
835
836
    /**
837
     * Clear any LIST chunk labeled as "adtl".
838
     * @private
839
     */
840
    clearLISTadtl_() {
841
        for (let i=0; i<this.LIST.length; i++) {
842
            if (this.LIST[i]["format"] == 'adtl') {
843
                this.LIST.splice(i);
844
            }
845
        }
846
    }
847
848
    /**
849
     * Create a new "labl" subchunk in a "LIST" chunk of type "adtl".
850
     * @param {number} dwName The ID of the cue point.
851
     * @param {string} label The label for the cue point.
852
     * @private
853
     */
854
    setLabl_(dwName, label) {
855
        /** @type {number|null} */
856
        let adtlIndex = this.getAdtlChunk_();
857
        if (adtlIndex === null) {
858
            this.LIST.push({
859
                "chunkId": "LIST",
860
                "chunkSize": 4,
861
                "format": "adtl",
862
                "chunkData": [],
863
                "subChunks": []});
864
            adtlIndex = this.LIST.length - 1;
865
        }
866
        this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
867
    }
868
869
    /**
870
     * Create a new "labl" subchunk in a "LIST" chunk of type "adtl".
871
     * @param {number} adtlIndex The index of the "adtl" LIST in this.LIST.
872
     * @param {number} dwName The ID of the cue point.
873
     * @param {string} label The label for the cue point.
874
     * @private
875
     */
876
    setLabelText_(adtlIndex, dwName, label) {
877
        this.LIST[adtlIndex]["subChunks"].push({
878
            "chunkId": "labl",
879
            "chunkSize": label.length,
880
            "dwName": dwName,
881
            "value": label
882
        });
883
        this.LIST[adtlIndex]["chunkSize"] += label.length + 4 + 4 + 4 + 1;
884
    }
885
886
    /**
887
     * Return the index of the "adtl" LIST in this.LIST.
888
     * @return {number|null}
889
     * @private
890
     */
891
    getAdtlChunk_() {
892
        for (let i=0; i<this.LIST.length; i++) {
893
            if(this.LIST[i]["format"] == 'adtl') {
894
                return i;
895
            }
896
        }
897
        return null;
898
    }
899
900
    /**
901
     * Return the index of a tag in a FILE chunk.
902
     * @param {string} tag The tag name.
903
     * @return {!Object<string, number|null>}
904
     *      Object.LIST is the INFO index in LIST
905
     *      Object.TAG is the tag index in the INFO
906
     * @private
907
     */
908
    getTagIndex_(tag) {
909
        /** @type {!Object} */
910
        let index = {LIST: null, TAG: null};
911
        for (let i=0; i<this.LIST.length; i++) {
912
            if (this.LIST[i]["format"] == "INFO") {
913
                index.LIST = i;
914
                for (let j=0; j<this.LIST[i]["subChunks"].length; j++) {
915
                    if (this.LIST[i]["subChunks"][j]["chunkId"] == tag) {
916
                        index.TAG = j;
917
                        break;
918
                    }
919
                }
920
                break;
921
            }
922
        }
923
        return index;
924
    }
925
926
    /**
927
     * Fix a RIFF tag format if possible, throw an error otherwise.
928
     * @param {string} tag The tag name.
929
     * @return {string} The tag name in proper fourCC format.
930
     * @private
931
     */
932
    fixTagName_(tag) {
933
        if (tag.constructor !== String) {
934
            throw new Error("Invalid tag name.");
935
        } else if(tag.length < 4) {
936
            for (let i=0; i<4-tag.length; i++) {
937
                tag += ' ';
938
            }
939
        }
940
        return tag;
941
    }
942
943
    /**
944
     * Create the header of a ADPCM wave file.
945
     * @param {string} bitDepth The audio bit depth
946
     * @param {number} numChannels The number of channels
947
     * @param {number} sampleRate The sample rate.
948
     * @param {number} numBytes The number of bytes each sample use.
949
     * @param {!Object} options The extra options, like container defintion.
950
     * @private
951
     */
952
    createADPCMHeader_(bitDepth, numChannels, sampleRate, numBytes, options) {
953
        this.createPCMHeader_(
954
            bitDepth, numChannels, sampleRate, numBytes, options);
955
        this.chunkSize = 40 + this.data.samples.length;
956
        this.fmt.chunkSize = 20;
957
        this.fmt.byteRate = 4055;
958
        this.fmt.blockAlign = 256;
959
        this.fmt.bitsPerSample = 4;
960
        this.fmt.cbSize = 2;
961
        this.fmt.validBitsPerSample = 505;
962
        this.fact.chunkId = "fact";
963
        this.fact.chunkSize = 4;
964
        this.fact.dwSampleLength = this.data.samples.length * 2;
965
        this.data.chunkSize = this.data.samples.length;
966
    }
967
968
    /**
969
     * Create the header of WAVE_FORMAT_EXTENSIBLE file.
970
     * @param {string} bitDepth The audio bit depth
971
     * @param {number} numChannels The number of channels
972
     * @param {number} sampleRate The sample rate.
973
     * @param {number} numBytes The number of bytes each sample use.
974
     * @param {!Object} options The extra options, like container defintion.
975
     * @private
976
     */
977
    createExtensibleHeader_(
978
            bitDepth, numChannels, sampleRate, numBytes, options) {
979
        this.createPCMHeader_(
980
            bitDepth, numChannels, sampleRate, numBytes, options);
981
        this.chunkSize = 36 + 24 + this.data.samples.length * numBytes;
982
        this.fmt.chunkSize = 40;
983
        this.fmt.bitsPerSample = ((parseInt(bitDepth, 10) - 1) | 7) + 1;
984
        this.fmt.cbSize = 22;
985
        this.fmt.validBitsPerSample = parseInt(bitDepth, 10);
986
        this.fmt.dwChannelMask = this.getDwChannelMask_();
987
        // subformat 128-bit GUID as 4 32-bit values
988
        // only supports uncompressed integer PCM samples
989
        this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
990
    }
991
992
    /**
993
     * Get the value for dwChannelMask according to the number of channels.
994
     * @return {number} the dwChannelMask value.
995
     * @private
996
     */
997
    getDwChannelMask_() {
998
        let dwChannelMask = 0;
999
        // mono = FC
1000
        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...
1001
            dwChannelMask = 0x4;
1002
        // stereo = FL, FR
1003
        } else if (this.fmt.numChannels == 2) {
1004
            dwChannelMask = 0x3;
1005
        // quad = FL, FR, BL, BR
1006
        } else if (this.fmt.numChannels == 4) {
1007
            dwChannelMask = 0x33;
1008
        // 5.1 = FL, FR, FC, LF, BL, BR
1009
        } else if (this.fmt.numChannels == 6) {
1010
            dwChannelMask = 0x3F;
1011
        // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR
1012
        } else if (this.fmt.numChannels == 8) {
1013
            dwChannelMask = 0x63F;
1014
        }
1015
        return dwChannelMask;
1016
    }
1017
1018
    /**
1019
     * Create the header of mu-Law and A-Law wave files.
1020
     * @param {string} bitDepth The audio bit depth
1021
     * @param {number} numChannels The number of channels
1022
     * @param {number} sampleRate The sample rate.
1023
     * @param {number} numBytes The number of bytes each sample use.
1024
     * @param {!Object} options The extra options, like container defintion.
1025
     * @private
1026
     */
1027
    createALawMulawHeader_(
1028
            bitDepth, numChannels, sampleRate, numBytes, options) {
1029
        this.createPCMHeader_(
1030
            bitDepth, numChannels, sampleRate, numBytes, options);
1031
        this.chunkSize = 40 + this.data.samples.length;
1032
        this.fmt.chunkSize = 20;
1033
        this.fmt.cbSize = 2;
1034
        this.fmt.validBitsPerSample = 8;
1035
        this.fact.chunkId = "fact";
1036
        this.fact.chunkSize = 4;
1037
        this.fact.dwSampleLength = this.data.samples.length;
1038
    }
1039
1040
    /**
1041
     * Create the header of a linear PCM wave file.
1042
     * @param {string} bitDepth The audio bit depth
1043
     * @param {number} numChannels The number of channels
1044
     * @param {number} sampleRate The sample rate.
1045
     * @param {number} numBytes The number of bytes each sample use.
1046
     * @param {!Object} options The extra options, like container defintion.
1047
     * @private
1048
     */
1049
    createPCMHeader_(bitDepth, numChannels, sampleRate, numBytes, options) {
1050
        this.clearHeader_();
1051
        this.container = options["container"];
1052
        this.chunkSize = 36 + this.data.samples.length * numBytes;
1053
        this.format = "WAVE";
1054
        this.fmt.chunkId = "fmt ";
1055
        this.fmt.chunkSize = 16;
1056
        this.fmt.byteRate = (numChannels * numBytes) * sampleRate;
1057
        this.fmt.blockAlign = numChannels * numBytes;
1058
        this.fmt.audioFormat = this.audioFormats_[bitDepth] ?
1059
            this.audioFormats_[bitDepth] : 65534;
1060
        this.fmt.numChannels = numChannels;
1061
        this.fmt.sampleRate = sampleRate;
1062
        this.fmt.bitsPerSample = parseInt(bitDepth, 10);
1063
        this.fmt.cbSize = 0;
1064
        this.fmt.validBitsPerSample = 0;
1065
    }
1066
1067
    /**
1068
     * Return the closest greater number of bits for a number of bits that
1069
     * do not fill a full sequence of bytes.
1070
     * @param {string} bitDepth The bit depth.
1071
     * @return {string}
1072
     * @private
1073
     */
1074
    realBitDepth_(bitDepth) {
1075
        if (bitDepth != "32f") {
1076
            bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1).toString();
1077
        }
1078
        return bitDepth;
1079
    }
1080
1081
    /**
1082
     * Validate the header of the file.
1083
     * @throws {Error} If any property of the object appears invalid.
1084
     * @private
1085
     */
1086
    validateHeader_() {
1087
        this.validateBitDepth_();
1088
        this.validateNumChannels_();
1089
        this.validateSampleRate_();
1090
    }
1091
1092
    /**
1093
     * Validate the bit depth.
1094
     * @return {boolean} True is the bit depth is valid.
1095
     * @throws {Error} If bit depth is invalid.
1096
     * @private
1097
     */
1098
    validateBitDepth_() {
1099
        if (!this.audioFormats_[this.bitDepth]) {
1100
            if (parseInt(this.bitDepth, 10) > 8 &&
1101
                    parseInt(this.bitDepth, 10) < 54) {
1102
                return true;
1103
            }
1104
            throw new Error("Invalid bit depth.");
1105
        }
1106
        return true;
1107
    }
1108
1109
    /**
1110
     * Validate the number of channels.
1111
     * @return {boolean} True is the number of channels is valid.
1112
     * @throws {Error} If the number of channels is invalid.
1113
     * @private
1114
     */
1115
    validateNumChannels_() {
1116
        /** @type {number} */
1117
        let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1118
        if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1119
            throw new Error("Invalid number of channels.");
1120
        }
1121
        return true;
1122
    }
1123
1124
    /**
1125
     * Validate the sample rate value.
1126
     * @return {boolean} True is the sample rate is valid.
1127
     * @throws {Error} If the sample rate is invalid.
1128
     * @private
1129
     */
1130
    validateSampleRate_() {
1131
        /** @type {number} */
1132
        let byteRate = this.fmt.numChannels *
1133
            (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1134
        if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1135
            throw new Error("Invalid sample rate.");
1136
        }
1137
        return true;
1138
    }
1139
1140
    /**
1141
     * Reset attributes that should emptied when a file is
1142
     * created with the fromScratch() or fromBuffer() methods.
1143
     * @private
1144
     */
1145
    clearHeader_() {
1146
        this.fmt.cbSize = 0;
1147
        this.fmt.validBitsPerSample = 0;
1148
        this.fact.chunkId = "";
1149
        this.ds64.chunkId = "";
1150
    }
1151
1152
    /**
1153
     * Make the file 16-bit if it is not.
1154
     * @private
1155
     */
1156
    assure16Bit_() {
1157
        this.assureUncompressed_();
1158
        if (this.bitDepth != "16") {
1159
            this.toBitDepth("16");
1160
        }
1161
    }
1162
1163
    /**
1164
     * Uncompress the samples in case of a compressed file.
1165
     * @private
1166
     */
1167
    assureUncompressed_() {
1168
        if (this.bitDepth == "8a") {
1169
            this.fromALaw();
1170
        } else if(this.bitDepth == "8m") {
1171
            this.fromMuLaw();
1172
        } else if (this.bitDepth == "4") {
1173
            this.fromIMAADPCM();
1174
        }
1175
    }
1176
1177
    /**
1178
     * Interleave the samples in case they are de-Interleaved.
1179
     * @private
1180
     */
1181
    assureInterleaved_() {
1182
        if (!this.isInterleaved) {
1183
            this.interleave();
1184
        }
1185
    }
1186
1187
    /**
1188
     * Set up to work wih big-endian or little-endian files.
1189
     * The types used are changed to LE or BE. If the
1190
     * the file is big-endian (RIFX), true is returned.
1191
     * @return {boolean} True if the file is RIFX.
1192
     * @private
1193
     */
1194
    LEorBE_() {
1195
        /** @type {boolean} */
1196
        let bigEndian = this.container === "RIFX";
1197
        uInt16_["be"] = bigEndian;
1198
        uInt32_["be"] = bigEndian;
1199
        return bigEndian;
1200
    }
1201
1202
    /**
1203
     * Find a chunk by its fourCC_ in a array of RIFF chunks.
1204
     * @param {!Array<!Object>} chunks The wav file chunks.
1205
     * @param {string} chunkId The chunk fourCC_.
1206
     * @param {boolean} multiple True if there may be multiple chunks
1207
     *      with the same chunkId.
1208
     * @return {Object|Array<!Object>}
1209
     * @private
1210
     */
1211
    findChunk_(chunks, chunkId, multiple=false) {
1212
        /** @type {!Array<!Object>} */
1213
        let chunk = [];
1214
        for (let i=0; i<chunks.length; i++) {
1215
            if (chunks[i]["chunkId"] == chunkId) {
1216
                if (multiple) {
1217
                    chunk.push(chunks[i]);
1218
                } else {
1219
                    return chunks[i];
1220
                }
1221
            }
1222
        }
1223
        if (chunkId == "LIST") {
1224
            return chunk.length ? chunk : null;
1225
        }
1226
        return null;
1227
    }
1228
1229
    /**
1230
     * Read the RIFF chunk a wave file.
1231
     * @param {!Uint8Array} bytes A wav buffer.
1232
     * @throws {Error} If no "RIFF" chunk is found.
1233
     * @private
1234
     */
1235
    readRIFFChunk_(bytes) {
1236
        this.head_ = 0;
1237
        this.container = this.readString_(bytes, 4);
1238
        if (["RIFF", "RIFX", "RF64"].indexOf(this.container) === -1) {
1239
            throw Error("Not a supported format.");
1240
        }
1241
        this.LEorBE_();
1242
        this.chunkSize = this.read_(bytes, uInt32_);
1243
        this.format = this.readString_(bytes, 4);
1244
        if (this.format != "WAVE") {
1245
            throw Error("Could not find the 'WAVE' format identifier");
1246
        }
1247
    }
1248
1249
    /**
1250
     * Read the "fmt " chunk of a wave file.
1251
     * @param {!Array<!Object>} chunks The wav file chunks.
1252
     * @throws {Error} If no "fmt " chunk is found.
1253
     * @private
1254
     */
1255
    readFmtChunk_(chunks) {
1256
        /** type {Array<!Object>} */
1257
        let chunk = this.findChunk_(chunks, "fmt ");
1258
        if (chunk) {
1259
            this.head_ = 0;
1260
            let chunkData = chunk["chunkData"];
1261
            this.fmt.chunkId = chunk["chunkId"];
1262
            this.fmt.chunkSize = chunk["chunkSize"];
1263
            this.fmt.audioFormat = this.read_(chunkData, uInt16_);
1264
            this.fmt.numChannels = this.read_(chunkData, uInt16_);
1265
            this.fmt.sampleRate = this.read_(chunkData, uInt32_);
1266
            this.fmt.byteRate = this.read_(chunkData, uInt32_);
1267
            this.fmt.blockAlign = this.read_(chunkData, uInt16_);
1268
            this.fmt.bitsPerSample = this.read_(chunkData, uInt16_);
1269
            this.readFmtExtension_(chunkData);
1270
        } else {
1271
            throw Error("Could not find the 'fmt ' chunk");
1272
        }
1273
    }
1274
1275
    /**
1276
     * Read the "fmt " chunk extension.
1277
     * @param {!Array<number>} chunkData The "fmt " chunk.
1278
     * @private
1279
     */
1280
    readFmtExtension_(chunkData) {
1281
        if (this.fmt.chunkSize > 16) {
1282
            this.fmt.cbSize = this.read_(
1283
                chunkData, uInt16_);
1284
            if (this.fmt.chunkSize > 18) {
1285
                this.fmt.validBitsPerSample = this.read_(chunkData, uInt16_);
1286
                if (this.fmt.chunkSize > 20) {
1287
                    this.fmt.dwChannelMask = this.read_(chunkData, uInt32_);
1288
                    this.fmt.subformat = [
1289
                        this.read_(chunkData, uInt32_),
1290
                        this.read_(chunkData, uInt32_),
1291
                        this.read_(chunkData, uInt32_),
1292
                        this.read_(chunkData, uInt32_)];
1293
                }
1294
            }
1295
        }
1296
    }
1297
1298
    /**
1299
     * Read the "fact" chunk of a wav file.
1300
     * @param {!Array<Object>} chunks The wav file chunks.
1301
     * @throws {Error} If no "fact" chunk is found.
1302
     * @private
1303
     */
1304
    readFactChunk_(chunks) {
1305
        /** type {Array<!Object>} */
1306
        let chunk = this.findChunk_(chunks, "fact");
1307
        if (chunk) {
1308
            this.head_ = 0;
1309
            this.fact.chunkId = chunk["chunkId"];
1310
            this.fact.chunkSize = chunk["chunkSize"];
1311
            this.fact.dwSampleLength = this.read_(chunk["chunkData"], uInt32_);
1312
        }
1313
    }
1314
1315
    /**
1316
     * Read the "cue " chunk of a wave file.
1317
     * @param {!Array<Object>} chunks The RIFF file chunks.
1318
     * @private
1319
     */
1320
    readCueChunk_(chunks) {
1321
        /** type {Array<!Object>} */
1322
        let chunk = this.findChunk_(chunks, "cue ");
1323
        if (chunk) {
1324
            this.head_ = 0;
1325
            let chunkData = chunk["chunkData"];
1326
            this.cue.chunkId = chunk["chunkId"];
1327
            this.cue.chunkSize = chunk["chunkSize"];
1328
            this.cue.dwCuePoints = this.read_(chunkData, uInt32_);
1329
            for (let i=0; i<this.cue.dwCuePoints; i++) {
1330
                this.cue.points.push({
1331
                    "dwName": this.read_(chunkData, uInt32_),
1332
                    "dwPosition": this.read_(chunkData, uInt32_),
1333
                    "fccChunk": this.readString_(chunkData, 4),
1334
                    "dwChunkStart": this.read_(chunkData, uInt32_),
1335
                    "dwBlockStart": this.read_(chunkData, uInt32_),
1336
                    "dwSampleOffset": this.read_(chunkData, uInt32_),
1337
                });
1338
            }
1339
        }
1340
    }
1341
1342
    /**
1343
     * Read the "data" chunk of a wave file.
1344
     * @param {!Array<Object>} chunks The RIFF file chunks.
1345
     * @throws {Error} If no "data" chunk is found.
1346
     * @private
1347
     */
1348
    readDataChunk_(chunks) {
1349
        /** type {Array<!Object>} */
1350
        let chunk = this.findChunk_(chunks, "data");
1351
        if (chunk) {
1352
            this.data.chunkId = "data";
1353
            this.data.chunkSize = chunk["chunkSize"];
1354
            this.samplesFromBytes_(chunk["chunkData"]);
1355
        } else {
1356
            throw Error("Could not find the 'data' chunk");
1357
        }
1358
    }
1359
1360
    /**
1361
     * Read the "bext" chunk of a wav file.
1362
     * @param {!Array<Object>} chunks The wav file chunks.
1363
     * @private
1364
     */
1365
    readBextChunk_(chunks) {
1366
        /** type {Array<!Object>} */
1367
        let chunk = this.findChunk_(chunks, "bext");
1368
        if (chunk) {
1369
            this.head_ = 0;
1370
            let chunkData = chunk["chunkData"];
1371
            this.bext.chunkId = chunk["chunkId"];
1372
            this.bext.chunkSize = chunk["chunkSize"];
1373
            this.bext.description = this.readString_(chunkData, 256);
1374
            this.bext.originator = this.readString_(chunkData, 32);
1375
            this.bext.originatorReference = this.readString_(chunkData, 32);
1376
            this.bext.originationDate = this.readString_(chunkData, 10);
1377
            this.bext.originationTime = this.readString_(chunkData, 8);
1378
            this.bext.timeReference = [
1379
                this.read_(chunkData, uInt32_),
1380
                this.read_(chunkData, uInt32_)];
1381
            this.bext.version = this.read_(chunkData, uInt16_);
1382
            this.bext.UMID = this.readString_(chunkData, 64);
1383
            this.bext.loudnessValue = this.read_(chunkData, uInt16_);
1384
            this.bext.loudnessRange = this.read_(chunkData, uInt16_);
1385
            this.bext.maxTruePeakLevel = this.read_(chunkData, uInt16_);
1386
            this.bext.maxMomentaryLoudness = this.read_(chunkData, uInt16_);
1387
            this.bext.maxShortTermLoudness = this.read_(chunkData, uInt16_);
1388
            this.bext.reserved = this.readString_(chunkData, 180);
1389
            this.bext.codingHistory = this.readString_(
1390
                chunkData, this.bext.chunkSize - 602);
1391
        }
1392
    }
1393
1394
    /**
1395
     * Read the "ds64" chunk of a wave file.
1396
     * @param {!Array<Object>} chunks The wav file chunks.
1397
     * @throws {Error} If no "ds64" chunk is found and the file is RF64.
1398
     * @private
1399
     */
1400
    readDs64Chunk_(chunks) {
1401
        /** type {Array<!Object>} */
1402
        let chunk = this.findChunk_(chunks, "ds64");
1403
        if (chunk) {
1404
            this.head_ = 0;
1405
            let chunkData = chunk["chunkData"];
1406
            this.ds64.chunkId = chunk["chunkId"];
1407
            this.ds64.chunkSize = chunk["chunkSize"];
1408
            this.ds64.riffSizeHigh = this.read_(chunkData, uInt32_);
1409
            this.ds64.riffSizeLow = this.read_(chunkData, uInt32_);
1410
            this.ds64.dataSizeHigh = this.read_(chunkData, uInt32_);
1411
            this.ds64.dataSizeLow = this.read_(chunkData, uInt32_);
1412
            this.ds64.originationTime = this.read_(chunkData, uInt32_);
1413
            this.ds64.sampleCountHigh = this.read_(chunkData, uInt32_);
1414
            this.ds64.sampleCountLow = this.read_(chunkData, uInt32_);
1415
            //if (this.ds64.chunkSize > 28) {
1416
            //    this.ds64.tableLength = byteData_.unpack(
1417
            //        chunkData.slice(28, 32), uInt32_);
1418
            //    this.ds64.table = chunkData.slice(
1419
            //         32, 32 + this.ds64.tableLength); 
1420
            //}
1421
        } else {
1422
            if (this.container == "RF64") {
1423
                throw Error("Could not find the 'ds64' chunk");    
1424
            }
1425
        }
1426
    }
1427
1428
    /**
1429
     * Read the "LIST" chunks of a wave file.
1430
     * @param {!Array<Object>} chunks The wav file chunks.
1431
     * @private
1432
     */
1433
    readLISTChunk_(chunks) {
1434
        /** type {Array<!Array<!Object>>>} */
1435
        let listChunks = this.findChunk_(chunks, "LIST", true);
1436
        if (listChunks === null) {
1437
            return;
1438
        }
1439
        for (let j=0; j<listChunks.length; j++) {
1440
            /** type {!Object} */
1441
            let subChunk = listChunks[j];
1442
            this.LIST.push({
1443
                "chunkId": subChunk["chunkId"],
1444
                "chunkSize": subChunk["chunkSize"],
1445
                "format": subChunk["format"],
1446
                "chunkData": subChunk["chunkData"],
1447
                "subChunks": []});
1448
            for (let x=0; x<subChunk["subChunks"].length; x++) {
1449
                this.readLISTSubChunks_(subChunk["subChunks"][x],
1450
                    subChunk["format"]);
1451
            }
1452
        }
1453
    }
1454
1455
    /**
1456
     * Read the sub chunks of a "LIST" chunk.
1457
     * @param {!Object} subChunk The "LIST" subchunks.
1458
     * @param {string} format The "LIST" format, "adtl" or "INFO".
1459
     * @private
1460
     */
1461
    readLISTSubChunks_(subChunk, format) {
1462
        // 'labl', 'note', 'ltxt', 'file'
1463
        if (format == 'adtl') {
1464
            if (["labl", "note"].indexOf(subChunk["chunkId"]) > -1) {
1465
                this.LIST[this.LIST.length - 1]["subChunks"].push({
1466
                    "chunkId": subChunk["chunkId"],
1467
                    "chunkSize": subChunk["chunkSize"],
1468
                    "dwName": byteData_.unpack(
1469
                        subChunk["chunkData"].slice(0, 4),uInt32_),
1470
                    "value": this.readZSTR_(subChunk["chunkData"].slice(4))
1471
                });
1472
            }
1473
        // RIFF 'INFO' tags like ICRD, ISFT, ICMT
1474
        // https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html#Info
1475
        } else if(format == 'INFO') {
1476
            this.LIST[this.LIST.length - 1]["subChunks"].push({
1477
                "chunkId": subChunk["chunkId"],
1478
                "chunkSize": subChunk["chunkSize"],
1479
                "value": this.readZSTR_(subChunk["chunkData"].slice(0))
1480
            });
1481
        } //else {
1482
        //    this.LIST[this.LIST.length - 1]["subChunks"].push({
1483
        //        "chunkId": subChunk["chunkId"],
1484
        //        "chunkSize": subChunk["chunkSize"],
1485
        //        "value": subChunk["chunkData"]
1486
        //    });
1487
        //}
1488
    }
1489
1490
    /**
1491
     * Read the "junk" chunk of a wave file.
1492
     * @param {!Array<Object>} chunks The wav file chunks.
1493
     * @private
1494
     */
1495
    readJunkChunk_(chunks) {
1496
        /** type {Array<!Object>} */
1497
        let chunk = this.findChunk_(chunks, "junk");
1498
        if (chunk) {
1499
            this.junk = {
1500
                "chunkId": chunk["chunkId"],
1501
                "chunkSize": chunk["chunkSize"],
1502
                "chunkData": chunk["chunkData"]
1503
            };
1504
        }
1505
    }
1506
1507
    /**
1508
     * Read bytes as a ZSTR string.
1509
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1510
     * @return {string} The string.
1511
     * @private
1512
     */
1513
    readZSTR_(bytes) {
1514
        /** type {string} */
1515
        let str = "";
1516
        for (let i=0; i<bytes.length; i++) {
1517
            if (bytes[i] === 0) {
1518
                break;
1519
            }
1520
            str += byteData_.unpack([bytes[i]], chr_);
1521
        }
1522
        return str;
1523
    }
1524
1525
    /**
1526
     * Read bytes as a string from a RIFF chunk.
1527
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1528
     * @param {number} maxSize the max size of the string.
1529
     * @return {string} The string.
1530
     * @private
1531
     */
1532
    readString_(bytes, maxSize) {
1533
        /** type {string} */
1534
        let str = "";
1535
        for (let i=0; i<maxSize; i++) {
1536
            str += byteData_.unpack([bytes[this.head_]], chr_);
1537
            this.head_++;
1538
        }
1539
        return str;
1540
    }
1541
1542
    /**
1543
     * Read a number from a chunk.
1544
     * @param {!Array<number>|!Uint8Array} bytes The chunk bytes.
1545
     * @param {!Object} bdType The type definition.
1546
     * @return {number} The number.
1547
     * @private
1548
     */
1549
    read_(bytes, bdType) {
1550
        /** type {number} */
1551
        let size = bdType["bits"] / 8;
1552
        /** type {number} */
1553
        let value = byteData_.unpack(
1554
            bytes.slice(this.head_, this.head_ + size), bdType);
1555
        this.head_ += size;
1556
        return value;
1557
    }
1558
1559
    /**
1560
     * Write a variable size string as bytes. If the string is smaller
1561
     * than the max size the output array is filled with 0s.
1562
     * @param {string} str The string to be written as bytes.
1563
     * @param {number} maxSize the max size of the string.
1564
     * @return {!Array<number>} The bytes.
1565
     * @private
1566
     */
1567
    writeString_(str, maxSize, push=true) {
1568
        /** type {!Array<number>} */   
1569
        let bytes = byteData_.packArray(str, chr_);
1570
        if (push) {
1571
            for (let i=bytes.length; i<maxSize; i++) {
1572
                bytes.push(0);
1573
            }    
1574
        }
1575
        return bytes;
1576
    }
1577
1578
    /**
1579
     * Turn the samples to bytes.
1580
     * @return {!Array<number>} The bytes.
1581
     * @private
1582
     */
1583
    samplesToBytes_() {
1584
        return byteData_.packArray(
1585
            this.data.samples, this.getSamplesType_());
1586
    }
1587
1588
    /**
1589
     * Turn bytes to samples and load them in the data.samples property.
1590
     * @param {!Array<number>} bytes The bytes.
1591
     * @private
1592
     */
1593
    samplesFromBytes_(bytes) {
1594
        this.data.samples = byteData_.unpackArray(
1595
            bytes, this.getSamplesType_());
1596
    }
1597
1598
    /**
1599
     * Get the data type definition for the samples.
1600
     * @return {!Object<string, number|boolean>} The type definition.
1601
     * @private
1602
     */
1603
    getSamplesType_() {
1604
        /** type {!Object<string, number|boolean>} */
1605
        let bdType = {
1606
            "be": this.container === "RIFX",
1607
            "bits": this.fmt.bitsPerSample == 4 ? 8 : this.fmt.bitsPerSample,
1608
            "float": this.fmt.audioFormat == 3 ? true : false
1609
        };
1610
        bdType["signed"] = bdType["bits"] == 8 ? false : true;
1611
        return bdType;
1612
    }
1613
1614
    /**
1615
     * Return the bytes of the "bext" chunk.
1616
     * @return {!Array<number>} The "bext" chunk bytes.
1617
     * @private
1618
     */
1619
    getBextBytes_() {
1620
        /** type {!Array<number>} */
1621
        let bytes = [];
1622
        if (this.bext.chunkId) {
1623
            bytes = bytes.concat(
1624
                byteData_.pack(this.bext.chunkId, fourCC_),
1625
                byteData_.pack(602 + this.bext.codingHistory.length, uInt32_),
1626
                this.writeString_(this.bext.description, 256),
1627
                this.writeString_(this.bext.originator, 32),
1628
                this.writeString_(this.bext.originatorReference, 32),
1629
                this.writeString_(this.bext.originationDate, 10),
1630
                this.writeString_(this.bext.originationTime, 8),
1631
                byteData_.pack(this.bext.timeReference[0], uInt32_),
1632
                byteData_.pack(this.bext.timeReference[1], uInt32_),
1633
                byteData_.pack(this.bext.version, uInt16_),
1634
                this.writeString_(this.bext.UMID, 64),
1635
                byteData_.pack(this.bext.loudnessValue, uInt16_),
1636
                byteData_.pack(this.bext.loudnessRange, uInt16_),
1637
                byteData_.pack(this.bext.maxTruePeakLevel, uInt16_),
1638
                byteData_.pack(this.bext.maxMomentaryLoudness, uInt16_),
1639
                byteData_.pack(this.bext.maxShortTermLoudness, uInt16_),
1640
                this.writeString_(this.bext.reserved, 180),
1641
                this.writeString_(
1642
                    this.bext.codingHistory, this.bext.codingHistory.length));
1643
        }
1644
        return bytes;
1645
    }
1646
1647
    /**
1648
     * Return the bytes of the "ds64" chunk.
1649
     * @return {!Array<number>} The "ds64" chunk bytes.
1650
     * @private
1651
     */
1652
    getDs64Bytes_() {
1653
        /** type {!Array<number>} */
1654
        let bytes = [];
1655
        if (this.ds64.chunkId) {
1656
            bytes = bytes.concat(
1657
                byteData_.pack(this.ds64.chunkId, fourCC_),
1658
                byteData_.pack(this.ds64.chunkSize, uInt32_), // 
1659
                byteData_.pack(this.ds64.riffSizeHigh, uInt32_),
1660
                byteData_.pack(this.ds64.riffSizeLow, uInt32_),
1661
                byteData_.pack(this.ds64.dataSizeHigh, uInt32_),
1662
                byteData_.pack(this.ds64.dataSizeLow, uInt32_),
1663
                byteData_.pack(this.ds64.originationTime, uInt32_),
1664
                byteData_.pack(this.ds64.sampleCountHigh, uInt32_),
1665
                byteData_.pack(this.ds64.sampleCountLow, uInt32_));          
1666
        }
1667
        //if (this.ds64.tableLength) {
1668
        //    ds64Bytes = ds64Bytes.concat(
1669
        //        byteData_.pack(this.ds64.tableLength, uInt32_),
1670
        //        this.ds64.table);
1671
        //}
1672
        return bytes;
1673
    }
1674
1675
    /**
1676
     * Return the bytes of the "cue " chunk.
1677
     * @return {!Array<number>} The "cue " chunk bytes.
1678
     * @private
1679
     */
1680
    getCueBytes_() {
1681
        /** type {!Array<number>} */
1682
        let bytes = [];
1683
        if (this.cue.chunkId) {
1684
            let cuePointsBytes = this.getCuePointsBytes_();
1685
            bytes = bytes.concat(
1686
                byteData_.pack(this.cue.chunkId, fourCC_),
1687
                byteData_.pack(cuePointsBytes.length + 4, uInt32_),
1688
                byteData_.pack(this.cue.dwCuePoints, uInt32_),
1689
                cuePointsBytes);
1690
        }
1691
        return bytes;
1692
    }
1693
1694
    /**
1695
     * Return the bytes of the "cue " points.
1696
     * @return {!Array<number>} The "cue " points as an array of bytes.
1697
     * @private
1698
     */
1699
    getCuePointsBytes_() {
1700
        /** type {!Array<number>} */
1701
        let points = [];
1702
        for (let i=0; i<this.cue.dwCuePoints; i++) {
1703
            points = points.concat(
1704
                byteData_.pack(this.cue.points[i]["dwName"], uInt32_),
1705
                byteData_.pack(this.cue.points[i]["dwPosition"], uInt32_),
1706
                byteData_.pack(this.cue.points[i]["fccChunk"], fourCC_),
1707
                byteData_.pack(this.cue.points[i]["dwChunkStart"], uInt32_),
1708
                byteData_.pack(this.cue.points[i]["dwBlockStart"], uInt32_),
1709
                byteData_.pack(this.cue.points[i]["dwSampleOffset"], uInt32_));
1710
        }
1711
        return points;
1712
    }
1713
1714
    /**
1715
     * Return the bytes of the "fact" chunk.
1716
     * @return {!Array<number>} The "fact" chunk bytes.
1717
     * @private
1718
     */
1719
    getFactBytes_() {
1720
        /** type {!Array<number>} */
1721
        let bytes = [];
1722
        if (this.fact.chunkId) {
1723
            bytes = bytes.concat(
1724
                byteData_.pack(this.fact.chunkId, fourCC_),
1725
                byteData_.pack(this.fact.chunkSize, uInt32_),
1726
                byteData_.pack(this.fact.dwSampleLength, uInt32_));
1727
        }
1728
        return bytes;
1729
    }
1730
1731
    /**
1732
     * Return the bytes of the "fmt " chunk.
1733
     * @return {!Array<number>} The "fmt" chunk bytes.
1734
     * @throws {Error} if no "fmt " chunk is present.
1735
     * @private
1736
     */
1737
    getFmtBytes_() {
1738
        if (this.fmt.chunkId) {
1739
            return [].concat(
1740
                byteData_.pack(this.fmt.chunkId, fourCC_),
1741
                byteData_.pack(this.fmt.chunkSize, uInt32_),
1742
                byteData_.pack(this.fmt.audioFormat, uInt16_),
1743
                byteData_.pack(this.fmt.numChannels, uInt16_),
1744
                byteData_.pack(this.fmt.sampleRate, uInt32_),
1745
                byteData_.pack(this.fmt.byteRate, uInt32_),
1746
                byteData_.pack(this.fmt.blockAlign, uInt16_),
1747
                byteData_.pack(this.fmt.bitsPerSample, uInt16_),
1748
                this.getFmtExtensionBytes_()
1749
            );
1750
        }
1751
        throw Error("Could not find the 'fmt ' chunk");
1752
    }
1753
1754
    /**
1755
     * Return the bytes of the fmt extension fields.
1756
     * @return {!Array<number>} The fmt extension bytes.
1757
     * @private
1758
     */
1759
    getFmtExtensionBytes_() {
1760
        /** type {!Array<number>} */
1761
        let extension = [];
1762
        if (this.fmt.chunkSize > 16) {
1763
            extension = extension.concat(
1764
                byteData_.pack(this.fmt.cbSize, uInt16_));
1765
        }
1766
        if (this.fmt.chunkSize > 18) {
1767
            extension = extension.concat(
1768
                byteData_.pack(this.fmt.validBitsPerSample, uInt16_));
1769
        }
1770
        if (this.fmt.chunkSize > 20) {
1771
            extension = extension.concat(
1772
                byteData_.pack(this.fmt.dwChannelMask, uInt32_));
1773
        }
1774
        if (this.fmt.chunkSize > 24) {
1775
            extension = extension.concat(
1776
                byteData_.pack(this.fmt.subformat[0], uInt32_),
1777
                byteData_.pack(this.fmt.subformat[1], uInt32_),
1778
                byteData_.pack(this.fmt.subformat[2], uInt32_),
1779
                byteData_.pack(this.fmt.subformat[3], uInt32_));
1780
        }
1781
        return extension;
1782
    }
1783
1784
    /**
1785
     * Return the bytes of the "LIST" chunk.
1786
     * @return {!Array<number>} The "LIST" chunk bytes.
1787
     * @export for tests
1788
     */
1789
    getLISTBytes_() {
1790
        /** type {!Array<number>} */
1791
        let bytes = [];
1792
        for (let i=0; i<this.LIST.length; i++) {
1793
            /** type {!Array<number>} */
1794
            let subChunksBytes = this.getLISTSubChunksBytes_(
1795
                    this.LIST[i]["subChunks"], this.LIST[i]["format"]);
1796
            bytes = bytes.concat(
1797
                byteData_.pack(this.LIST[i]["chunkId"], fourCC_),
1798
                byteData_.pack(subChunksBytes.length + 4, uInt32_),
1799
                byteData_.pack(this.LIST[i]["format"], fourCC_),
1800
                subChunksBytes);
1801
        }
1802
        return bytes;
1803
    }
1804
1805
    /**
1806
     * Return the bytes of the sub chunks of a "LIST" chunk.
1807
     * @param {!Array<Object>} subChunks The "LIST" sub chunks.
1808
     * @param {string} format The format of the "LIST" chunk.
1809
     *      Currently supported values are "adtl" or "INFO".
1810
     * @return {!Array<number>} The sub chunk bytes.
1811
     * @private
1812
     */
1813
    getLISTSubChunksBytes_(subChunks, format) {
1814
        /** type {!Array<number>} */
1815
        let bytes = [];
1816
        for (let i=0; i<subChunks.length; i++) {
1817
            if (format == "INFO") {
1818
                bytes = bytes.concat(
1819
                    byteData_.pack(subChunks[i]["chunkId"], fourCC_),
1820
                    byteData_.pack(subChunks[i]["value"].length + 1, uInt32_),
1821
                    this.writeString_(
1822
                        subChunks[i]["value"], subChunks[i]["value"].length));
1823
                bytes.push(0);
1824
            } else if (format == "adtl") {
1825
                if (["labl", "note"].indexOf(subChunks[i]["chunkId"]) > -1) {
1826
                    bytes = bytes.concat(
1827
                        byteData_.pack(subChunks[i]["chunkId"], fourCC_),
1828
                        byteData_.pack(
1829
                            subChunks[i]["value"].length + 4 + 1, uInt32_),
1830
                        byteData_.pack(subChunks[i]["dwName"], uInt32_),
1831
                        this.writeString_(
1832
                            subChunks[i]["value"],
1833
                            subChunks[i]["value"].length));
1834
                    bytes.push(0);
1835
                }
1836
            } //else {
1837
            //    bytes = bytes.concat(
1838
            //        byteData_.pack(
1839
            //            subChunks[i]["chunkData"].length, uInt32_),
1840
            //        subChunks[i]["chunkData"]
1841
            //    );
1842
            //}
1843
            if (bytes.length % 2) {
1844
                bytes.push(0);
1845
            }
1846
        }
1847
        return bytes;
1848
    }
1849
1850
    /**
1851
     * Return the bytes of the "junk" chunk.
1852
     * @return {!Array<number>} The "junk" chunk bytes.
1853
     * @private
1854
     */
1855
    getJunkBytes_() {
1856
        /** type {!Array<number>} */
1857
        let bytes = [];
1858
        if (this.junk.chunkId) {
1859
            return bytes.concat(
1860
                byteData_.pack(this.junk.chunkId, fourCC_),
1861
                byteData_.pack(this.junk.chunkData.length, uInt32_),
1862
                this.junk.chunkData);
1863
        }
1864
        return bytes;
1865
    }
1866
1867
    /**
1868
     * Return "RIFF" if the container is "RF64", the current container name
1869
     * otherwise. Used to enforce "RIFF" when RF64 is not allowed.
1870
     * @return {string}
1871
     * @private
1872
     */
1873
    correctContainer_() {
1874
        return this.container == "RF64" ? "RIFF" : this.container;
1875
    }
1876
1877
    /**
1878
     * Set the string code of the bit depth based on the "fmt " chunk.
1879
     * @private
1880
     */
1881
    bitDepthFromFmt_() {
1882
        if (this.fmt.audioFormat == 3 && this.fmt.bitsPerSample == 32) {
1883
            this.bitDepth = "32f";
1884
        } else if (this.fmt.audioFormat == 6) {
1885
            this.bitDepth = "8a";
1886
        } else if (this.fmt.audioFormat == 7) {
1887
            this.bitDepth = "8m";
1888
        } else {
1889
            this.bitDepth = this.fmt.bitsPerSample.toString();
1890
        }
1891
    }
1892
1893
    /**
1894
     * Return a .wav file byte buffer with the data from the WaveFile object.
1895
     * The return value of this method can be written straight to disk.
1896
     * @return {!Uint8Array} The wav file bytes.
1897
     * @private
1898
     */
1899
    createWaveFile_() {
1900
        /** type {!Array<number>} */
1901
        let samplesBytes = this.samplesToBytes_();
1902
        /** type {!Array<number>} */
1903
        let fileBody = [].concat(
1904
            byteData_.pack(this.format, fourCC_),
1905
            this.getJunkBytes_(),
1906
            this.getDs64Bytes_(),
1907
            this.getBextBytes_(),
1908
            this.getFmtBytes_(),
1909
            this.getFactBytes_(),
1910
            byteData_.pack(this.data.chunkId, fourCC_),
1911
            byteData_.pack(samplesBytes.length, uInt32_),
1912
            samplesBytes,
1913
            this.getCueBytes_(),
1914
            this.getLISTBytes_());
1915
        // concat with the main header and return a .wav file
1916
        return new Uint8Array([].concat(
1917
            byteData_.pack(this.container, fourCC_),
1918
            byteData_.pack(fileBody.length, uInt32_),
1919
            fileBody));            
1920
    }
1921
}
1922
1923
module.exports = WaveFile;
1924