Completed
Branch v8.x (5bdf7b)
by Rafael S.
02:02
created

index.js (5 issues)

1
/*
2
 * wavefile: Read and write wave files.
3
 * https://github.com/rochars/wavefile
4
 *
5
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining
8
 * a copy of this software and associated documentation files (the
9
 * "Software"), to deal in the Software without restriction, including
10
 * without limitation the rights to use, copy, modify, merge, publish,
11
 * distribute, sublicense, and/or sell copies of the Software, and to
12
 * permit persons to whom the Software is furnished to do so, subject to
13
 * the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be
16
 * included in all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
 *
26
 */
27
28
/**
29
 * @fileoverview The WaveFile class.
30
 */
31
32
/** @module wavefile */
33
34
import bitdepth from 'bitdepth';
35
import riffChunks from 'riff-chunks';
36
import {decode as decodeADPCM, encode as encodeADPCM} from 'imaadpcm';
37
import alawmulaw from 'alawmulaw';
38
import {encode, decode} from 'base64-arraybuffer-es6';
0 ignored issues
show
The variable decode already seems to be declared on line 36. Are you sure you want to import using decode as name?
Loading history...
The variable encode already seems to be declared on line 36. Are you sure you want to import using encode as name?
Loading history...
39
import {pack, unpackFrom, unpackString, packStringTo, packTo,
40
  packString, unpackArray, packArrayTo} from 'byte-data';
41
42
/**
43
 * @type {!Object}
44
 * @private
45
 */
46
let uInt16_ = {bits: 16};
47
/**
48
 * @type {!Object}
49
 * @private
50
 */
51
let uInt32_ = {bits: 32};
52
53
/**
54
 * Class representing a wav file.
55
 */
56
export default class WaveFile {
57
58
  /**
59
   * @param {?Uint8Array} bytes A wave file buffer.
60
   * @throws {Error} If no 'RIFF' chunk is found.
61
   * @throws {Error} If no 'fmt ' chunk is found.
62
   * @throws {Error} If no 'data' chunk is found.
63
   */
64
  constructor(bytes=null) {
65
    /**
66
     * The container identifier.
67
     * 'RIFF', 'RIFX' and 'RF64' are supported.
68
     * @type {string}
69
     * @export
70
     */
71
    this.container = '';
72
    /**
73
     * @type {number}
74
     * @export
75
     */
76
    this.chunkSize = 0;
77
    /**
78
     * The format.
79
     * Always 'WAVE'.
80
     * @type {string}
81
     * @export
82
     */
83
    this.format = '';
84
    /**
85
     * The data of the 'fmt' chunk.
86
     * @type {!Object<string, *>}
87
     * @export
88
     */
89
    this.fmt = {
90
      /** @export @type {string} */
91
      chunkId: '',
92
      /** @export @type {number} */
93
      chunkSize: 0,
94
      /** @export @type {number} */
95
      audioFormat: 0,
96
      /** @export @type {number} */
97
      numChannels: 0,
98
      /** @export @type {number} */
99
      sampleRate: 0,
100
      /** @export @type {number} */
101
      byteRate: 0,
102
      /** @export @type {number} */
103
      blockAlign: 0,
104
      /** @export @type {number} */
105
      bitsPerSample: 0,
106
      /** @export @type {number} */
107
      cbSize: 0,
108
      /** @export @type {number} */
109
      validBitsPerSample: 0,
110
      /** @export @type {number} */
111
      dwChannelMask: 0,
112
      /**
113
       * 4 32-bit values representing a 128-bit ID
114
       * @export @type {!Array<number>}
115
       */
116
      subformat: []
117
    };
118
    /**
119
     * The data of the 'fact' chunk.
120
     * @type {!Object<string, *>}
121
     * @export
122
     */
123
    this.fact = {
124
      /** @export @type {string} */
125
      chunkId: '',
126
      /** @export @type {number} */
127
      chunkSize: 0,
128
      /** @export @type {number} */
129
      dwSampleLength: 0
130
    };
131
    /**
132
     * The data of the 'cue ' chunk.
133
     * @type {!Object<string, *>}
134
     * @export
135
     */
136
    this.cue = {
137
      /** @export @type {string} */
138
      chunkId: '',
139
      /** @export @type {number} */
140
      chunkSize: 0,
141
      /** @export @type {number} */
142
      dwCuePoints: 0,
143
      /** @export @type {!Array<!Object>} */
144
      points: [],
145
    };
146
    /**
147
     * The data of the 'smpl' chunk.
148
     * @type {!Object<string, *>}
149
     * @export
150
     */
151
    this.smpl = {
152
      /** @export @type {string} */
153
      chunkId: '',
154
      /** @export @type {number} */
155
      chunkSize: 0,
156
      /** @export @type {number} */
157
      dwManufacturer: 0,
158
      /** @export @type {number} */
159
      dwProduct: 0,
160
      /** @export @type {number} */
161
      dwSamplePeriod: 0,
162
      /** @export @type {number} */
163
      dwMIDIUnityNote: 0,
164
      /** @export @type {number} */
165
      dwMIDIPitchFraction: 0,
166
      /** @export @type {number} */
167
      dwSMPTEFormat: 0,
168
      /** @export @type {number} */
169
      dwSMPTEOffset: 0,
170
      /** @export @type {number} */
171
      dwNumSampleLoops: 0,
172
      /** @export @type {number} */
173
      dwSamplerData: 0,
174
      /** @export @type {!Array<!Object>} */
175
      loops: [],
176
    };
177
    /**
178
     * The data of the 'bext' chunk.
179
     * @type {!Object<string, *>}
180
     * @export
181
     */
182
    this.bext = {
183
      /** @export @type {string} */
184
      chunkId: '',
185
      /** @export @type {number} */
186
      chunkSize: 0,
187
      /** @export @type {string} */
188
      description: '', //256
189
      /** @export @type {string} */
190
      originator: '', //32
191
      /** @export @type {string} */
192
      originatorReference: '', //32
193
      /** @export @type {string} */
194
      originationDate: '', //10
195
      /** @export @type {string} */
196
      originationTime: '', //8
197
      /**
198
       * 2 32-bit values, timeReference high and low
199
       * @export @type {!Array<number>}
200
       */
201
      timeReference: [0, 0],
202
      /** @export @type {number} */
203
      version: 0, //WORD
204
      /** @export @type {string} */
205
      UMID: '', // 64 chars
206
      /** @export @type {number} */
207
      loudnessValue: 0, //WORD
208
      /** @export @type {number} */
209
      loudnessRange: 0, //WORD
210
      /** @export @type {number} */
211
      maxTruePeakLevel: 0, //WORD
212
      /** @export @type {number} */
213
      maxMomentaryLoudness: 0, //WORD
214
      /** @export @type {number} */
215
      maxShortTermLoudness: 0, //WORD
216
      /** @export @type {string} */
217
      reserved: '', //180
218
      /** @export @type {string} */
219
      codingHistory: '' // string, unlimited
220
    };
221
    /**
222
     * The data of the 'ds64' chunk.
223
     * Used only with RF64 files.
224
     * @type {!Object<string, *>}
225
     * @export
226
     */
227
    this.ds64 = {
228
      /** @type {string} */
229
      chunkId: '',
230
      /** @export @type {number} */
231
      chunkSize: 0,
232
      /** @export @type {number} */
233
      riffSizeHigh: 0, // DWORD
234
      /** @export @type {number} */
235
      riffSizeLow: 0, // DWORD
236
      /** @export @type {number} */
237
      dataSizeHigh: 0, // DWORD
238
      /** @export @type {number} */
239
      dataSizeLow: 0, // DWORD
240
      /** @export @type {number} */
241
      originationTime: 0, // DWORD
242
      /** @export @type {number} */
243
      sampleCountHigh: 0, // DWORD
244
      /** @export @type {number} */
245
      sampleCountLow: 0, // DWORD
246
      /** @export @type {number} */
247
      //'tableLength': 0, // DWORD
248
      /** @export @type {!Array<number>} */
249
      //'table': []
250
    };
251
    /**
252
     * The data of the 'data' chunk.
253
     * @type {!Object<string, *>}
254
     * @export
255
     */
256
    this.data = {
257
      /** @export @type {string} */
258
      chunkId: '',
259
      /** @export @type {number} */
260
      chunkSize: 0,
261
      /** @export @type {!Uint8Array} */
262
      samples: new Uint8Array(0)
263
    };
264
    /**
265
     * The data of the 'LIST' chunks.
266
     * Each item in this list must have this signature:
267
     *  {
268
     *    'chunkId': '',
269
     *    'chunkSize': 0,
270
     *    'format': '',
271
     *    'subChunks': []
272
     *   }
273
     * @type {!Array<!Object>}
274
     * @export
275
     */
276
    this.LIST = [];
277
    /**
278
     * The data of the 'junk' chunk.
279
     * @type {!Object<string, *>}
280
     * @export
281
     */
282
    this.junk = {
283
      /** @export @type {string} */
284
      chunkId: '',
285
      /** @export @type {number} */
286
      chunkSize: 0,
287
      /** @export @type {!Array<number>} */
288
      chunkData: []
289
    };
290
    /**
291
     * The bit depth code according to the samples.
292
     * @type {string}
293
     * @export
294
     */
295
    this.bitDepth = '0';
296
    /**
297
     * Audio formats.
298
     * Formats not listed here will be set to 65534
299
     * and treated as WAVE_FORMAT_EXTENSIBLE
300
     * @enum {number}
301
     * @private
302
     */
303
    this.audioFormats_ = {
304
      '4': 17,
305
      '8': 1,
306
      '8a': 6,
307
      '8m': 7,
308
      '16': 1,
309
      '24': 1,
310
      '32': 1,
311
      '32f': 3,
312
      '64': 3
313
    };
314
    /**
315
     * @type {number}
316
     * @private
317
     */
318
    this.head_ = 0;
319
    // Load a file from the buffer if one was passed
320
    // when creating the object
321
    this.dataType = {};
322
    if(bytes) {
323
      this.fromBuffer(bytes);
324
    }
325
  }
326
327
  /**
328
   * Set up the WaveFile object based on the arguments passed.
329
   * @param {number} numChannels The number of channels
330
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
331
   * @param {number} sampleRate The sample rate.
332
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
333
   * @param {string} bitDepth The audio bit depth code.
334
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
335
   *    or any value between '8' and '32' (like '12').
336
   * @param {!Array<number>|!Array<!Array<number>>} samples Array of samples to be written.
337
   *    The samples must be in the correct range according to the
338
   *    bit depth.
339
   * @param {?Object} options Optional. Used to force the container
340
   *    as RIFX with {'container': 'RIFX'}
341
   * @throws {Error} If any argument does not meet the criteria.
342
   * @export
343
   */
344
  fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) {
345
    if (!options['container']) {
346
      options['container'] = 'RIFF';
347
    }
348
    this.container = options['container'];
349
    this.bitDepth = bitDepth;
350
    samples = this.interleave_(samples);
351
    /** @type {number} */
352
    let numBytes = (((parseInt(bitDepth, 10) - 1) | 7) + 1) / 8;
353
    // Turn the samples to bytes
354
    this.updateDataType_();
355
    this.data.samples = new Uint8Array(samples.length * numBytes);
356
    packArrayTo(samples, this.dataType, this.data.samples);
357
    // create headers
358
    this.createPCMHeader_(
359
      bitDepth, numChannels, sampleRate, numBytes, options);
360
    if (bitDepth == '4') {
361
      this.createADPCMHeader_(
362
        bitDepth, numChannels, sampleRate, numBytes, options);
363
    } else if (bitDepth == '8a' || bitDepth == '8m') {
364
      this.createALawMulawHeader_(
365
        bitDepth, numChannels, sampleRate, numBytes, options);
366
    } else if(Object.keys(this.audioFormats_).indexOf(bitDepth) == -1 ||
367
        this.fmt.numChannels > 2) {
368
      this.createExtensibleHeader_(
369
        bitDepth, numChannels, sampleRate, numBytes, options);
370
    }
371
    // the data chunk
372
    this.data.chunkId = 'data';
373
    this.data.chunkSize = this.data.samples.length;
374
    this.validateHeader_();
375
    this.LEorBE_();
376
  }
