Passed
Push — master ( fb8bed...284c32 )
by Rafael S.
02:00
created

index.js (1 issue)

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;
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) {
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 milisenconds
771
     * @param {!string} labl The LIST adtl labl text of the marker.
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) {
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
     * Create a new "labl" subchunk in a "LIST" chunk of type "adtl".
809
     * @param {!number} dwName The ID of the cue point.
810
     * @param {!string} label The label for the cue point.
811
     * @private
812
     */
813
    setLabl_(dwName, label="") {
814
        let adtlIndex = this.getAdtlChunk_();
815
        if (adtlIndex === null) {
816
            this.LIST.push({
817
                "chunkId": "LIST",
818
                "chunkSize": 4,
819
                "format": "adtl",
820
                "chunkData": [],
821
                "subChunks": []});
822
            adtlIndex = this.getAdtlChunk_();
823
        }
824
        this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
825
    }
826
827
    /**
828
     * Create a new "labl" subchunk in a "LIST" chunk of type "adtl".
829
     * @param {!number} adtlIndex The index of the "adtl" LIST in this.LIST.
830
     * @param {!number} dwName The ID of the cue point.
831
     * @param {!string} label The label for the cue point.
832
     * @private
833
     */
834
    setLabelText_(adtlIndex, dwName, label) {
835
        this.LIST[adtlIndex]["subChunks"].push({
836
            "chunkId": "labl",
837
            "chunkSize": label.length,
838
            "dwName": dwName,
839
            "value": label
840
        });
841
        this.LIST[adtlIndex]["chunkSize"] += label.length + 4 + 4 + 4 + 1;
842
    }
843
844
    /**
845
     * Return the index of the "adtl" LIST in this.LIST.
846
     * @return {!number|null}
847
     * @private
848
     */
849
    getAdtlChunk_() {
850
        for (let i=0; i<this.LIST.length; i++) {
851
            if(this.LIST[i]["format"] == 'adtl') {
852
                return i;
853
            }
854
        }
855
        return null;
856
    }
857
858
    /**
859
     * Remove a cue point from a wave file.
860
     * @param {!number} index the index of the point. First is 1,
861
     *      second is 2, and so on.
862
     * @export
863
     */
864
    deleteCuePoint(index) {
865
        this.cue.chunkId = "cue ";
866
        let existingPoints = this.getCuePoints_();
867
        this.clearLISTadtl_();
868
        let len = this.cue.points.length;
869
        this.cue.points = [];
870
        for (let i=0; i<len; i++) {
871
            if (i + 1 != index) {
872
                this.setCuePoint_(
873
                    existingPoints[i]["dwPosition"],
874
                    i + 1,
875
                    existingPoints[i]["label"]);
876
            }
877
        }
878
        this.cue.dwCuePoints = this.cue.points.length;
879
        if (this.cue.dwCuePoints) {
880
            this.cue.chunkId = 'cue ';
881
        } else {
882
            this.cue.chunkId = '';
883
            this.clearLISTadtl_();
884
        }
885
    }
886
887
    /**
888
     * Push a new cue point in this.cue.points.
889
     * @param {!number} position The position in miliseconds
890
     * @param {!number} dwName the dwName of the cue point
891
     */
892
    setCuePoint_(position, dwName, label) {
893
        this.cue.points.push({
894
            "dwName": dwName,
895
            "dwPosition": position,
896
            "fccChunk": "data",
897
            "dwChunkStart": 0,
898
            "dwBlockStart": 0,
899
            "dwSampleOffset": position,
900
        });
901
        this.setLabl_(dwName, label);
902
    }
903
904
    /**
905
     * Return an array with the position of all cue points in the file.
906
     * @return {!Array<number>}
907
     */
908
    getCuePoints_() {
909
        let points = [];
910
        for (let i=0; i<this.cue.points.length; i++) {
911
            let point = {
912
                'dwPosition': this.cue.points[i]["dwPosition"],
913
                'label': this.getLabelForCuePoint_(
914
                    this.cue.points[i]["dwName"])};
915
            points.push(point);
916
        }
917
        return points;
918
    }
919
920
    /**
921
     * Return the label for a cue point.
922
     * @param {!number} pointDwName The ID of the cue point.
923
     * @return {!string}
924
     * @private
925
     */
926
    getLabelForCuePoint_(pointDwName) {
927
        let adtlIndex = this.getAdtlChunk_();
928
        if (adtlIndex !== null) {
929
            for (let i=0; i<this.LIST[adtlIndex]["subChunks"].length; i++) {
930
                if (this.LIST[adtlIndex]["subChunks"][i]["dwName"] ==
931
                        pointDwName) {
932
                    return this.LIST[adtlIndex]["subChunks"][i]["value"];
933
                }
934
            }
935
        }
936
        return "";
937
    }
938
939
    /**
940
     * Clear any LIST chunk labeled as "adtl".
941
     */
942
    clearLISTadtl_() {
943
        for (let i=0; i<this.LIST.length; i++) {
944
            if (this.LIST[i]["format"] == 'adtl') {
945
                this.LIST.splice(i);
946
            }
947
        }
948
    }
949
950
    /**
951
     * Return the index of a tag in a FILE chunk.
952
     * @param {!string} tag The tag name.
953
     * @return {!Object<string, number|null>}
954
     *      Object.LIST is the INFO index in LIST
955
     *      Object.TAG is the tag index in the INFO
956
     * @private
957
     */
958
    getTagIndex_(tag) {
959
        let index = {LIST: null, TAG: null};
960
        for (let i=0; i<this.LIST.length; i++) {
961
            if (this.LIST[i]["format"] == "INFO") {
962
                index.LIST = i;
963
                for (let j=0; j<this.LIST[i]["subChunks"].length; j++) {
964
                    if (this.LIST[i]["subChunks"][j]["chunkId"] == tag) {
965
                        index.TAG = j;
966
                        break;
967
                    }
968
                }
969
                break;
970
            }
971
        }
972
        return index;
973
    }
974
975
    /**
976
     * Fix a RIFF tag format if possible, throw an error otherwise.
977
     * @param {!string} tag The tag name.
978
     * @return {string} The tag name in proper fourCC format.
979
     * @private
980
     */
981
    fixTagName_(tag) {
982
        if (tag.constructor !== String) {
983
            throw new Error("Invalid tag name.");
984
        } else if(tag.length < 4) {
985
            for (let i=0; i<4-tag.length; i++) {
986
                tag += ' ';
987
            }
988
        }
989
        return tag;
990
    }
991
992
    /**
993
     * Return the closest greater number of bits for a number of bits that
994
     * do not fill a full sequence of bytes.
995
     * @param {!string} bitDepth The bit depth.
996
     * @return {!string}
997
     */
998
    realBitDepth_(bitDepth) {
999
        if (bitDepth != "32f") {
1000
            bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1).toString();
1001
        }
1002
        return bitDepth;
1003
    }
