Passed
Branch v8.x (84f809)
by Rafael S.
02:21
created

index.js (6 issues)

1
/*
2
 * wavefile: Read and write wave files.
3
 * https://github.com/rochars/wavefile
4
 *
5
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining
8
 * a copy of this software and associated documentation files (the
9
 * "Software"), to deal in the Software without restriction, including
10
 * without limitation the rights to use, copy, modify, merge, publish,
11
 * distribute, sublicense, and/or sell copies of the Software, and to
12
 * permit persons to whom the Software is furnished to do so, subject to
13
 * the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be
16
 * included in all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
 *
26
 */
27
28
/**
29
 * @fileoverview The WaveFile class.
30
 */
31
32
/** @module wavefile */
33
34
import bitdepth from 'bitdepth';
35
import riffChunks from 'riff-chunks';
36
import {decode as decodeADPCM, encode as encodeADPCM} from 'imaadpcm';
37
import alawmulaw from 'alawmulaw';
38
import {encode, decode} from 'base64-arraybuffer-es6';
0 ignored issues
show
The variable decode already seems to be declared on line 36. Are you sure you want to import using decode as name?
Loading history...
The variable encode already seems to be declared on line 36. Are you sure you want to import using encode as name?
Loading history...
39
import {pack, 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) {
0 ignored issues
show
Comparing this.fmt.numChannels to 1 using the != operator is not safe. Consider using !== instead.
Loading history...
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) {
0 ignored issues
show
Comparing len to 0 using the == operator is not safe. Consider using === instead.
Loading history...
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) {
0 ignored issues
show
Comparing this.fmt.numChannels to 1 using the == operator is not safe. Consider using === instead.
Loading history...
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