377
378
  /**
379
   * Update the type definition used to read and write the samples.
380
   */
381
  updateDataType_() {
382
    /** @type {!Object} */
383
    this.dataType = {
384
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
385
      float: this.bitDepth == '32f' || this.bitDepth == '64',
386
      signed: this.bitDepth != '8',
387
      be: this.container == 'RIFX'
388
    };
389
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
390
      this.dataType.bits = 8;
391
      this.dataType.signed = false;
392
    }
393
  }
394
395
  /**
396
   * Set up the WaveFile object from a byte buffer.
397
   * @param {!Array<number>|!Array<!Array<number>>} samples The samples.
398
   */
399
  interleave_(samples) {
400
    if (samples.length > 0) {
401
      if (samples[0].constructor === Array) {
402
        /** @type {!Array<number>} */
403
        let finalSamples = [];
404
        for (let i=0; i < samples[0].length; i++) {
405
          for (let j=0; j < samples.length; j++) {
406
            finalSamples.push(samples[j][i]);
407
          }
408
        }
409
        samples = finalSamples;
410
      }
411
    }
412
    return samples;
413
  }
414
415
  /**
416
   * Set up the WaveFile object from a byte buffer.
417
   * @param {!Uint8Array} bytes The buffer.
418
   * @param {boolean=} samples True if the samples should be loaded.
419
   * @throws {Error} If container is not RIFF, RIFX or RF64.
420
   * @throws {Error} If no 'fmt ' chunk is found.
421
   * @throws {Error} If no 'data' chunk is found.
422
   * @export
423
   */
424
  fromBuffer(bytes, samples=true) {
425
    this.head_ = 0;
426
    this.clearHeader_();
427
    this.readRIFFChunk_(bytes);
428
    /** @type {!Object} */
429
    let chunk = riffChunks(bytes);
430
    this.readDs64Chunk_(bytes, chunk.subChunks);
431
    this.readFmtChunk_(bytes, chunk.subChunks);
432
    this.readFactChunk_(bytes, chunk.subChunks);
433
    this.readBextChunk_(bytes, chunk.subChunks);
434
    this.readCueChunk_(bytes, chunk.subChunks);
435
    this.readSmplChunk_(bytes, chunk.subChunks);
436
    this.readDataChunk_(bytes, chunk.subChunks, samples);
437
    this.readJunkChunk_(bytes, chunk.subChunks);
438
    this.readLISTChunk_(bytes, chunk.subChunks);
439
    this.bitDepthFromFmt_();
440
    this.updateDataType_();
441
  }
442
443
  /**
444
   * Return a byte buffer representig the WaveFile object as a .wav file.
445
   * The return value of this method can be written straight to disk.
446
   * @return {!Uint8Array} A .wav file.
447
   * @throws {Error} If any property of the object appears invalid.
448
   * @export
449
   */
450
  toBuffer() {
451
    this.validateHeader_();
452
    return this.createWaveFile_();
453
  }
454
455
  /**
456
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
457
   * @param {string} base64String A .wav file as a base64 string.
458
   * @throws {Error} If any property of the object appears invalid.
459
   * @export
460
   */
461
  fromBase64(base64String) {
462
    this.fromBuffer(new Uint8Array(decode(base64String)));
463
  }
464
465
  /**
466
   * Return a base64 string representig the WaveFile object as a .wav file.
467
   * @return {string} A .wav file as a base64 string.
468
   * @throws {Error} If any property of the object appears invalid.
469
   * @export
470
   */
471
  toBase64() {
472
    /** @type {Uint8Array} */
473
    let buffer = this.toBuffer();
474
    return encode(buffer, 0, buffer.length);
475
  }
476
477
  /**
478
   * Return a DataURI 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
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
490
   * @param {string} dataURI A .wav file as DataURI.
491
   * @throws {Error} If any property of the object appears invalid.
492
   * @export
493
   */
494
  fromDataURI(dataURI) {
495
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
496
  }
497
498
  /**
499
   * Force a file as RIFF.
500
   * @export
501
   */
502
  toRIFF() {
503
    if (this.container == 'RF64') {
504
      this.fromScratch(
505
        this.fmt.numChannels,
506
        this.fmt.sampleRate,
507
        this.bitDepth,
508
        unpackArray(this.data.samples, this.dataType));
509
    } else {
510
      this.dataType.be = true;
511
      this.fromScratch(
512
        this.fmt.numChannels,
513
        this.fmt.sampleRate,
514
        this.bitDepth,
515
        unpackArray(this.data.samples, this.dataType));
516
    }
517
  }
518
519
  /**
520
   * Force a file as RIFX.
521
   * @export
522
   */
523
  toRIFX() {
524
    if (this.container == 'RF64') {
525
      this.fromScratch(
526
        this.fmt.numChannels,
527
        this.fmt.sampleRate,
528
        this.bitDepth,
529
        unpackArray(this.data.samples, this.dataType),
530
        {'container': 'RIFX'});
531
    } else {
532
      this.fromScratch(
533
        this.fmt.numChannels,
534
        this.fmt.sampleRate,
535
        this.bitDepth,
536
        unpackArray(this.data.samples, this.dataType),
537
        {'container': 'RIFX'});
538
    }
539
  }
540
541
  /**
542
   * Change the bit depth of the samples.
543
   * @param {string} bitDepth The new bit depth of the samples.
544
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
545
   * @param {boolean} changeResolution A boolean indicating if the
546
   *    resolution of samples should be actually changed or not.
547
   * @throws {Error} If the bit depth is not valid.
548
   * @export
549
   */
550
  toBitDepth(bitDepth, changeResolution=true) {
551
    /** @type {string} */
552
    let toBitDepth = bitDepth;
553
    /** @type {string} */
554
    let thisBitDepth = this.bitDepth;
555
    if (!changeResolution) {
556
      toBitDepth = this.realBitDepth_(bitDepth);
557
      thisBitDepth = this.realBitDepth_(this.bitDepth);
558
    }
559
    this.assureUncompressed_();
560
    /** @type {!Array<number>} */
561
    let samples = unpackArray(this.data.samples, this.dataType);
562
    this.truncateSamples(samples);
563
    bitdepth(samples, thisBitDepth, toBitDepth);
564
    this.fromScratch(
565
      this.fmt.numChannels,
566
      this.fmt.sampleRate,
567
      bitDepth,
568
      samples,
569
      {'container': this.correctContainer_()});
570
  }
571
572
  /**
573
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
574
   * @throws {Error} If sample rate is not 8000.
575
   * @throws {Error} If number of channels is not 1.
576
   * @export
577
   */
578
  toIMAADPCM() {
579
    if (this.fmt.sampleRate != 8000) {
580
      throw new Error(
581
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
582
    } 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...
583
      throw new Error(
584
        'Only mono files can be compressed as IMA-ADPCM.');
585
    } else {
586
      this.assure16Bit_();
587
      this.fromScratch(
588
        this.fmt.numChannels,
589
        this.fmt.sampleRate,
590
        '4',
591
        encodeADPCM(unpackArray(this.data.samples, this.dataType)),
592
        {'container': this.correctContainer_()});
593
    }
594
  }
595
596
  /**
597
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
598
   * @param {string} bitDepth The new bit depth of the samples.
599
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
600
   *    Optional. Default is 16.
601
   * @export
602
   */