1004
1005
    /**
1006
     * Validate the header of the file.
1007
     * @throws {Error} If any property of the object appears invalid.
1008
     * @private
1009
     */
1010
    validateHeader_() {
1011
        this.validateBitDepth_();
1012
        this.validateNumChannels_();
1013
        this.validateSampleRate_();
1014
    }
1015
1016
    /**
1017
     * Validate the bit depth.
1018
     * @return {!boolean} True is the bit depth is valid.
1019
     * @throws {Error} If bit depth is invalid.
1020
     * @private
1021
     */
1022
    validateBitDepth_() {
1023
        if (!this.audioFormats_[this.bitDepth]) {
1024
            if (parseInt(this.bitDepth, 10) > 8 &&
1025
                    parseInt(this.bitDepth, 10) < 54) {
1026
                return true;
1027
            }
1028
            throw new Error("Invalid bit depth.");
1029
        }
1030
        return true;
1031
    }
1032
1033
    /**
1034
     * Validate the number of channels.
1035
     * @return {!boolean} True is the number of channels is valid.
1036
     * @throws {Error} If the number of channels is invalid.
1037
     * @private
1038
     */
1039
    validateNumChannels_() {
1040
        let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1041
        if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1042
            throw new Error("Invalid number of channels.");
1043
        }
1044
        return true;
1045
    }
1046
1047
    /**
1048
     * Validate the sample rate value.
1049
     * @return {!boolean} True is the sample rate is valid.
1050
     * @throws {Error} If the sample rate is invalid.
1051
     * @private
1052
     */
1053
    validateSampleRate_() {
1054
        let byteRate = this.fmt.numChannels *
1055
            (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1056
        if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1057
            throw new Error("Invalid sample rate.");
1058
        }
1059
        return true;
1060
    }
1061
1062
    /**
1063
     * Reset attributes that should emptied when a file is
1064
     * created with the fromScratch() or fromBuffer() methods.
1065
     * @private
1066
     */
1067
    clearHeader_() {
1068
        this.fmt.cbSize = 0;
1069
        this.fmt.validBitsPerSample = 0;
1070
        this.fact.chunkId = "";
1071
        this.ds64.chunkId = "";
1072
    }
1073
1074
    /**
1075
     * Make the file 16-bit if it is not.
1076
     * @private
1077
     */
1078
    assure16Bit_() {
1079
        this.assureUncompressed_();
1080
        if (this.bitDepth != "16") {
1081
            this.toBitDepth("16");
1082
        }
1083
    }
1084
1085
    /**
1086
     * Uncompress the samples in case of a compressed file.
1087
     * @private
1088
     */
1089
    assureUncompressed_() {
1090
        if (this.bitDepth == "8a") {
1091
            this.fromALaw();
1092
        } else if(this.bitDepth == "8m") {
1093
            this.fromMuLaw();
1094
        } else if (this.bitDepth == "4") {
1095
            this.fromIMAADPCM();
1096
        }
1097
    }
1098
1099
    /**
1100
     * Interleave the samples in case they are de-Interleaved.
1101
     * @private
1102
     */
1103
    assureInterleaved_() {
1104
        if (!this.isInterleaved) {
1105
            this.interleave();
1106
        }
1107
    }
1108
1109
    /**
1110
     * Set up to work wih big-endian or little-endian files.
1111
     * The types used are changed to LE or BE. If the
1112
     * the file is big-endian (RIFX), true is returned.
1113
     * @return {!boolean} True if the file is RIFX.
1114
     * @private
1115
     */
