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