603
  fromIMAADPCM(bitDepth='16') {
604
    this.fromScratch(
605
      this.fmt.numChannels,
606
      this.fmt.sampleRate,
607
      '16',
608
      decodeADPCM(this.data.samples, this.fmt.blockAlign),
609
      {'container': this.correctContainer_()});
610
    if (bitDepth != '16') {
611
      this.toBitDepth(bitDepth);
612
    }
613
  }
614
615
  /**
616
   * Encode 16-bit wave file as 8-bit A-Law.
617
   * @export
618
   */
619
  toALaw() {
620
    this.assure16Bit_();
621
    this.fromScratch(
622
      this.fmt.numChannels,
623
      this.fmt.sampleRate,
624
      '8a',
625
      alawmulaw.alaw.encode(unpackArray(this.data.samples, this.dataType)),
626
      {'container': this.correctContainer_()});
627
  }
628
629
  /**
630
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
631
   * @param {string} bitDepth The new bit depth of the samples.
632
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
633
   *    Optional. Default is 16.
634
   * @export
635
   */
636
  fromALaw(bitDepth='16') {
637
    this.fromScratch(
638
      this.fmt.numChannels,
639
      this.fmt.sampleRate,
640
      '16',
641
      alawmulaw.alaw.decode(this.data.samples),
642
      {'container': this.correctContainer_()});
643
    if (bitDepth != '16') {
644
      this.toBitDepth(bitDepth);
645
    }
646
  }
647
648
  /**
649
   * Encode 16-bit wave file as 8-bit mu-Law.
650
   * @export
651
   */
652
  toMuLaw() {
653
    this.assure16Bit_();
654
    /** @type {Array<number>} */
655
    let samples = unpackArray(this.data.samples, this.dataType);
656
    this.fromScratch(
657
      this.fmt.numChannels,
658
      this.fmt.sampleRate,
659
      '8m',
660
      alawmulaw.mulaw.encode(samples),
661
      {'container': this.correctContainer_()});
662
  }
663
664
  /**
665
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
666
   * @param {string} bitDepth The new bit depth of the samples.
667
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
668
   *    Optional. Default is 16.
669
   * @export
670
   */
671
  fromMuLaw(bitDepth='16') {
672
    this.fromScratch(
673
      this.fmt.numChannels,
674
      this.fmt.sampleRate,
675
      '16',
676
      alawmulaw.mulaw.decode(this.data.samples),
677
      {'container': this.correctContainer_()});
678
    if (bitDepth != '16') {
679
      this.toBitDepth(bitDepth);
680
    }
681
  }
682
683
  /**
684
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
685
   * then it is created. It if exists, it is overwritten.
686
   * @param {string} tag The tag name.
687
   * @param {string} value The tag value.
688
   * @throws {Error} If the tag name is not valid.
689
   * @export
690
   */
691
  setTag(tag, value) {
692
    tag = this.fixTagName_(tag);
693
    /** @type {!Object} */
694
    let index = this.getTagIndex_(tag);
695
    if (index.TAG !== null) {
696
      this.LIST[index.LIST]['subChunks'][index.TAG]['chunkSize'] =
697
        value.length + 1;
698
      this.LIST[index.LIST]['subChunks'][index.TAG]['value'] = value;
699
    } else if (index.LIST !== null) {
700
      this.LIST[index.LIST]['subChunks'].push({
701
        'chunkId': tag,
702
        'chunkSize': value.length + 1,
703
        'value': value});
704
    } else {
705
      this.LIST.push({
706
        'chunkId': 'LIST',
707
        'chunkSize': 8 + value.length + 1,
708
        'format': 'INFO',
709
        'subChunks': []});
710
      this.LIST[this.LIST.length - 1]['subChunks'].push({
711
        'chunkId': tag,
712
        'chunkSize': value.length + 1,
713
        'value': value});
714
    }
715
  }
716
717
  /**
718
   * Return the value of a RIFF tag in the INFO chunk.
719
   * @param {string} tag The tag name.
720
   * @return {?string} The value if the tag is found, null otherwise.
721
   * @export
722
   */
723
  getTag(tag) {
724
    /** @type {!Object} */
725
    let index = this.getTagIndex_(tag);
726
    if (index.TAG !== null) {
727
      return this.LIST[index.LIST]['subChunks'][index.TAG]['value'];
728
    }
729
    return null;
730
  }
731
732
  /**
733
   * Remove a RIFF tag in the INFO chunk.
734
   * @param {string} tag The tag name.
735
   * @return {boolean} True if a tag was deleted.
736
   * @export
737
   */
738
  deleteTag(tag) {
739
    /** @type {!Object} */
740
    let index = this.getTagIndex_(tag);
741
    if (index.TAG !== null) {
742
      this.LIST[index.LIST]['subChunks'].splice(index.TAG, 1);
743
      return true;
744
    }
745
    return false;
746
  }
747
748
  /**
749
   * Create a cue point in the wave file.
750
   * @param {number} position The cue point position in milliseconds.
751
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
752
   * @export
753
   */
754
  setCuePoint(position, labl='') {
755
    this.cue.chunkId = 'cue ';
756
    position = (position * this.fmt.sampleRate) / 1000;
757
    /** @type {!Array<!Object>} */
758
    let existingPoints = this.getCuePoints_();
759
    this.clearLISTadtl_();
760
    /** @type {number} */
761
    let len = this.cue.points.length;
762
    this.cue.points = [];
763
    /** @type {boolean} */
764
    let hasSet = false;
765
    if (len == 0) {
0 ignored issues
show
Comparing len to 0 using the == operator is not safe. Consider using === instead.
Loading history...
766
      this.setCuePoint_(position, 1, labl);
767
    } else {
768
      for (let i=0; i<len; i++) {
769
        if (existingPoints[i]['dwPosition'] > position && !hasSet) {
770
          this.setCuePoint_(position, i + 1, labl);
771
          this.setCuePoint_(
772
            existingPoints[i]['dwPosition'],
773
            i + 2,
774
            existingPoints[i]['label']);
775
          hasSet = true;
776
        } else {
777
          this.setCuePoint_(
778
            existingPoints[i]['dwPosition'],
779
            i + 1,
780
            existingPoints[i]['label']);
781
        }
782
      }
783
      if (!hasSet) {
784
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
785
      }
786
    }
787
    this.cue.dwCuePoints = this.cue.points.length;
788
  }
789
790
  /**
791
   * Remove a cue point from a wave file.
792
   * @param {number} index the index of the point. First is 1,
793
   *    second is 2, and so on.
794
   * @export
795
   */
796
  deleteCuePoint(index) {
797
    this.cue.chunkId = 'cue ';
798
    /** @type {!Array<!Object>} */
799
    let existingPoints = this.getCuePoints_();
800
    this.clearLISTadtl_();
801
    /** @type {number} */
802
    let len = this.cue.points.length;
803
    this.cue.points = [];
804
    for (let i=0; i<len; i++) {
805
      if (i + 1 != index) {
806
        this.setCuePoint_(
807
          existingPoints[i]['dwPosition'],
808
          i + 1,
809
          existingPoints[i]['label']);
810
      }
811
    }
812
    this.cue.dwCuePoints = this.cue.points.length;
813
    if (this.cue.dwCuePoints) {
814
      this.cue.chunkId = 'cue ';
815
    } else {
816
      this.cue.chunkId = '';
817
      this.clearLISTadtl_();
818
    }
819
  }
820
821
  /**
822
   * Update the label of a cue point.
823
   * @param {number} pointIndex The ID of the cue point.
824
   * @param {string} label The new text for the label.
825
   * @export
826
   */
827
  updateLabel(pointIndex, label) {
828
    /** @type {?number} */
829
    let adtlIndex = this.getAdtlChunk_();
830
    if (adtlIndex !== null) {
831
      for (let i=0; i<this.LIST[adtlIndex]['subChunks'].length; i++) {
832
        if (this.LIST[adtlIndex]['subChunks'][i]['dwName'] ==
833
            pointIndex) {
834
          this.LIST[adtlIndex]['subChunks'][i]['value'] = label;
835
        }
836
      }
837
    }
838
  }
839
840
  /**
841
   * Push a new cue point in this.cue.points.
842
   * @param {number} position The position in milliseconds.
843
   * @param {number} dwName the dwName of the cue point
844
   * @private
845
   */
846
  setCuePoint_(position, dwName, label) {
847
    this.cue.points.push({
848
      'dwName': dwName,
849
      'dwPosition': position,
850
      'fccChunk': 'data',
851
      'dwChunkStart': 0,
852
      'dwBlockStart': 0,
853
      'dwSampleOffset': position,
854
    });
855
    this.setLabl_(dwName, label);
856
  }
857
858
  /**
859
   * Return an array with the position of all cue points in the file.
860
   * @return {!Array<!Object>}
861
   * @private
862
   */
863
  getCuePoints_() {
864
    /** @type {!Array<!Object>} */
865
    let points = [];
866
    for (let i=0; i<this.cue.points.length; i++) {
867
      points.push({
868
        'dwPosition': this.cue.points[i]['dwPosition'],
869
        'label': this.getLabelForCuePoint_(
870
          this.cue.points[i]['dwName'])});
871
    }
872
    return points;
873
  }
874
875
  /**
876
   * Return the label of a cue point.
877
   * @param {number} pointDwName The ID of the cue point.
878
   * @return {string}
879
   * @private
880
   */
881
  getLabelForCuePoint_(pointDwName) {
882
    /** @type {?number} */
883
    let adtlIndex = this.getAdtlChunk_();
884
    if (adtlIndex !== null) {
885
      for (let i=0; i<this.LIST[adtlIndex]['subChunks'].length; i++) {
886
        if (this.LIST[adtlIndex]['subChunks'][i]['dwName'] ==
887
            pointDwName) {
888
          return this.LIST[adtlIndex]['subChunks'][i]['value'];
889
        }
890
      }
891
    }
892
    return '';
893
  }