1116
    LEorBE_() {
1117
        let bigEndian = this.container === "RIFX";
1118
        uInt16_["be"] = bigEndian;
1119
        uInt32_["be"] = bigEndian;
1120
        return bigEndian;
1121
    }
1122
1123
    /**
1124
     * Find a chunk by its fourCC_ in a array of RIFF chunks.
1125
     * @param {!Array<!Object>} chunks The wav file chunks.
1126
     * @param {!string} chunkId The chunk fourCC_.
1127
     * @param {boolean} multiple True if there may be multiple chunks
1128
     *      with the same chunkId.
1129
     * @return {Object|Array<Object>|null}
1130
     * @private
1131
     */
1132
    findChunk_(chunks, chunkId, multiple=false) {
1133
        let chunk = [];
1134
        for (let i=0; i<chunks.length; i++) {
1135
            if (chunks[i]["chunkId"] == chunkId) {
1136
                if (multiple) {
1137
                    chunk.push(chunks[i]);
1138
                } else {
1139
                    return chunks[i];
1140
                }
1141
            }
1142
        }
1143
        if (chunkId == "LIST") {
1144
            return chunk.length ? chunk : null;
1145
        }
1146
        return null;
1147
    }
1148
1149
    /**
1150
     * Read the RIFF chunk a wave file.
1151
     * @param {!Uint8Array|!Array<number>} bytes A wav buffer.
1152
     * @throws {Error} If no "RIFF" chunk is found.
1153
     * @private
1154
     */
1155
    readRIFFChunk_(bytes) {
1156
        this.head_ = 0;
1157
        this.container = this.readString_(bytes, 4);
1158
        if (["RIFF", "RIFX", "RF64"].indexOf(this.container) === -1) {
1159
            throw Error("Not a supported format.");
1160
        }
1161
        this.LEorBE_();
1162
        this.chunkSize = this.read_(bytes, uInt32_);
1163
        this.format = this.readString_(bytes, 4);
1164
        if (this.format != "WAVE") {
1165
            throw Error("Could not find the 'WAVE' format identifier");
1166
        }
1167
    }
1168
1169
    /**
1170
     * Read the "fmt " chunk of a wave file.
1171
     * @param {!Array<!Object>} chunks The wav file chunks.
1172
     * @throws {Error} If no "fmt " chunk is found.
1173
     * @private
1174
     */
1175
    readFmtChunk_(chunks) {
1176
        let chunk = this.findChunk_(chunks, "fmt ");
1177
        if (chunk) {
1178
            this.head_ = 0;
1179
            let chunkData = chunk["chunkData"];
1180
            this.fmt.chunkId = chunk["chunkId"];
1181
            this.fmt.chunkSize = chunk["chunkSize"];
1182
            this.fmt.audioFormat = this.read_(chunkData, uInt16_);
1183
            this.fmt.numChannels = this.read_(chunkData, uInt16_);
1184
            this.fmt.sampleRate = this.read_(chunkData, uInt32_);
1185
            this.fmt.byteRate = this.read_(chunkData, uInt32_);
1186
            this.fmt.blockAlign = this.read_(chunkData, uInt16_);
1187
            this.fmt.bitsPerSample = this.read_(chunkData, uInt16_);
1188
            this.readFmtExtension_(chunkData);
1189
        } else {
1190
            throw Error("Could not find the 'fmt ' chunk");
1191
        }
1192
    }
1193
1194
    /**
1195
     * Read the "fmt " chunk extension.
1196
     * @param {!Array<number>} chunkData The "fmt " chunk.
1197
     * @private
1198
     */
1199
    readFmtExtension_(chunkData) {
1200
        if (this.fmt.chunkSize > 16) {
1201
            this.fmt.cbSize = this.read_(
1202
                chunkData, uInt16_);
1203
            if (this.fmt.chunkSize > 18) {
1204
                this.fmt.validBitsPerSample = this.read_(chunkData, uInt16_);
1205
                if (this.fmt.chunkSize > 20) {
1206
                    this.fmt.dwChannelMask = this.read_(chunkData, uInt32_);
1207
                    this.fmt.subformat = [
1208
                        this.read_(chunkData, uInt32_),
1209
                        this.read_(chunkData, uInt32_),
1210
                        this.read_(chunkData, uInt32_),
1211
                        this.read_(chunkData, uInt32_)];
1212
                }
1213
            }
1214
        }
1215
    }
1216
1217
    /**
1218
     * Read the "fact" chunk of a wav file.
1219
     * @param {!Array<Object>} chunks The wav file chunks.
1220
     * @throws {Error} If no "fact" chunk is found.
1221
     * @private
1222
     */
1223
    readFactChunk_(chunks) {
1224
        let chunk = this.findChunk_(chunks, "fact");
1225
        if (chunk) {
1226
            this.head_ = 0;
1227
            this.fact.chunkId = chunk["chunkId"];
1228
            this.fact.chunkSize = chunk["chunkSize"];
1229
            this.fact.dwSampleLength = this.read_(chunk["chunkData"], uInt32_);
1230
        }
1231
    }
