Passed
Push — master ( 924f69...058802 )
by Rafael S.
03:36
created

index.js (3 issues)

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