894
895
  /**
896
   * Clear any LIST chunk labeled as 'adtl'.
897
   * @private
898
   */
899
  clearLISTadtl_() {
900
    for (let i=0; i<this.LIST.length; i++) {
901
      if (this.LIST[i]['format'] == 'adtl') {
902
        this.LIST.splice(i);
903
      }
904
    }
905
  }
906
907
  /**
908
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
909
   * @param {number} dwName The ID of the cue point.
910
   * @param {string} label The label for the cue point.
911
   * @private
912
   */
913
  setLabl_(dwName, label) {
914
    /** @type {?number} */
915
    let adtlIndex = this.getAdtlChunk_();
916
    if (adtlIndex === null) {
917
      this.LIST.push({
918
        'chunkId': 'LIST',
919
        'chunkSize': 4,
920
        'format': 'adtl',
921
        'subChunks': []});
922
      adtlIndex = this.LIST.length - 1;
923
    }
924
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
925
  }
926
927
  /**
928
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
929
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
930
   * @param {number} dwName The ID of the cue point.
931
   * @param {string} label The label for the cue point.
932
   * @private
933
   */
934
  setLabelText_(adtlIndex, dwName, label) {
935
    this.LIST[adtlIndex]['subChunks'].push({
936
      'chunkId': 'labl',
937
      'chunkSize': label.length,
938
      'dwName': dwName,
939
      'value': label
940
    });
941
    this.LIST[adtlIndex]['chunkSize'] += label.length + 4 + 4 + 4 + 1;
942
  }
943
944
  /**
945
   * Return the index of the 'adtl' LIST in this.LIST.
946
   * @return {?number}
947
   * @private
948
   */
949
  getAdtlChunk_() {
950
    for (let i=0; i<this.LIST.length; i++) {
951
      if(this.LIST[i]['format'] == 'adtl') {
952
        return i;
953
      }
954
    }
955
    return null;
956
  }
957
958
  /**
959
   * Return the index of a tag in a FILE chunk.
960
   * @param {string} tag The tag name.
961
   * @return {!Object<string, ?number>}
962
   *    Object.LIST is the INFO index in LIST
963
   *    Object.TAG is the tag index in the INFO
964
   * @private
965
   */
966
  getTagIndex_(tag) {
967
    /** @type {!Object<string, ?number>} */
968
    let index = {LIST: null, TAG: null};
969
    for (let i=0; i<this.LIST.length; i++) {
970
      if (this.LIST[i]['format'] == 'INFO') {
971
        index.LIST = i;
972
        for (let j=0; j<this.LIST[i]['subChunks'].length; j++) {
973
          if (this.LIST[i]['subChunks'][j]['chunkId'] == tag) {
974
            index.TAG = j;
975
            break;
976
          }
977
        }
978
        break;
979
      }
980
    }
981
    return index;
982
  }
983
984
  /**
985
   * Fix a RIFF tag format if possible, throw an error otherwise.
986
   * @param {string} tag The tag name.
987
   * @return {string} The tag name in proper fourCC format.
988
   * @private
989
   */
990
  fixTagName_(tag) {
991
    if (tag.constructor !== String) {
992
      throw new Error('Invalid tag name.');
993
    } else if(tag.length < 4) {
994
      for (let i=0; i<4-tag.length; i++) {
995
        tag += ' ';
996
      }
997
    }
998
    return tag;
999
  }
1000
1001
  /**
1002
   * Create the header of a ADPCM wave file.
1003
   * @param {string} bitDepth The audio bit depth
1004
   * @param {number} numChannels The number of channels
1005
   * @param {number} sampleRate The sample rate.
1006
   * @param {number} numBytes The number of bytes each sample use.
1007
   * @param {!Object} options The extra options, like container defintion.
1008
   * @private
1009
   */
1010
  createADPCMHeader_(bitDepth, numChannels, sampleRate, numBytes, options) {
1011
    this.createPCMHeader_(
1012
      bitDepth, numChannels, sampleRate, numBytes, options);
1013
    this.chunkSize = 40 + this.data.samples.length;
1014
    this.fmt.chunkSize = 20;
1015
    this.fmt.byteRate = 4055;
1016
    this.fmt.blockAlign = 256;
1017
    this.fmt.bitsPerSample = 4;
1018
    this.fmt.cbSize = 2;
1019
    this.fmt.validBitsPerSample = 505;
1020
    this.fact.chunkId = 'fact';
1021
    this.fact.chunkSize = 4;
1022
    this.fact.dwSampleLength = this.data.samples.length * 2;
1023
    this.data.chunkSize = this.data.samples.length;
1024
  }
1025
1026
  /**
1027
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
1028
   * @param {string} bitDepth The audio bit depth
1029
   * @param {number} numChannels The number of channels
1030
   * @param {number} sampleRate The sample rate.
1031
   * @param {number} numBytes The number of bytes each sample use.
1032
   * @param {!Object} options The extra options, like container defintion.
1033
   * @private
1034
   */
1035
  createExtensibleHeader_(
1036
      bitDepth, numChannels, sampleRate, numBytes, options) {
1037
    this.createPCMHeader_(
1038
      bitDepth, numChannels, sampleRate, numBytes, options);
1039
    //this.chunkSize = 36 + 24 + this.data.samples.length * numBytes;
1040
    this.chunkSize = 36 + 24 + this.data.samples.length;
1041
    this.fmt.chunkSize = 40;
1042
    this.fmt.bitsPerSample = ((parseInt(bitDepth, 10) - 1) | 7) + 1;
1043
    this.fmt.cbSize = 22;
1044
    this.fmt.validBitsPerSample = parseInt(bitDepth, 10);
1045
    this.fmt.dwChannelMask = this.getDwChannelMask_();
1046
    // subformat 128-bit GUID as 4 32-bit values
1047
    // only supports uncompressed integer PCM samples
1048
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
1049
  }
1050
1051
  /**
1052
   * Get the value for dwChannelMask according to the number of channels.
1053
   * @return {number} the dwChannelMask value.
1054
   * @private
1055
   */
1056
  getDwChannelMask_() {
1057
    /** @type {number} */
1058
    let dwChannelMask = 0;
1059
    // mono = FC
1060
    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...
1061
      dwChannelMask = 0x4;
1062
    // stereo = FL, FR
1063
    } else if (this.fmt.numChannels == 2) {
1064
      dwChannelMask = 0x3;
1065
    // quad = FL, FR, BL, BR
1066
    } else if (this.fmt.numChannels == 4) {
1067
      dwChannelMask = 0x33;
1068
    // 5.1 = FL, FR, FC, LF, BL, BR
1069
    } else if (this.fmt.numChannels == 6) {
1070
      dwChannelMask = 0x3F;
1071
    // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR
1072
    } else if (this.fmt.numChannels == 8) {
1073
      dwChannelMask = 0x63F;
1074
    }
1075
    return dwChannelMask;
1076
  }
1077
1078
  /**
1079
   * Create the header of mu-Law and A-Law wave files.
1080
   * @param {string} bitDepth The audio bit depth
1081
   * @param {number} numChannels The number of channels
1082
   * @param {number} sampleRate The sample rate.
1083
   * @param {number} numBytes The number of bytes each sample use.
1084
   * @param {!Object} options The extra options, like container defintion.
1085
   * @private
1086
   */
1087
  createALawMulawHeader_(
1088
      bitDepth, numChannels, sampleRate, numBytes, options) {
1089
    this.createPCMHeader_(
1090
      bitDepth, numChannels, sampleRate, numBytes, options);
1091
    this.chunkSize = 40 + this.data.samples.length;
1092
    this.fmt.chunkSize = 20;
1093
    this.fmt.cbSize = 2;
1094
    this.fmt.validBitsPerSample = 8;
1095
    this.fact.chunkId = 'fact';
1096
    this.fact.chunkSize = 4;
1097
    this.fact.dwSampleLength = this.data.samples.length;
1098
  }
1099
1100
  /**
1101
   * Create the header of a linear PCM wave file.
1102
   * @param {string} bitDepth The audio bit depth
1103
   * @param {number} numChannels The number of channels
1104
   * @param {number} sampleRate The sample rate.
1105
   * @param {number} numBytes The number of bytes each sample use.
1106
   * @param {!Object} options The extra options, like container defintion.
1107
   * @private
1108
   */
1109
  createPCMHeader_(bitDepth, numChannels, sampleRate, numBytes, options) {
1110
    this.clearHeader_();
1111
    this.container = options['container'];
1112
    //this.chunkSize = 36 + this.data.samples.length * numBytes;
1113
    this.chunkSize = 36 + this.data.samples.length;
1114
    this.format = 'WAVE';
1115
    this.fmt.chunkId = 'fmt ';
1116
    this.fmt.chunkSize = 16;
1117
    this.fmt.byteRate = (numChannels * numBytes) * sampleRate;
1118
    this.fmt.blockAlign = numChannels * numBytes;
1119
    this.fmt.audioFormat = this.audioFormats_[bitDepth] ?
1120
      this.audioFormats_[bitDepth] : 65534;
1121
    this.fmt.numChannels = numChannels;
1122
    this.fmt.sampleRate = sampleRate;
1123
    this.fmt.bitsPerSample = parseInt(bitDepth, 10);
1124
    this.fmt.cbSize = 0;
1125
    this.fmt.validBitsPerSample = 0;
1126
  }
1127
1128
  /**
1129
   * Return the closest greater number of bits for a number of bits that
1130
   * do not fill a full sequence of bytes.
1131
   * @param {string} bitDepth The bit depth.
1132
   * @return {string}
1133
   * @private
1134
   */
1135
  realBitDepth_(bitDepth) {
1136
    if (bitDepth != '32f') {
1137
      bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1).toString();
1138
    }
1139
    return bitDepth;
1140
  }
1141
1142
  /**
1143
   * Validate the header of the file.
1144
   * @throws {Error} If any property of the object appears invalid.
1145
   * @private
1146
   */
