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