1232
1233
    /**
1234
     * Read the "cue " chunk of a wave file.
1235
     * @param {!Array<Object>} chunks The RIFF file chunks.
1236
     * @private
1237
     */
1238
    readCueChunk_(chunks) {
1239
        let chunk = this.findChunk_(chunks, "cue ");
1240
        if (chunk) {
1241
            this.head_ = 0;
1242
            let chunkData = chunk["chunkData"];
1243
            this.cue.chunkId = chunk["chunkId"];
1244
            this.cue.chunkSize = chunk["chunkSize"];
1245
            this.cue.dwCuePoints = this.read_(chunkData, uInt32_);
1246
            for (let i=0; i<this.cue.dwCuePoints; i++) {
1247
                this.cue.points.push({
1248
                    "dwName": this.read_(chunkData, uInt32_),
1249
                    "dwPosition": this.read_(chunkData, uInt32_),
1250
                    "fccChunk": this.readString_(chunkData, 4),
1251
                    "dwChunkStart": this.read_(chunkData, uInt32_),
1252
                    "dwBlockStart": this.read_(chunkData, uInt32_),
1253
                    "dwSampleOffset": this.read_(chunkData, uInt32_),
1254
                });
1255
            }
1256
        }
1257
    }
1258
1259
    /**
1260
     * Read the "data" chunk of a wave file.
1261
     * @param {!Array<Object>} chunks The RIFF file chunks.
1262
     * @throws {Error} If no "data" chunk is found.
1263
     * @private
1264
     */
1265
    readDataChunk_(chunks) {
1266
        let chunk = this.findChunk_(chunks, "data");
1267
        if (chunk) {
1268
            this.data.chunkId = "data";
1269
            this.data.chunkSize = chunk["chunkSize"];
1270
            this.samplesFromBytes_(chunk["chunkData"]);
1271
        } else {
1272
            throw Error("Could not find the 'data' chunk");
1273
        }
1274
    }
1275
1276
    /**
1277
     * Read the "bext" chunk of a wav file.
1278
     * @param {!Array<Object>} chunks The wav file chunks.
1279
     * @private
1280
     */
1281
    readBextChunk_(chunks) {
1282
        let chunk = this.findChunk_(chunks, "bext");
1283
        if (chunk) {
1284
            this.head_ = 0;
1285
            let chunkData = chunk["chunkData"];
1286
            this.bext.chunkId = chunk["chunkId"];
1287
            this.bext.chunkSize = chunk["chunkSize"];
1288
            this.bext.description = this.readString_(chunkData, 256);
1289
            this.bext.originator = this.readString_(chunkData, 32);
1290
            this.bext.originatorReference = this.readString_(chunkData, 32);
1291
            this.bext.originationDate = this.readString_(chunkData, 10);
1292
            this.bext.originationTime = this.readString_(chunkData, 8);
1293
            this.bext.timeReference = [
1294
                this.read_(chunkData, uInt32_),
1295
                this.read_(chunkData, uInt32_)];
1296
            this.bext.version = this.read_(chunkData, uInt16_);
1297
            this.bext.UMID = this.readString_(chunkData, 64);
1298
            this.bext.loudnessValue = this.read_(chunkData, uInt16_);
1299
            this.bext.loudnessRange = this.read_(chunkData, uInt16_);
1300
            this.bext.maxTruePeakLevel = this.read_(chunkData, uInt16_);
1301
            this.bext.maxMomentaryLoudness = this.read_(chunkData, uInt16_);
1302
            this.bext.maxShortTermLoudness = this.read_(chunkData, uInt16_);
1303
            this.bext.reserved = this.readString_(chunkData, 180);
1304
            this.bext.codingHistory = this.readString_(
1305
                chunkData, this.bext.chunkSize - 602);
1306
        }
1307
    }
1308
1309
    /**
1310
     * Read the "ds64" chunk of a wave file.
1311
     * @param {!Array<Object>} chunks The wav file chunks.
1312
     * @throws {Error} If no "ds64" chunk is found and the file is RF64.
1313
     * @private
1314
     */
1315
    readDs64Chunk_(chunks) {
1316
        let chunk = this.findChunk_(chunks, "ds64");
1317
        if (chunk) {
1318
            this.head_ = 0;
1319
            let chunkData = chunk["chunkData"];
1320
            this.ds64.chunkId = chunk["chunkId"];
1321
            this.ds64.chunkSize = chunk["chunkSize"];
1322
            this.ds64.riffSizeHigh = this.read_(chunkData, uInt32_);
1323
            this.ds64.riffSizeLow = this.read_(chunkData, uInt32_);
1324
            this.ds64.dataSizeHigh = this.read_(chunkData, uInt32_);
1325
            this.ds64.dataSizeLow = this.read_(chunkData, uInt32_);
1326
            this.ds64.originationTime = this.read_(chunkData, uInt32_);
1327
            this.ds64.sampleCountHigh = this.read_(chunkData, uInt32_);
1328
            this.ds64.sampleCountLow = this.read_(chunkData, uInt32_);
1329
            //if (this.ds64.chunkSize > 28) {
1330
            //    this.ds64.tableLength = byteData_.unpack(
1331
            //        chunkData.slice(28, 32), uInt32_);
1332
            //    this.ds64.table = chunkData.slice(
1333
            //         32, 32 + this.ds64.tableLength); 
1334
            //}
1335
        } else {
1336
            if (this.container == "RF64") {
1337
                throw Error("Could not find the 'ds64' chunk");    
1338
            }
1339
        }
1340
    }