1147
  validateHeader_() {
1148
    this.validateBitDepth_();
1149
    this.validateNumChannels_();
1150
    this.validateSampleRate_();
1151
  }
1152
1153
  /**
1154
   * Validate the bit depth.
1155
   * @return {boolean} True is the bit depth is valid.
1156
   * @throws {Error} If bit depth is invalid.
1157
   * @private
1158
   */
1159
  validateBitDepth_() {
1160
    if (!this.audioFormats_[this.bitDepth]) {
1161
      if (parseInt(this.bitDepth, 10) > 8 &&
1162
          parseInt(this.bitDepth, 10) < 54) {
1163
        return true;
1164
      }
1165
      throw new Error('Invalid bit depth.');
1166
    }
1167
    return true;
1168
  }
1169
1170
  /**
1171
   * Validate the number of channels.
1172
   * @return {boolean} True is the number of channels is valid.
1173
   * @throws {Error} If the number of channels is invalid.
1174
   * @private
1175
   */
1176
  validateNumChannels_() {
1177
    /** @type {number} */
1178
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1179
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1180
      throw new Error('Invalid number of channels.');
1181
    }
1182
    return true;
1183
  }
1184
1185
  /**
1186
   * Validate the sample rate value.
1187
   * @return {boolean} True is the sample rate is valid.
1188
   * @throws {Error} If the sample rate is invalid.
1189
   * @private
1190
   */
1191
  validateSampleRate_() {
1192
    /** @type {number} */
1193
    let byteRate = this.fmt.numChannels *
1194
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1195
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1196
      throw new Error('Invalid sample rate.');
1197
    }
1198
    return true;
1199
  }
1200
1201
  /**
1202
   * Reset attributes that should emptied when a file is
1203
   * created with the fromScratch() or fromBuffer() methods.
1204
   * @private
1205
   */
1206
  clearHeader_() {
1207
    this.fmt.cbSize = 0;
1208
    this.fmt.validBitsPerSample = 0;
1209
    this.fact.chunkId = '';
1210
    this.ds64.chunkId = '';
1211
  }
1212
1213
  /**
1214
   * Make the file 16-bit if it is not.
1215
   * @private
1216
   */
1217
  assure16Bit_() {
1218
    this.assureUncompressed_();
1219
    if (this.bitDepth != '16') {
1220
      this.toBitDepth('16');
1221
    }
1222
  }
1223
1224
  /**
1225
   * Uncompress the samples in case of a compressed file.
1226
   * @private
1227
   */
1228
  assureUncompressed_() {
1229
    if (this.bitDepth == '8a') {
1230
      this.fromALaw();
1231
    } else if(this.bitDepth == '8m') {
1232
      this.fromMuLaw();
1233
    } else if (this.bitDepth == '4') {
1234
      this.fromIMAADPCM();
1235
    }
1236
  }
1237
1238
  /**
1239
   * Set up to work wih big-endian or little-endian files.
1240
   * The types used are changed to LE or BE. If the
1241
   * the file is big-endian (RIFX), true is returned.
1242
   * @return {boolean} True if the file is RIFX.
1243
   * @private
1244
   */
1245
  LEorBE_() {
1246
    /** @type {boolean} */
1247
    let bigEndian = this.container === 'RIFX';
1248
    uInt16_.be = bigEndian;
1249
    uInt32_.be = bigEndian;
1250
    return bigEndian;
1251
  }
1252
1253
  /**
1254
   * Find a chunk by its fourCC_ in a array of RIFF chunks.
1255
   * @param {!Object} chunks The wav file chunks.
1256
   * @param {string} chunkId The chunk fourCC_.
1257
   * @param {boolean} multiple True if there may be multiple chunks
1258
   *    with the same chunkId.
1259
   * @return {?Array<!Object>}
1260
   * @private
1261
   */
1262
  findChunk_(chunks, chunkId, multiple=false) {
1263
    /** @type {!Array<!Object>} */
1264
    let chunk = [];
1265
    for (let i=0; i<chunks.length; i++) {
1266
      if (chunks[i].chunkId == chunkId) {
1267
        if (multiple) {
1268
          chunk.push(chunks[i]);
1269
        } else {
1270
          return chunks[i];
1271
        }
1272
      }
1273
    }
1274
    if (chunkId == 'LIST') {
1275
      return chunk.length ? chunk : null;
1276
    }
1277
    return null;
1278
  }
1279
1280
  /**
1281
   * Read the RIFF chunk a wave file.
1282
   * @param {!Uint8Array} bytes A wav buffer.
1283
   * @throws {Error} If no 'RIFF' chunk is found.
1284
   * @private
1285
   */
1286
  readRIFFChunk_(bytes) {
1287
    this.head_ = 0;
1288
    this.container = this.readString_(bytes, 4);
1289
    if (['RIFF', 'RIFX', 'RF64'].indexOf(this.container) === -1) {
1290
      throw Error('Not a supported format.');
1291
    }
1292
    this.LEorBE_();
1293
    this.chunkSize = this.read_(bytes, uInt32_);
1294
    this.format = this.readString_(bytes, 4);
1295
    if (this.format != 'WAVE') {
1296
      throw Error('Could not find the "WAVE" format identifier');
1297
    }
1298
  }
1299
1300
  /**
1301
   * Read the 'fmt ' chunk of a wave file.
1302
   * @param {!Uint8Array} buffer The wav file buffer.
1303
   * @param {!Object} signature The file signature.
1304
   * @throws {Error} If no 'fmt ' chunk is found.
1305
   * @private
1306
   */
1307
  readFmtChunk_(buffer, signature) {
1308
    /** @type {?Object} */
1309
    let chunk = this.findChunk_(signature, 'fmt ');
1310
    if (chunk) {
1311
      this.head_ = chunk.chunkData.start;
1312
      this.fmt.chunkId = chunk.chunkId;
1313
      this.fmt.chunkSize = chunk.chunkSize;
1314
      this.fmt.audioFormat = this.read_(buffer, uInt16_);
1315
      this.fmt.numChannels = this.read_(buffer, uInt16_);
1316
      this.fmt.sampleRate = this.read_(buffer, uInt32_);
1317
      this.fmt.byteRate = this.read_(buffer, uInt32_);
1318
      this.fmt.blockAlign = this.read_(buffer, uInt16_);
1319
      this.fmt.bitsPerSample = this.read_(buffer, uInt16_);
1320
      this.readFmtExtension_(buffer);
1321
    } else {
1322
      throw Error('Could not find the "fmt " chunk');
1323
    }
1324
  }
1325
1326
  /**
1327
   * Read the 'fmt ' chunk extension.
1328
   * @param {!Uint8Array} buffer The wav file buffer.
1329
   * @private
1330
   */
1331
  readFmtExtension_(buffer) {
1332
    if (this.fmt.chunkSize > 16) {
1333
      this.fmt.cbSize = this.read_(buffer, uInt16_);
1334
      if (this.fmt.chunkSize > 18) {
1335
        this.fmt.validBitsPerSample = this.read_(buffer, uInt16_);
1336
        if (this.fmt.chunkSize > 20) {
1337
          this.fmt.dwChannelMask = this.read_(buffer, uInt32_);
1338
          this.fmt.subformat = [
1339
            this.read_(buffer, uInt32_),
1340
            this.read_(buffer, uInt32_),
1341
            this.read_(buffer, uInt32_),
1342
            this.read_(buffer, uInt32_)];
1343
        }
1344
      }
1345
    }
1346
  }
1347
1348
  /**
1349
   * Read the 'fact' chunk of a wav file.
1350
   * @param {!Uint8Array} buffer The wav file buffer.
1351
   * @param {!Object} signature The file signature.
1352
   * @private
1353
   */
1354
  readFactChunk_(buffer, signature) {
1355
    /** @type {?Object} */
1356
    let chunk = this.findChunk_(signature, 'fact');
1357
    if (chunk) {
1358
      this.head_ = chunk.chunkData.start;
1359
      this.fact.chunkId = chunk.chunkId;
1360
      this.fact.chunkSize = chunk.chunkSize;
1361
      this.fact.dwSampleLength = this.read_(buffer, uInt32_);
1362
    }
1363
  }
1364
1365
  /**
1366
   * Read the 'cue ' chunk of a wave file.
1367
   * @param {!Uint8Array} buffer The wav file buffer.
1368
   * @param {!Object} signature The file signature.
1369
   * @private
1370
   */
1371
  readCueChunk_(buffer, signature) {
1372
    /** @type {?Object} */
1373
    let chunk = this.findChunk_(signature, 'cue ');
1374
    if (chunk) {
1375
      this.head_ = chunk.chunkData.start;
1376
      this.cue.chunkId = chunk.chunkId;
1377
      this.cue.chunkSize = chunk.chunkSize;
1378
      this.cue.dwCuePoints = this.read_(buffer, uInt32_);
1379
      for (let i=0; i<this.cue.dwCuePoints; i++) {
1380
        this.cue.points.push({
1381
          'dwName': this.read_(buffer, uInt32_),
1382
          'dwPosition': this.read_(buffer, uInt32_),
1383
          'fccChunk': this.readString_(buffer, 4),
1384
          'dwChunkStart': this.read_(buffer, uInt32_),
1385
          'dwBlockStart': this.read_(buffer, uInt32_),
1386
          'dwSampleOffset': this.read_(buffer, uInt32_),
1387
        });
1388
      }
1389
    }
1390
  }
1391
1392
  /**
1393
   * Read the 'smpl' chunk of a wave file.
1394
   * @param {!Uint8Array} buffer The wav file buffer.
1395
   * @param {!Object} signature The file signature.
1396
   * @private
1397
   */
