Passed
Push — master ( f844b0...b17e08 )
by Rafael S.
02:14
created

index.js (3 issues)

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