1341
1342
    /**
1343
     * Read the "LIST" chunks of a wave file.
1344
     * @param {!Array<Object>} chunks The wav file chunks.
1345
     * @private
1346
     */
1347
    readLISTChunk_(chunks) {
1348
        let listChunks = this.findChunk_(chunks, "LIST", true);
1349
        if (listChunks === null) {
1350
            return;
1351
        }
1352
        for (let j=0; j<listChunks.length; j++) {
1353
            let subChunk = listChunks[j];
1354
            this.LIST.push({
1355
                "chunkId": subChunk["chunkId"],
1356
                "chunkSize": subChunk["chunkSize"],
1357
                "format": subChunk["format"],
1358
                "chunkData": subChunk["chunkData"],
1359
                "subChunks": []});
1360
            for (let x=0; x<subChunk["subChunks"].length; x++) {
1361
                this.readLISTSubChunks_(subChunk["subChunks"][x],
1362
                    subChunk["format"]);
1363
            }
1364
        }
1365
    }
1366
1367
    /**
1368
     * Read the sub chunks of a "LIST" chunk.
1369
     * @param {!Object} subChunk The "LIST" subchunks.
1370
     * @param {!string} format The "LIST" format, "adtl" or "INFO".
1371
     * @private
1372
     */
1373
    readLISTSubChunks_(subChunk, format) {
1374
        // 'labl', 'note', 'ltxt', 'file'
1375
        if (format == 'adtl') {
1376
            if (["labl", "note"].indexOf(subChunk["chunkId"]) > -1) {
1377
                this.LIST[this.LIST.length - 1]["subChunks"].push({
1378
                    "chunkId": subChunk["chunkId"],
1379
                    "chunkSize": subChunk["chunkSize"],
1380
                    "dwName": byteData_.unpack(
1381
                        subChunk["chunkData"].slice(0, 4),uInt32_),
1382
                    "value": this.readZSTR_(subChunk["chunkData"].slice(4))
1383
                });
1384
            }
1385
        // RIFF 'INFO' tags like ICRD, ISFT, ICMT
1386
        // https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html#Info
1387
        } else if(format == 'INFO') {
1388
            this.LIST[this.LIST.length - 1]["subChunks"].push({
1389
                "chunkId": subChunk["chunkId"],
1390
                "chunkSize": subChunk["chunkSize"],
1391
                "value": this.readZSTR_(subChunk["chunkData"].slice(0))
1392
            });
1393
        } //else {
1394
        //    this.LIST[this.LIST.length - 1]["subChunks"].push({
1395
        //        "chunkId": subChunk["chunkId"],
1396
        //        "chunkSize": subChunk["chunkSize"],
1397
        //        "value": subChunk["chunkData"]
1398
        //    });
1399
        //}
1400
    }
1401
1402
    /**
1403
     * Read the "junk" chunk of a wave file.
1404
     * @param {!Array<Object>} chunks The wav file chunks.
1405
     * @private
1406
     */
1407
    readJunkChunk_(chunks) {
1408
        let chunk = this.findChunk_(chunks, "junk");
1409
        if (chunk) {
1410
            this.junk = {
1411
                "chunkId": chunk["chunkId"],
1412
                "chunkSize": chunk["chunkSize"],
1413
                "chunkData": chunk["chunkData"]
1414
            };
1415
        }
1416
    }
1417
1418
    /**
1419
     * Read bytes as a ZSTR string.
1420
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1421
     * @return {!string} The string.
1422
     * @private
1423
     */
1424
    readZSTR_(bytes) {
1425
        let str = "";
1426
        for (let i=0; i<bytes.length; i++) {
1427
            if (bytes[i] === 0) {
1428
                break;
1429
            }
1430
            str += byteData_.unpack([bytes[i]], chr_);
1431
        }
1432
        return str;
1433
    }
1434
1435
    /**
1436
     * Read bytes as a string from a RIFF chunk.
1437
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1438
     * @param {!number} maxSize the max size of the string.
1439
     * @return {!string} The string.
1440
     * @private
1441
     */
1442
    readString_(bytes, maxSize) {
1443
        let str = "";
1444
        for (let i=0; i<maxSize; i++) {
1445
            str += byteData_.unpack([bytes[this.head_]], chr_);
1446
            this.head_++;
1447
        }
1448
        return str;
1449
    }
1450
1451
    /**
1452
     * Read a number from a chunk.
1453
     * @param {!Array<number>|!Uint8Array} bytes The chunk bytes.
1454
     * @param {!Object} bdType The type definition.
1455
     * @return {number} The number.
1456
     * @private
1457
     */
1458
    read_(bytes, bdType) {
1459
        let size = bdType["bits"] / 8;
1460
        let value = byteData_.unpack(
1461
            bytes.slice(this.head_, this.head_ + size), bdType);
1462
        this.head_ += size;
1463
        return value;
1464
    }