1398
  readSmplChunk_(buffer, signature) {
1399
    /** @type {?Object} */
1400
    let chunk = this.findChunk_(signature, 'smpl');
1401
    if (chunk) {
1402
      this.head_ = chunk.chunkData.start;
1403
      this.smpl.chunkId = chunk.chunkId;
1404
      this.smpl.chunkSize = chunk.chunkSize;
1405
      this.smpl.dwManufacturer = this.read_(buffer, uInt32_);
1406
      this.smpl.dwProduct = this.read_(buffer, uInt32_);
1407
      this.smpl.dwSamplePeriod = this.read_(buffer, uInt32_);
1408
      this.smpl.dwMIDIUnityNote = this.read_(buffer, uInt32_);
1409
      this.smpl.dwMIDIPitchFraction = this.read_(buffer, uInt32_);
1410
      this.smpl.dwSMPTEFormat = this.read_(buffer, uInt32_);
1411
      this.smpl.dwSMPTEOffset = this.read_(buffer, uInt32_);
1412
      this.smpl.dwNumSampleLoops = this.read_(buffer, uInt32_);
1413
      this.smpl.dwSamplerData = this.read_(buffer, uInt32_);
1414
      for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
1415
        this.smpl.loops.push({
1416
          'dwName': this.read_(buffer, uInt32_),
1417
          'dwType': this.read_(buffer, uInt32_),
1418
          'dwStart': this.read_(buffer, uInt32_),
1419
          'dwEnd': this.read_(buffer, uInt32_),
1420
          'dwFraction': this.read_(buffer, uInt32_),
1421
          'dwPlayCount': this.read_(buffer, uInt32_),
1422
        });
1423
      }
1424
    }
1425
  }
1426
1427
  /**
1428
   * Read the 'data' chunk of a wave file.
1429
   * @param {!Uint8Array} buffer The wav file buffer.
1430
   * @param {!Object} signature The file signature.
1431
   * @param {boolean} samples True if the samples should be loaded.
1432
   * @throws {Error} If no 'data' chunk is found.
1433
   * @private
1434
   */
1435
  readDataChunk_(buffer, signature, samples) {
1436
    /** @type {?Object} */
1437
    let chunk = this.findChunk_(signature, 'data');
1438
    if (chunk) {
1439
      this.data.chunkId = 'data';
1440
      this.data.chunkSize = chunk.chunkSize;
1441
      if (samples) {
1442
        this.data.samples = buffer.slice(
1443
          chunk.chunkData.start,
1444
          chunk.chunkData.end);
1445
      }
1446
    } else {
1447
      throw Error('Could not find the "data" chunk');
1448
    }
1449
  }
1450
1451
  /**
1452
   * Read the 'bext' chunk of a wav file.
1453
   * @param {!Uint8Array} buffer The wav file buffer.
1454
   * @param {!Object} signature The file signature.
1455
   * @private
1456
   */
1457
  readBextChunk_(buffer, signature) {
1458
    /** @type {?Object} */
1459
    let chunk = this.findChunk_(signature, 'bext');
1460
    if (chunk) {
1461
      this.head_ = chunk.chunkData.start;
1462
      this.bext.chunkId = chunk.chunkId;
1463
      this.bext.chunkSize = chunk.chunkSize;
1464
      this.bext.description = this.readString_(buffer, 256);
1465
      this.bext.originator = this.readString_(buffer, 32);
1466
      this.bext.originatorReference = this.readString_(buffer, 32);
1467
      this.bext.originationDate = this.readString_(buffer, 10);
1468
      this.bext.originationTime = this.readString_(buffer, 8);
1469
      this.bext.timeReference = [
1470
        this.read_(buffer, uInt32_),
1471
        this.read_(buffer, uInt32_)];
1472
      this.bext.version = this.read_(buffer, uInt16_);
1473
      this.bext.UMID = this.readString_(buffer, 64);
1474
      this.bext.loudnessValue = this.read_(buffer, uInt16_);
1475
      this.bext.loudnessRange = this.read_(buffer, uInt16_);
1476
      this.bext.maxTruePeakLevel = this.read_(buffer, uInt16_);
1477
      this.bext.maxMomentaryLoudness = this.read_(buffer, uInt16_);
1478
      this.bext.maxShortTermLoudness = this.read_(buffer, uInt16_);
1479
      this.bext.reserved = this.readString_(buffer, 180);
1480
      this.bext.codingHistory = this.readString_(
1481
        buffer, this.bext.chunkSize - 602);
1482
    }
1483
  }
1484
1485
  /**
1486
   * Read the 'ds64' chunk of a wave file.
1487
   * @param {!Uint8Array} buffer The wav file buffer.
1488
   * @param {!Object} signature The file signature.
1489
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
1490
   * @private
1491
   */
1492
  readDs64Chunk_(buffer, signature) {
1493
    /** @type {?Object} */
1494
    let chunk = this.findChunk_(signature, 'ds64');
1495
    if (chunk) {
1496
      this.head_ = chunk.chunkData.start;
1497
      this.ds64.chunkId = chunk.chunkId;
1498
      this.ds64.chunkSize = chunk.chunkSize;
1499
      this.ds64.riffSizeHigh = this.read_(buffer, uInt32_);
1500
      this.ds64.riffSizeLow = this.read_(buffer, uInt32_);
1501
      this.ds64.dataSizeHigh = this.read_(buffer, uInt32_);
1502
      this.ds64.dataSizeLow = this.read_(buffer, uInt32_);
1503
      this.ds64.originationTime = this.read_(buffer, uInt32_);
1504
      this.ds64.sampleCountHigh = this.read_(buffer, uInt32_);
1505
      this.ds64.sampleCountLow = this.read_(buffer, uInt32_);
1506
      //if (this.ds64.chunkSize > 28) {
1507
      //  this.ds64.tableLength = unpack(
1508
      //    chunkData.slice(28, 32), uInt32_);
1509
      //  this.ds64.table = chunkData.slice(
1510
      //     32, 32 + this.ds64.tableLength); 
1511
      //}
1512
    } else {
1513
      if (this.container == 'RF64') {
1514
        throw Error('Could not find the "ds64" chunk');  
1515
      }
1516
    }
1517
  }
1518
1519
  /**
1520
   * Read the 'LIST' chunks of a wave file.
1521
   * @param {!Uint8Array} buffer The wav file buffer.
1522
   * @param {!Object} signature The file signature.
1523
   * @private
1524
   */
1525
  readLISTChunk_(buffer, signature) {
1526
    /** @type {?Object} */
1527
    let listChunks = this.findChunk_(signature, 'LIST', true);
1528
    if (listChunks === null) {
1529
      return;
1530
    }
1531
    for (let j=0; j < listChunks.length; j++) {
1532
      /** @type {!Object} */
1533
      let subChunk = listChunks[j];
1534
      this.LIST.push({
1535
        'chunkId': subChunk.chunkId,
1536
        'chunkSize': subChunk.chunkSize,
1537
        'format': subChunk.format,
1538
        'subChunks': []});
1539
      for (let x=0; x<subChunk.subChunks.length; x++) {
1540
        this.readLISTSubChunks_(subChunk.subChunks[x],
1541
          subChunk.format, buffer);
1542
      }
1543
    }
1544
  }
1545
1546
  /**
1547
   * Read the sub chunks of a 'LIST' chunk.
1548
   * @param {!Object} subChunk The 'LIST' subchunks.
1549
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
1550
   * @param {!Uint8Array} buffer The wav file buffer.
1551
   * @private
1552
   */
1553
  readLISTSubChunks_(subChunk, format, buffer) {
1554
    if (format == 'adtl') {
1555
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
1556
        this.head_ = subChunk.chunkData.start;
1557
        /** @type {!Object<string, string|number>} */
1558
        let item = {
1559
          'chunkId': subChunk.chunkId,
1560
          'chunkSize': subChunk.chunkSize,
1561
          'dwName': this.read_(buffer, uInt32_)
1562
        };
1563
        if (subChunk.chunkId == 'ltxt') {
1564
          item['dwSampleLength'] = this.read_(buffer, uInt32_);
1565
          item['dwPurposeID'] = this.read_(buffer, uInt32_);
1566
          item['dwCountry'] = this.read_(buffer, uInt16_);
1567
          item['dwLanguage'] = this.read_(buffer, uInt16_);
1568
          item['dwDialect'] = this.read_(buffer, uInt16_);
1569
          item['dwCodePage'] = this.read_(buffer, uInt16_);
1570
        }
1571
        item['value'] = this.readZSTR_(buffer, this.head_);
1572
        this.LIST[this.LIST.length - 1]['subChunks'].push(item);
1573
      }
1574
    // RIFF INFO tags like ICRD, ISFT, ICMT
1575
    } else if(format == 'INFO') {
1576
      this.head_ = subChunk.chunkData.start;
1577
      this.LIST[this.LIST.length - 1]['subChunks'].push({
1578
        'chunkId': subChunk.chunkId,
1579
        'chunkSize': subChunk.chunkSize,
1580
        'value': this.readZSTR_(buffer,  this.head_)
1581
      });
1582
    }
1583
  }
1584
1585
  /**
1586
   * Read the 'junk' chunk of a wave file.
1587
   * @param {!Uint8Array} buffer The wav file buffer.
1588
   * @param {!Object} signature The file signature.
1589
   * @private
1590
   */
1591
  readJunkChunk_(buffer, signature) {
1592
    /** @type {?Object} */
1593
    let chunk = this.findChunk_(signature, 'junk');
1594
    if (chunk) {
1595
      this.junk = {
1596
        'chunkId': chunk.chunkId,
1597
        'chunkSize': chunk.chunkSize,
1598
        'chunkData': [].slice.call(buffer.slice(
1599
          chunk.chunkData.start,
1600
          chunk.chunkData.end))
1601
      };
1602
    }
1603
  }
1604
1605
  /**
1606
   * Read bytes as a ZSTR string.
1607
   * @param {!Uint8Array} bytes The bytes.
1608
   * @return {string} The string.
1609
   * @private
1610
   */
