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