1465
1466
    /**
1467
     * Write a variable size string as bytes. If the string is smaller
1468
     * than the max size the output array is filled with 0s.
1469
     * @param {!string} str The string to be written as bytes.
1470
     * @param {!number} maxSize the max size of the string.
1471
     * @return {!Array<number>} The bytes.
1472
     * @private
1473
     */
1474
    writeString_(str, maxSize, push=true) {        
1475
        let bytes = byteData_.packArray(str, chr_);
1476
        if (push) {
1477
            for (let i=bytes.length; i<maxSize; i++) {
1478
                bytes.push(0);
1479
            }    
1480
        }
1481
        return bytes;
1482
    }
1483
1484
    /**
1485
     * Turn the samples to bytes.
1486
     * @return {!Array<number>} The bytes.
1487
     * @private
1488
     */
1489
    samplesToBytes_() {
1490
        let bdType = {
1491
            "be": this.container === "RIFX",
1492
            "bits": this.fmt.bitsPerSample == 4 ? 8 : this.fmt.bitsPerSample,
1493
            "float": this.fmt.audioFormat == 3 ? true : false
1494
        };
1495
        bdType["signed"] = bdType["bits"] == 8 ? false : true;
1496
        let bytes = byteData_.packArray(this.data.samples, bdType);
1497
        return bytes;
1498
    }
1499
1500
    /**
1501
     * Turn bytes to samples and load them in the data.samples property.
1502
     * @param {!Array<number>|!Uint8Array} bytes The bytes.
1503
     * @private
1504
     */
1505
    samplesFromBytes_(bytes) {
1506
        let bdType = {
1507
            "be": this.container === "RIFX",
1508
            "bits": this.fmt.bitsPerSample == 4 ? 8 : this.fmt.bitsPerSample,
1509
            "float": this.fmt.audioFormat == 3 ? true : false
1510
        };
1511
        bdType["signed"] = bdType["bits"] == 8 ? false : true;
1512
        this.data.samples = byteData_.unpackArray(bytes, bdType);
1513
    }
1514
1515
    /**
1516
     * Return the bytes of the "bext" chunk.
1517
     * @return {!Array<number>} The "bext" chunk bytes.
1518
     * @private
1519
     */
1520
    getBextBytes_() {
1521
        let bextBytes = [];
1522
        if (this.bext.chunkId) {
1523
            return bextBytes.concat(
1524
                byteData_.pack(this.bext.chunkId, fourCC_),
1525
                byteData_.pack(602 + this.bext.codingHistory.length, uInt32_),
1526
                this.writeString_(this.bext.description, 256),
1527
                this.writeString_(this.bext.originator, 32),
1528
                this.writeString_(this.bext.originatorReference, 32),
1529
                this.writeString_(this.bext.originationDate, 10),
1530
                this.writeString_(this.bext.originationTime, 8),
1531
                byteData_.pack(this.bext.timeReference[0], uInt32_),
1532
                byteData_.pack(this.bext.timeReference[1], uInt32_),
1533
                byteData_.pack(this.bext.version, uInt16_),
1534
                this.writeString_(this.bext.UMID, 64),
1535
                byteData_.pack(this.bext.loudnessValue, uInt16_),
1536
                byteData_.pack(this.bext.loudnessRange, uInt16_),
1537
                byteData_.pack(this.bext.maxTruePeakLevel, uInt16_),
1538
                byteData_.pack(this.bext.maxMomentaryLoudness, uInt16_),
1539
                byteData_.pack(this.bext.maxShortTermLoudness, uInt16_),
1540
                this.writeString_(this.bext.reserved, 180),
1541
                this.writeString_(
1542
                    this.bext.codingHistory, this.bext.codingHistory.length));
1543
        }
1544
        return bextBytes;
1545
    }
1546
1547
    /**
1548
     * Return the bytes of the "ds64" chunk.
1549
     * @return {!Array<number>} The "ds64" chunk bytes.
1550
     * @private
1551
     */
1552
    getDs64Bytes_() {
1553
        let ds64Bytes = [];
1554
        if (this.ds64.chunkId) {
1555
            ds64Bytes = ds64Bytes.concat(
1556
                byteData_.pack(this.ds64.chunkId, fourCC_),
1557
                byteData_.pack(this.ds64.chunkSize, uInt32_), // 
1558
                byteData_.pack(this.ds64.riffSizeHigh, uInt32_),
1559
                byteData_.pack(this.ds64.riffSizeLow, uInt32_),
1560
                byteData_.pack(this.ds64.dataSizeHigh, uInt32_),
1561
                byteData_.pack(this.ds64.dataSizeLow, uInt32_),
1562
                byteData_.pack(this.ds64.originationTime, uInt32_),
1563
                byteData_.pack(this.ds64.sampleCountHigh, uInt32_),
1564
                byteData_.pack(this.ds64.sampleCountLow, uInt32_));          
1565
        }
1566
        //if (this.ds64.tableLength) {
1567
        //    ds64Bytes = ds64Bytes.concat(
1568
        //        byteData_.pack(this.ds64.tableLength, uInt32_),
1569
        //        this.ds64.table);
1570
        //}
1571
        return ds64Bytes;
1572
    }