1611
  readZSTR_(bytes, index=0) {
1612
    /** @type {string} */
1613
    let str = '';
1614
    for (let i=index; i<bytes.length; i++) {
1615
      this.head_++;
1616
      if (bytes[i] === 0) {
1617
        break;
1618
      }
1619
      str += unpackString(bytes, i, 1);
1620
    }
1621
    return str;
1622
  }
1623
1624
  /**
1625
   * Read bytes as a string from a RIFF chunk.
1626
   * @param {!Uint8Array} bytes The bytes.
1627
   * @param {number} maxSize the max size of the string.
1628
   * @return {string} The string.
1629
   * @private
1630
   */
1631
  readString_(bytes, maxSize) {
1632
    /** @type {string} */
1633
    let str = '';
1634
    for (let i=0; i<maxSize; i++) {
1635
      str += unpackString(bytes, this.head_, 1);
1636
      this.head_++;
1637
    }
1638
    return str;
1639
  }
1640
1641
  /**
1642
   * Read a number from a chunk.
1643
   * @param {!Uint8Array} bytes The chunk bytes.
1644
   * @param {!Object} bdType The type definition.
1645
   * @return {number} The number.
1646
   * @private
1647
   */
1648
  read_(bytes, bdType) {
1649
    /** @type {number} */
1650
    let size = bdType.bits / 8;
1651
    /** @type {number} */
1652
    let value = unpackFrom(bytes, bdType, this.head_);
1653
    this.head_ += size;
1654
    return value;
1655
  }
1656
1657
  /**
1658
   * Write a variable size string as bytes. If the string is smaller
1659
   * than the max size the output array is filled with 0s.
1660
   * @param {string} str The string to be written as bytes.
1661
   * @param {number} maxSize the max size of the string.
1662
   * @return {!Array<number>} The bytes.
1663
   * @private
1664
   */
1665
  writeString_(str, maxSize, push=true) {
1666
    /** @type {!Array<number>} */   
1667
    let bytes = packString(str);
1668
    if (push) {
1669
      for (let i=bytes.length; i<maxSize; i++) {
1670
        bytes.push(0);
1671
      }  
1672
    }
1673
    return bytes;
1674
  }
1675
1676
  /**
1677
   * Truncate float samples on over and underflow.
1678
   * @private
1679
   */
1680
  truncateSamples(samples) {
1681
    if (this.fmt.audioFormat == 3) {
1682
      /** @type {number} */   
1683
      let len = samples.length;
1684
      for (let i=0; i<len; i++) {
1685
        if (samples[i] > 1) {
1686
          samples[i] = 1;
1687
        } else if (samples[i] < -1) {
1688
          samples[i] = -1;
1689
        }
1690
      }
1691
    }
1692
  }
1693
1694
  /**
1695
   * Return the bytes of the 'bext' chunk.
1696
   * @return {!Array<number>} The 'bext' chunk bytes.
1697
   * @private
1698
   */
1699
  getBextBytes_() {
1700
    /** @type {!Array<number>} */
1701
    let bytes = [];
1702
    this.enforceBext_();
1703
    if (this.bext.chunkId) {
1704
      bytes = bytes.concat(
1705
        packString(this.bext.chunkId),
1706
        pack(602 + this.bext.codingHistory.length, uInt32_),
1707
        this.writeString_(this.bext.description, 256),
1708
        this.writeString_(this.bext.originator, 32),
1709
        this.writeString_(this.bext.originatorReference, 32),
1710
        this.writeString_(this.bext.originationDate, 10),
1711
        this.writeString_(this.bext.originationTime, 8),
1712
        pack(this.bext.timeReference[0], uInt32_),
1713
        pack(this.bext.timeReference[1], uInt32_),
1714
        pack(this.bext.version, uInt16_),
1715
        this.writeString_(this.bext.UMID, 64),
1716
        pack(this.bext.loudnessValue, uInt16_),
1717
        pack(this.bext.loudnessRange, uInt16_),
1718
        pack(this.bext.maxTruePeakLevel, uInt16_),
1719
        pack(this.bext.maxMomentaryLoudness, uInt16_),
1720
        pack(this.bext.maxShortTermLoudness, uInt16_),
1721
        this.writeString_(this.bext.reserved, 180),
1722
        this.writeString_(
1723
          this.bext.codingHistory, this.bext.codingHistory.length));
1724
    }
1725
    return bytes;
1726
  }
1727
1728
  /**
1729
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
1730
   * @private
1731
   */
1732
  enforceBext_() {
1733
    for (var prop in this.bext) {
1734
      if (this.bext.hasOwnProperty(prop)) {
1735
        if (this.bext[prop] && prop != 'timeReference') {
1736
          this.bext.chunkId = 'bext';
1737
          break;
1738
        }
1739
      }
1740
    }
1741
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
1742
      this.bext.chunkId = 'bext';
1743
    }
1744
  }
1745
1746
  /**
1747
   * Return the bytes of the 'ds64' chunk.
1748
   * @return {!Array<number>} The 'ds64' chunk bytes.
1749
   * @private
1750
   */
1751
  getDs64Bytes_() {
1752
    /** @type {!Array<number>} */
1753
    let bytes = [];
1754
    if (this.ds64.chunkId) {
1755
      bytes = bytes.concat(
1756
        packString(this.ds64.chunkId),
1757
        pack(this.ds64.chunkSize, uInt32_),
1758
        pack(this.ds64.riffSizeHigh, uInt32_),
1759
        pack(this.ds64.riffSizeLow, uInt32_),
1760
        pack(this.ds64.dataSizeHigh, uInt32_),
1761
        pack(this.ds64.dataSizeLow, uInt32_),
1762
        pack(this.ds64.originationTime, uInt32_),
1763
        pack(this.ds64.sampleCountHigh, uInt32_),
1764
        pack(this.ds64.sampleCountLow, uInt32_));
1765
    }
1766
    //if (this.ds64.tableLength) {
1767
    //  ds64Bytes = ds64Bytes.concat(
1768
    //    pack(this.ds64.tableLength, uInt32_),
1769
    //    this.ds64.table);
1770
    //}
1771
    return bytes;
1772
  }
1773
1774
  /**
1775
   * Return the bytes of the 'cue ' chunk.
1776
   * @return {!Array<number>} The 'cue ' chunk bytes.
1777
   * @private
1778
   */
1779
  getCueBytes_() {
1780
    /** @type {!Array<number>} */
1781
    let bytes = [];
1782
    if (this.cue.chunkId) {
1783
      /** @type {!Array<number>} */
1784
      let cuePointsBytes = this.getCuePointsBytes_();
1785
      bytes = bytes.concat(
1786
        packString(this.cue.chunkId),
1787
        pack(cuePointsBytes.length + 4, uInt32_),
1788
        pack(this.cue.dwCuePoints, uInt32_),
1789
        cuePointsBytes);
1790
    }
1791
    return bytes;
1792
  }
1793
1794
  /**
1795
   * Return the bytes of the 'cue ' points.
1796
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
1797
   * @private
1798
   */
1799
  getCuePointsBytes_() {
1800
    /** @type {!Array<number>} */
1801
    let points = [];
1802
    for (let i=0; i<this.cue.dwCuePoints; i++) {
1803
      points = points.concat(
1804
        pack(this.cue.points[i]['dwName'], uInt32_),
1805
        pack(this.cue.points[i]['dwPosition'], uInt32_),
1806
        packString(this.cue.points[i]['fccChunk']),
1807
        pack(this.cue.points[i]['dwChunkStart'], uInt32_),
1808
        pack(this.cue.points[i]['dwBlockStart'], uInt32_),
1809
        pack(this.cue.points[i]['dwSampleOffset'], uInt32_));
1810
    }
1811
    return points;
1812
  }
1813
1814
  /**
1815
   * Return the bytes of the 'smpl' chunk.
1816
   * @return {!Array<number>} The 'smpl' chunk bytes.
1817
   * @private
1818
   */
1819
  getSmplBytes_() {
1820
    /** @type {!Array<number>} */
1821
    let bytes = [];
1822
    if (this.smpl.chunkId) {
1823
      /** @type {!Array<number>} */
1824
      let smplLoopsBytes = this.getSmplLoopsBytes_();
1825
      bytes = bytes.concat(
1826
        packString(this.smpl.chunkId),
1827
        pack(smplLoopsBytes.length + 36, uInt32_),
1828
        pack(this.smpl.dwManufacturer, uInt32_),
1829
        pack(this.smpl.dwProduct, uInt32_),
1830
        pack(this.smpl.dwSamplePeriod, uInt32_),
1831
        pack(this.smpl.dwMIDIUnityNote, uInt32_),
1832
        pack(this.smpl.dwMIDIPitchFraction, uInt32_),
1833
        pack(this.smpl.dwSMPTEFormat, uInt32_),
1834
        pack(this.smpl.dwSMPTEOffset, uInt32_),
1835
        pack(this.smpl.dwNumSampleLoops, uInt32_),
1836
        pack(this.smpl.dwSamplerData, uInt32_),
1837
        smplLoopsBytes);
1838
    }
1839
    return bytes;
1840
  }
1841
1842
  /**
1843
   * Return the bytes of the 'smpl' loops.
1844
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
1845
   * @private
1846
   */
1847
  getSmplLoopsBytes_() {
1848
    /** @type {!Array<number>} */
1849
    let loops = [];
1850
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
1851
      loops = loops.concat(
1852
        pack(this.smpl.loops[i]['dwName'], uInt32_),
1853
        pack(this.smpl.loops[i]['dwType'], uInt32_),
1854
        pack(this.smpl.loops[i]['dwStart'], uInt32_),
1855
        pack(this.smpl.loops[i]['dwEnd'], uInt32_),
1856
        pack(this.smpl.loops[i]['dwFraction'], uInt32_),
1857
        pack(this.smpl.loops[i]['dwPlayCount'], uInt32_));
1858
    }
