Passed
Push — master ( b7dccd...8b0f51 )
by Rafael S.
02:19
created

WaveFile.writeWavBuffer_   A

Complexity

Conditions 3

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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