1573
1574
    /**
1575
     * Return the bytes of the "cue " chunk.
1576
     * @return {!Array<number>} The "cue " chunk bytes.
1577
     * @private
1578
     */
1579
    getCueBytes_() {
1580
        let cueBytes = [];
1581
        if (this.cue.chunkId) {
1582
            let cuePointsBytes = this.getCuePointsBytes_();
1583
            return cueBytes.concat(
1584
                byteData_.pack(this.cue.chunkId, fourCC_),
1585
                byteData_.pack(cuePointsBytes.length + 4, uInt32_),
1586
                byteData_.pack(this.cue.dwCuePoints, uInt32_),
1587
                cuePointsBytes);
1588
        }
1589
        return cueBytes;
1590
    }
1591
1592
    /**
1593
     * Return the bytes of the "cue " points.
1594
     * @return {!Array<number>} The "cue " points as an array of bytes.
1595
     * @private
1596
     */
1597
    getCuePointsBytes_() {
1598
        let points = [];
1599
        for (let i=0; i<this.cue.dwCuePoints; i++) {
1600
            points = points.concat(
1601
                byteData_.pack(this.cue.points[i]["dwName"], uInt32_),
1602
                byteData_.pack(this.cue.points[i]["dwPosition"], uInt32_),
1603
                byteData_.pack(this.cue.points[i]["fccChunk"], fourCC_),
1604
                byteData_.pack(this.cue.points[i]["dwChunkStart"], uInt32_),
1605
                byteData_.pack(this.cue.points[i]["dwBlockStart"], uInt32_),
1606
                byteData_.pack(this.cue.points[i]["dwSampleOffset"], uInt32_));
1607
        }
1608
        return points;
1609
    }
1610
1611
    /**
1612
     * Return the bytes of the "fact" chunk.
1613
     * @return {!Array<number>} The "fact" chunk bytes.
1614
     * @private
1615
     */
1616
    getFactBytes_() {
1617
        let factBytes = [];
1618
        if (this.fact.chunkId) {
1619
            return factBytes.concat(
1620
                byteData_.pack(this.fact.chunkId, fourCC_),
1621
                byteData_.pack(this.fact.chunkSize, uInt32_),
1622
                byteData_.pack(this.fact.dwSampleLength, uInt32_));
1623
        }
1624
        return factBytes;
1625
    }
1626
1627
    /**
1628
     * Return the bytes of the "fmt " chunk.
1629
     * @return {!Array<number>} The "fmt" chunk bytes.
1630
     * @throws {Error} if no "fmt " chunk is present.
1631
     * @private
1632
     */
1633
    getFmtBytes_() {
1634
        if (this.fmt.chunkId) {
1635
            return [].concat(
1636
                byteData_.pack(this.fmt.chunkId, fourCC_),
1637
                byteData_.pack(this.fmt.chunkSize, uInt32_),
1638
                byteData_.pack(this.fmt.audioFormat, uInt16_),
1639
                byteData_.pack(this.fmt.numChannels, uInt16_),
1640
                byteData_.pack(this.fmt.sampleRate, uInt32_),
1641
                byteData_.pack(this.fmt.byteRate, uInt32_),
1642
                byteData_.pack(this.fmt.blockAlign, uInt16_),
1643
                byteData_.pack(this.fmt.bitsPerSample, uInt16_),
1644
                this.getFmtExtensionBytes_()
1645
            );
1646
        } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
1647
            throw Error("Could not find the 'fmt ' chunk");
1648
        }
1649
    }
1650
1651
    /**
1652
     * Return the bytes of the fmt extension fields.
1653
     * @return {!Array<number>} The fmt extension bytes.
1654
     * @private
1655
     */
1656
    getFmtExtensionBytes_() {
1657
        let extension = [];
1658
        if (this.fmt.chunkSize > 16) {
1659
            extension = extension.concat(
1660
                byteData_.pack(this.fmt.cbSize, uInt16_));
1661
        }
1662
        if (this.fmt.chunkSize > 18) {
1663
            extension = extension.concat(
1664
                byteData_.pack(this.fmt.validBitsPerSample, uInt16_));
1665
        }
1666
        if (this.fmt.chunkSize > 20) {
1667
            extension = extension.concat(
1668
                byteData_.pack(this.fmt.dwChannelMask, uInt32_));
1669
        }
1670
        if (this.fmt.chunkSize > 24) {
1671
            extension = extension.concat(
1672
                byteData_.pack(this.fmt.subformat[0], uInt32_),
1673
                byteData_.pack(this.fmt.subformat[1], uInt32_),
1674
                byteData_.pack(this.fmt.subformat[2], uInt32_),
1675
                byteData_.pack(this.fmt.subformat[3], uInt32_));
1676
        }
1677
        return extension;
1678
    }
1679
1680
    /**
1681
     * Return the bytes of the "LIST" chunk.
1682
     * @return {!Array<number>} The "LIST" chunk bytes.
1683
     * @export for tests
1684
     */