1859
    return loops;
1860
  }
1861
1862
  /**
1863
   * Return the bytes of the 'fact' chunk.
1864
   * @return {!Array<number>} The 'fact' chunk bytes.
1865
   * @private
1866
   */
1867
  getFactBytes_() {
1868
    // @type {!Array<number>} 
1869
    let bytes = [];
1870
    if (this.fact.chunkId) {
1871
      bytes = bytes.concat(
1872
        packString(this.fact.chunkId),
1873
        pack(this.fact.chunkSize, uInt32_),
1874
        pack(this.fact.dwSampleLength, uInt32_));
1875
    }
1876
    return bytes;
1877
  }
1878
1879
  /**
1880
   * Return the bytes of the 'fmt ' chunk.
1881
   * @return {!Array<number>} The 'fmt' chunk bytes.
1882
   * @throws {Error} if no 'fmt ' chunk is present.
1883
   * @private
1884
   */
1885
  getFmtBytes_() {
1886
    if (this.fmt.chunkId) {
1887
      return [].concat(
1888
        packString(this.fmt.chunkId),
1889
        pack(this.fmt.chunkSize, uInt32_),
1890
        pack(this.fmt.audioFormat, uInt16_),
1891
        pack(this.fmt.numChannels, uInt16_),
1892
        pack(this.fmt.sampleRate, uInt32_),
1893
        pack(this.fmt.byteRate, uInt32_),
1894
        pack(this.fmt.blockAlign, uInt16_),
1895
        pack(this.fmt.bitsPerSample, uInt16_),
1896
        this.getFmtExtensionBytes_());
1897
    }
1898
    throw Error('Could not find the "fmt " chunk');
1899
  }
1900
1901
  /**
1902
   * Return the bytes of the fmt extension fields.
1903
   * @return {!Array<number>} The fmt extension bytes.
1904
   * @private
1905
   */
1906
  getFmtExtensionBytes_() {
1907
    /** @type {!Array<number>} */
1908
    let extension = [];
1909
    if (this.fmt.chunkSize > 16) {
1910
      extension = extension.concat(
1911
        pack(this.fmt.cbSize, uInt16_));
1912
    }
1913
    if (this.fmt.chunkSize > 18) {
1914
      extension = extension.concat(
1915
        pack(this.fmt.validBitsPerSample, uInt16_));
1916
    }
1917
    if (this.fmt.chunkSize > 20) {
1918
      extension = extension.concat(
1919
        pack(this.fmt.dwChannelMask, uInt32_));
1920
    }
1921
    if (this.fmt.chunkSize > 24) {
1922
      extension = extension.concat(
1923
        pack(this.fmt.subformat[0], uInt32_),
1924
        pack(this.fmt.subformat[1], uInt32_),
1925
        pack(this.fmt.subformat[2], uInt32_),
1926
        pack(this.fmt.subformat[3], uInt32_));
1927
    }
1928
    return extension;
1929
  }
1930
1931
  /**
1932
   * Return the bytes of the 'LIST' chunk.
1933
   * @return {!Array<number>} The 'LIST' chunk bytes.
1934
   * @export for tests
1935
   */
1936
  getLISTBytes_() {
1937
    /** @type {!Array<number>} */
1938
    let bytes = [];
1939
    for (let i=0; i<this.LIST.length; i++) {
1940
      /** @type {!Array<number>} */
1941
      let subChunksBytes = this.getLISTSubChunksBytes_(
1942
          this.LIST[i]['subChunks'], this.LIST[i]['format']);
1943
      bytes = bytes.concat(
1944
        packString(this.LIST[i]['chunkId']),
1945
        pack(subChunksBytes.length + 4, uInt32_),
1946
        packString(this.LIST[i]['format']),
1947
        subChunksBytes);
1948
    }
1949
    return bytes;
1950
  }
1951
1952
  /**
1953
   * Return the bytes of the sub chunks of a 'LIST' chunk.
1954
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
1955
   * @param {string} format The format of the 'LIST' chunk.
1956
   *    Currently supported values are 'adtl' or 'INFO'.
1957
   * @return {!Array<number>} The sub chunk bytes.
1958
   * @private
1959
   */
1960
  getLISTSubChunksBytes_(subChunks, format) {
1961
    /** @type {!Array<number>} */
1962
    let bytes = [];
1963
    for (let i=0; i<subChunks.length; i++) {
1964
      if (format == 'INFO') {
1965
        bytes = bytes.concat(
1966
          packString(subChunks[i]['chunkId']),
1967
          pack(subChunks[i]['value'].length + 1, uInt32_),
1968
          this.writeString_(
1969
            subChunks[i]['value'], subChunks[i]['value'].length));
1970
        bytes.push(0);
1971
      } else if (format == 'adtl') {
1972
        if (['labl', 'note'].indexOf(subChunks[i]['chunkId']) > -1) {
1973
          bytes = bytes.concat(
1974
            packString(subChunks[i]['chunkId']),
1975
            pack(
1976
              subChunks[i]['value'].length + 4 + 1, uInt32_),
1977
            pack(subChunks[i]['dwName'], uInt32_),
1978
            this.writeString_(
1979
              subChunks[i]['value'],
1980
              subChunks[i]['value'].length));
1981
          bytes.push(0);
1982
        } else if (subChunks[i]['chunkId'] == 'ltxt') {
1983
          bytes = bytes.concat(
1984
            this.getLtxtChunkBytes_(subChunks[i]));
1985
        }
1986
      }
1987
      if (bytes.length % 2) {
1988
        bytes.push(0);
1989
      }
1990
    }
1991
    return bytes;
1992
  }
1993
1994
  /**
1995
   * Return the bytes of a 'ltxt' chunk.
1996
   * @param {!Object} ltxt the 'ltxt' chunk.
1997
   * @return {!Array<number>} The 'ltxt' chunk bytes.
1998
   * @private
1999
   */
2000
  getLtxtChunkBytes_(ltxt) {
2001
    return [].concat(
2002
      packString(ltxt['chunkId']),
2003
      pack(ltxt['value'].length + 20, uInt32_),
2004
      pack(ltxt['dwName'], uInt32_),
2005
      pack(ltxt['dwSampleLength'], uInt32_),
2006
      pack(ltxt['dwPurposeID'], uInt32_),
2007
      pack(ltxt['dwCountry'], uInt16_),
2008
      pack(ltxt['dwLanguage'], uInt16_),
2009
      pack(ltxt['dwLanguage'], uInt16_),
2010
      pack(ltxt['dwCodePage'], uInt16_),
2011
      this.writeString_(ltxt['value'], ltxt['value'].length));
2012
  }
2013
2014
  /**
2015
   * Return the bytes of the 'junk' chunk.
2016
   * @return {!Array<number>} The 'junk' chunk bytes.
2017
   * @private
2018
   */
2019
  getJunkBytes_() {
2020
    /** @type {!Array<number>} */
2021
    let bytes = [];
2022
    if (this.junk.chunkId) {
2023
      return bytes.concat(
2024
        packString(this.junk.chunkId),
2025
        pack(this.junk.chunkData.length, uInt32_),
2026
        this.junk.chunkData);
2027
    }
2028
    return bytes;
2029
  }
2030
2031
  /**
2032
   * Return 'RIFF' if the container is 'RF64', the current container name
2033
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
2034
   * @return {string}
2035
   * @private
2036
   */
2037
  correctContainer_() {
2038
    return this.container == 'RF64' ? 'RIFF' : this.container;
2039
  }
2040
2041
  /**
2042
   * Set the string code of the bit depth based on the 'fmt ' chunk.
2043
   * @private
2044
   */
2045
  bitDepthFromFmt_() {
2046
    if (this.fmt.audioFormat == 3 && this.fmt.bitsPerSample == 32) {
2047
      this.bitDepth = '32f';
2048
    } else if (this.fmt.audioFormat == 6) {
2049
      this.bitDepth = '8a';
2050
    } else if (this.fmt.audioFormat == 7) {
2051
      this.bitDepth = '8m';
2052
    } else {
2053
      this.bitDepth = this.fmt.bitsPerSample.toString();
2054
    }
2055
  }
2056
2057
  /**
2058
   * Return a .wav file byte buffer with the data from the WaveFile object.
2059
   * The return value of this method can be written straight to disk.
2060
   * @return {!Uint8Array} The wav file bytes.
2061
   * @private
2062
   */
2063
  createWaveFile_() {
2064
    // @type {!Array<Array<number>|Uint8Array>}
2065
    let fileBody = [
2066
      this.getJunkBytes_(),
2067
      this.getDs64Bytes_(),
2068
      this.getBextBytes_(),
2069
      this.getFmtBytes_(),
2070
      this.getFactBytes_(),
2071
      packString(this.data.chunkId),
2072
      pack(this.data.samples.length, uInt32_),
2073
      this.data.samples,
2074
      this.getCueBytes_(),
2075
      this.getSmplBytes_(),
2076
      this.getLISTBytes_()
2077
    ];
2078
    // @type {number}
2079
    let fileBodyLength = 0;
2080
    for (let i=0; i<fileBody.length; i++) {
2081
      fileBodyLength += fileBody[i].length;
2082
    }
2083
    // @type {!Uint8Array}
2084
    let file = new Uint8Array(fileBodyLength + 12);
2085
    // @type {number}
2086
    let index = 0;
2087
    index = packStringTo(this.container, file, index);
2088
    index = packTo(fileBodyLength + 4, uInt32_, file, index);
2089
    index = packStringTo(this.format, file, index);
2090
    for (let i=0; i<fileBody.length; i++) {
2091
      file.set(fileBody[i], index);
2092
      index += fileBody[i].length;
2093
    }
2094
    return file;
2095
  }
2096
}
2097