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

index.js (1 issue)

Severity
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';
39
import {pack, packArray, unpackFrom, unpackString, packStringTo,
0 ignored issues
show
The variable packArray seems to be never used. Consider removing it.
Loading history...
40
  packTo, 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) {
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) {
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) {
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