1685
    getLISTBytes_() {
1686
        let bytes = [];
1687
        for (let i=0; i<this.LIST.length; i++) {
1688
            let subChunksBytes = this.getLISTSubChunksBytes_(
1689
                    this.LIST[i]["subChunks"], this.LIST[i]["format"]);
1690
            bytes = bytes.concat(
1691
                byteData_.pack(this.LIST[i]["chunkId"], fourCC_),
1692
                byteData_.pack(subChunksBytes.length + 4, uInt32_),
1693
                byteData_.pack(this.LIST[i]["format"], fourCC_),
1694
                subChunksBytes);
1695
        }
1696
        return bytes;
1697
    }
1698
1699
    /**
1700
     * Return the bytes of the sub chunks of a "LIST" chunk.
1701
     * @param {!Array<Object>} subChunks The "LIST" sub chunks.
1702
     * @param {!string} format The format of the "LIST" chunk.
1703
     *      Currently supported values are "adtl" or "INFO".
1704
     * @return {!Array<number>} The sub chunk bytes.
1705
     * @private
1706
     */
1707
    getLISTSubChunksBytes_(subChunks, format) {
1708
        let bytes = [];
1709
        for (let i=0; i<subChunks.length; i++) {
1710
            if (format == "INFO") {
1711
                bytes = bytes.concat(
1712
                    byteData_.pack(subChunks[i]["chunkId"], fourCC_),
1713
                    byteData_.pack(subChunks[i]["value"].length + 1, uInt32_),
1714
                    this.writeString_(
1715
                        subChunks[i]["value"], subChunks[i]["value"].length));
1716
                bytes.push(0);
1717
            } else if (format == "adtl") {
1718
                if (["labl", "note"].indexOf(subChunks[i]["chunkId"]) > -1) {
1719
                    bytes = bytes.concat(
1720
                        byteData_.pack(subChunks[i]["chunkId"], fourCC_),
1721
                        byteData_.pack(
1722
                            subChunks[i]["value"].length + 4 + 1, uInt32_),
1723
                        byteData_.pack(subChunks[i]["dwName"], uInt32_),
1724
                        this.writeString_(
1725
                            subChunks[i]["value"],
1726
                            subChunks[i]["value"].length));
1727
                    bytes.push(0);
1728
                }
1729
            } //else {
1730
            //    bytes = bytes.concat(
1731
            //        byteData_.pack(
1732
            //            subChunks[i]["chunkData"].length, uInt32_),
1733
            //        subChunks[i]["chunkData"]
1734
            //    );
1735
            //}
1736
            if (bytes.length % 2) {
1737
                bytes.push(0);
1738
            }
1739
        }
1740
        return bytes;
1741
    }
1742
1743
    /**
1744
     * Return the bytes of the "junk" chunk.
1745
     * @return {!Array<number>} The "junk" chunk bytes.
1746
     * @private
1747
     */
1748
    getJunkBytes_() {
1749
        let junkBytes = [];
1750
        if (this.junk.chunkId) {
1751
            return junkBytes.concat(
1752
                byteData_.pack(this.junk.chunkId, fourCC_),
1753
                byteData_.pack(this.junk.chunkData.length, uInt32_),
1754
                this.junk.chunkData);
1755
        }
1756
        return junkBytes;
1757
    }
1758
1759
    /**
1760
     * Return "RIFF" if the container is "RF64", the current container name
1761
     * otherwise. Used to enforce "RIFF" when RF64 is not allowed.
1762
     * @return {!string}
1763
     * @private
1764
     */
1765
    correctContainer_() {
1766
        return this.container == "RF64" ? "RIFF" : this.container;
1767
    }
1768
1769
    /**
1770
     * Set the string code of the bit depth based on the "fmt " chunk.
1771
     * @private
1772
     */
1773
    bitDepthFromFmt_() {
1774
        if (this.fmt.audioFormat == 3 && this.fmt.bitsPerSample == 32) {
1775
            this.bitDepth = "32f";
1776
        } else if (this.fmt.audioFormat == 6) {
1777
            this.bitDepth = "8a";
1778
        } else if (this.fmt.audioFormat == 7) {
1779
            this.bitDepth = "8m";
1780
        } else {
1781
            this.bitDepth = this.fmt.bitsPerSample.toString();
1782
        }
1783
    }
1784
1785
    /**
1786
     * Return a .wav file byte buffer with the data from the WaveFile object.
1787
     * The return value of this method can be written straight to disk.
1788
     * @return {!Uint8Array} The wav file bytes.
1789
     * @private
1790
     */
1791
    createWaveFile_() {
1792
        let samplesBytes = this.samplesToBytes_();
1793
        let fileBody = [].concat(
1794
            byteData_.pack(this.format, fourCC_),
1795
            this.getJunkBytes_(),
1796
            this.getDs64Bytes_(),
1797
            this.getBextBytes_(),
1798
            this.getFmtBytes_(),
1799
            this.getFactBytes_(),
1800
            byteData_.pack(this.data.chunkId, fourCC_),
1801
            byteData_.pack(samplesBytes.length, uInt32_),
1802
            samplesBytes,
1803
            this.getCueBytes_(),
1804
            this.getLISTBytes_());
1805
        return new Uint8Array([].concat(
1806
            byteData_.pack(this.container, fourCC_),
1807
            byteData_.pack(fileBody.length, uInt32_),
1808
            fileBody));            
1809
    }
1810
}
1811
1812
module.exports = WaveFile;
1813