Passed
Push — master ( e41145...59df0c )
by Rafael S.
02:39
created

index.js (2 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;
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) {
0 ignored issues
show
The parameter labl is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

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