Passed
Branch v8.x (741daf)
by Rafael S.
02:15
created

index.js ➔ validateSampleRate_   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 9
rs 10
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
import {wavHeader, AUDIO_FORMATS} from './lib/wavheader.js';
38
// @type {WavIO}
39
import WavIO from './lib/wavio.js';
40
41
/**
42
 * Class representing a wav file.
43
 * @extends WavIO
44
 */
45
export default class WaveFile extends WavIO {
46
47
  /**
48
   * @param {?Uint8Array} bytes A wave file buffer.
49
   * @throws {Error} If no 'RIFF' chunk is found.
50
   * @throws {Error} If no 'fmt ' chunk is found.
51
   * @throws {Error} If no 'data' chunk is found.
52
   */
53
  constructor(bytes=null) {
54
    super();
55
    // Load a file from the buffer if one was passed
56
    // when creating the object
57
    if(bytes) {
58
      this.fromBuffer(bytes);
59
    }
60
  }
61
62
  /**
63
   * Set up the WaveFile object based on the arguments passed.
64
   * @param {number} numChannels The number of channels
65
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
66
   * @param {number} sampleRate The sample rate.
67
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
68
   * @param {string} bitDepthCode The audio bit depth code.
69
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
70
   *    or any value between '8' and '32' (like '12').
71
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
72
   *    The samples. Must be in the correct range according to the bit depth.
73
   * @param {?Object} options Optional. Used to force the container
74
   *    as RIFX with {'container': 'RIFX'}
75
   * @throws {Error} If any argument does not meet the criteria.
76
   */
77
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
78
    if (!options.container) {
79
      options.container = 'RIFF';
80
    }
81
    this.container = options.container;
82
    this.bitDepth = bitDepthCode;
83
    samples = this.interleave_(samples);
84
    /** @type {number} */
85
    let numBytes = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1) / 8;
86
    this.updateDataType_();
87
    this.data.samples = new Uint8Array(samples.length * numBytes);
88
    packArrayTo(samples, this.dataType, this.data.samples);
89
    /** @type {!Object} */
90
    let header = wavHeader(
91
      bitDepthCode, numChannels, sampleRate,
92
      numBytes, this.data.samples.length, options);
93
    this.clearHeader_();
94
    this.chunkSize = header.chunkSize;
95
    this.format = header.format;
96
    this.fmt = header.fmt;
97
    if (header.fact) {
98
      this.fact = header.fact;
99
    }
100
    this.data.chunkId = 'data';
101
    this.data.chunkSize = this.data.samples.length;
102
    this.validateHeader_();
103
    this.LEorBE_();
104
  }
105
106
  /**
107
   * Set up the WaveFile object from a byte buffer.
108
   * @param {!Uint8Array} bytes The buffer.
109
   * @param {boolean=} samples True if the samples should be loaded.
110
   * @throws {Error} If container is not RIFF, RIFX or RF64.
111
   * @throws {Error} If no 'fmt ' chunk is found.
112
   * @throws {Error} If no 'data' chunk is found.
113
   */
114
  fromBuffer(bytes, samples=true) {
115
    this.readWavBuffer(bytes, samples);
116
  }
117
118
  /**
119
   * Return a byte buffer representig the WaveFile object as a .wav file.
120
   * The return value of this method can be written straight to disk.
121
   * @return {!Uint8Array} A .wav file.
122
   * @throws {Error} If any property of the object appears invalid.
123
   */
124
  toBuffer() {
125
    this.validateHeader_();
126
    return this.createWaveFile_();
127
  }
128
129
  /**
130
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
131
   * @param {string} base64String A .wav file as a base64 string.
132
   * @throws {Error} If any property of the object appears invalid.
133
   */
134
  fromBase64(base64String) {
135
    this.fromBuffer(new Uint8Array(decode(base64String)));
136
  }
137
138
  /**
139
   * Return a base64 string representig the WaveFile object as a .wav file.
140
   * @return {string} A .wav file as a base64 string.
141
   * @throws {Error} If any property of the object appears invalid.
142
   */
143
  toBase64() {
144
    /** @type {!Uint8Array} */
145
    let buffer = this.toBuffer();
146
    return encode(buffer, 0, buffer.length);
147
  }
148
149
  /**
150
   * Return a DataURI string representig the WaveFile object as a .wav file.
151
   * The return of this method can be used to load the audio in browsers.
152
   * @return {string} A .wav file as a DataURI.
153
   * @throws {Error} If any property of the object appears invalid.
154
   */
155
  toDataURI() {
156
    return 'data:audio/wav;base64,' + this.toBase64();
157
  }
158
159
  /**
160
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
161
   * @param {string} dataURI A .wav file as DataURI.
162
   * @throws {Error} If any property of the object appears invalid.
163
   */
164
  fromDataURI(dataURI) {
165
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
166
  }
167
168
  /**
169
   * Force a file as RIFF.
170
   */
171
  toRIFF() {
172
    if (this.container == 'RF64') {
173
      this.fromScratch(
174
        this.fmt.numChannels,
175
        this.fmt.sampleRate,
176
        this.bitDepth,
177
        unpackArray(this.data.samples, this.dataType));
178
    } else {
179
      this.dataType.be = true;
180
      this.fromScratch(
181
        this.fmt.numChannels,
182
        this.fmt.sampleRate,
183
        this.bitDepth,
184
        unpackArray(this.data.samples, this.dataType));
185
    }
186
  }
187
188
  /**
189
   * Force a file as RIFX.
190
   */
191
  toRIFX() {
192
    if (this.container == 'RF64') {
193
      this.fromScratch(
194
        this.fmt.numChannels,
195
        this.fmt.sampleRate,
196
        this.bitDepth,
197
        unpackArray(this.data.samples, this.dataType),
198
        {container: 'RIFX'});
199
    } else {
200
      this.fromScratch(
201
        this.fmt.numChannels,
202
        this.fmt.sampleRate,
203
        this.bitDepth,
204
        unpackArray(this.data.samples, this.dataType),
205
        {container: 'RIFX'});
206
    }
207
  }
208
209
  /**
210
   * Change the bit depth of the samples.
211
   * @param {string} newBitDepth The new bit depth of the samples.
212
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
213
   * @param {boolean} changeResolution A boolean indicating if the
214
   *    resolution of samples should be actually changed or not.
215
   * @throws {Error} If the bit depth is not valid.
216
   */
217
  toBitDepth(newBitDepth, changeResolution=true) {
218
    // @type {string}
219
    let toBitDepth = newBitDepth;
220
    // @type {string}
221
    let thisBitDepth = this.bitDepth;
222
    if (!changeResolution) {
223
      toBitDepth = this.realBitDepth_(newBitDepth);
224
      thisBitDepth = this.realBitDepth_(this.bitDepth);
225
    }
226
    this.assureUncompressed_();
227
    // @type {number}
228
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
229
    // @type {!Float64Array}
230
    let typedSamplesInput = new Float64Array(sampleCount + 1);
231
    // @type {!Float64Array}
232
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
233
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
234
    this.truncateSamples(typedSamplesInput);
235
    bitDepthLib(
236
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
237
    this.fromScratch(
238
      this.fmt.numChannels,
239
      this.fmt.sampleRate,
240
      newBitDepth,
241
      typedSamplesOutput,
242
      {container: this.correctContainer_()});
243
  }
244
245
  /**
246
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
247
   * @throws {Error} If sample rate is not 8000.
248
   * @throws {Error} If number of channels is not 1.
249
   */
250
  toIMAADPCM() {
251
    if (this.fmt.sampleRate !== 8000) {
252
      throw new Error(
253
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
254
    } else if(this.fmt.numChannels !== 1) {
255
      throw new Error(
256
        'Only mono files can be compressed as IMA-ADPCM.');
257
    } else {
258
      this.assure16Bit_();
259
      let output = new Int16Array(this.data.samples.length / 2);
260
      unpackArrayTo(this.data.samples, this.dataType, output);
261
      this.fromScratch(
262
        this.fmt.numChannels,
263
        this.fmt.sampleRate,
264
        '4',
265
        imaadpcm.encode(output),
266
        {container: this.correctContainer_()});
267
    }
268
  }
269
270
  /**
271
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
272
   * @param {string} bitDepthCode The new bit depth of the samples.
273
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
274
   *    Optional. Default is 16.
275
   */
276
  fromIMAADPCM(bitDepthCode='16') {
277
    this.fromScratch(
278
      this.fmt.numChannels,
279
      this.fmt.sampleRate,
280
      '16',
281
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
282
      {container: this.correctContainer_()});
283
    if (bitDepthCode != '16') {
284
      this.toBitDepth(bitDepthCode);
285
    }
286
  }
287
288
  /**
289
   * Encode a 16-bit wave file as 8-bit A-Law.
290
   */
291
  toALaw() {
292
    this.assure16Bit_();
293
    let output = new Int16Array(this.data.samples.length / 2);
294
    unpackArrayTo(this.data.samples, this.dataType, output);
295
    this.fromScratch(
296
      this.fmt.numChannels,
297
      this.fmt.sampleRate,
298
      '8a',
299
      alawmulaw.alaw.encode(output),
300
      {container: this.correctContainer_()});
301
  }
302
303
  /**
304
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
305
   * @param {string} bitDepthCode The new bit depth of the samples.
306
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
307
   *    Optional. Default is 16.
308
   */
309
  fromALaw(bitDepthCode='16') {
310
    this.fromScratch(
311
      this.fmt.numChannels,
312
      this.fmt.sampleRate,
313
      '16',
314
      alawmulaw.alaw.decode(this.data.samples),
315
      {container: this.correctContainer_()});
316
    if (bitDepthCode != '16') {
317
      this.toBitDepth(bitDepthCode);
318
    }
319
  }
320
321
  /**
322
   * Encode 16-bit wave file as 8-bit mu-Law.
323
   */
324
  toMuLaw() {
325
    this.assure16Bit_();
326
    let output = new Int16Array(this.data.samples.length / 2);
327
    unpackArrayTo(this.data.samples, this.dataType, output);
328
    this.fromScratch(
329
      this.fmt.numChannels,
330
      this.fmt.sampleRate,
331
      '8m',
332
      alawmulaw.mulaw.encode(output),
333
      {container: this.correctContainer_()});
334
  }
335
336
  /**
337
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
338
   * @param {string} bitDepthCode The new bit depth of the samples.
339
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
340
   *    Optional. Default is 16.
341
   */
342
  fromMuLaw(bitDepthCode='16') {
343
    this.fromScratch(
344
      this.fmt.numChannels,
345
      this.fmt.sampleRate,
346
      '16',
347
      alawmulaw.mulaw.decode(this.data.samples),
348
      {container: this.correctContainer_()});
349
    if (bitDepthCode != '16') {
350
      this.toBitDepth(bitDepthCode);
351
    }
352
  }
353
354
  /**
355
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
356
   * then it is created. It if exists, it is overwritten.
357
   * @param {string} tag The tag name.
358
   * @param {string} value The tag value.
359
   * @throws {Error} If the tag name is not valid.
360
   */
361
  setTag(tag, value) {
362
    tag = this.fixTagName_(tag);
363
    /** @type {!Object} */
364
    let index = this.getTagIndex_(tag);
365
    if (index.TAG !== null) {
366
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
367
        value.length + 1;
368
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
369
    } else if (index.LIST !== null) {
370
      this.LIST[index.LIST].subChunks.push({
371
        chunkId: tag,
372
        chunkSize: value.length + 1,
373
        value: value});
374
    } else {
375
      this.LIST.push({
376
        chunkId: 'LIST',
377
        chunkSize: 8 + value.length + 1,
378
        format: 'INFO',
379
        subChunks: []});
380
      this.LIST[this.LIST.length - 1].subChunks.push({
381
        chunkId: tag,
382
        chunkSize: value.length + 1,
383
        value: value});
384
    }
385
  }
386
387
  /**
388
   * Return the value of a RIFF tag in the INFO chunk.
389
   * @param {string} tag The tag name.
390
   * @return {?string} The value if the tag is found, null otherwise.
391
   */
392
  getTag(tag) {
393
    /** @type {!Object} */
394
    let index = this.getTagIndex_(tag);
395
    if (index.TAG !== null) {
396
      return this.LIST[index.LIST].subChunks[index.TAG].value;
397
    }
398
    return null;
399
  }
400
401
  /**
402
   * Remove a RIFF tag in the INFO chunk.
403
   * @param {string} tag The tag name.
404
   * @return {boolean} True if a tag was deleted.
405
   */
406
  deleteTag(tag) {
407
    /** @type {!Object} */
408
    let index = this.getTagIndex_(tag);
409
    if (index.TAG !== null) {
410
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
411
      return true;
412
    }
413
    return false;
414
  }
415
416
  /**
417
   * Create a cue point in the wave file.
418
   * @param {number} position The cue point position in milliseconds.
419
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
420
   */
421
  setCuePoint(position, labl='') {
422
    this.cue.chunkId = 'cue ';
423
    position = (position * this.fmt.sampleRate) / 1000;
424
    /** @type {!Array<!Object>} */
425
    let existingPoints = this.getCuePoints_();
426
    this.clearLISTadtl_();
427
    /** @type {number} */
428
    let len = this.cue.points.length;
429
    this.cue.points = [];
430
    /** @type {boolean} */
431
    let hasSet = false;
432
    if (len === 0) {
433
      this.setCuePoint_(position, 1, labl);
434
    } else {
435
      for (let i=0; i<len; i++) {
436
        if (existingPoints[i].dwPosition > position && !hasSet) {
437
          this.setCuePoint_(position, i + 1, labl);
438
          this.setCuePoint_(
439
            existingPoints[i].dwPosition,
440
            i + 2,
441
            existingPoints[i].label);
442
          hasSet = true;
443
        } else {
444
          this.setCuePoint_(
445
            existingPoints[i].dwPosition,
446
            i + 1,
447
            existingPoints[i].label);
448
        }
449
      }
450
      if (!hasSet) {
451
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
452
      }
453
    }
454
    this.cue.dwCuePoints = this.cue.points.length;
455
  }
456
457
  /**
458
   * Remove a cue point from a wave file.
459
   * @param {number} index the index of the point. First is 1,
460
   *    second is 2, and so on.
461
   */
462
  deleteCuePoint(index) {
463
    this.cue.chunkId = 'cue ';
464
    /** @type {!Array<!Object>} */
465
    let existingPoints = this.getCuePoints_();
466
    this.clearLISTadtl_();
467
    /** @type {number} */
468
    let len = this.cue.points.length;
469
    this.cue.points = [];
470
    for (let i=0; i<len; i++) {
471
      if (i + 1 !== index) {
472
        this.setCuePoint_(
473
          existingPoints[i].dwPosition,
474
          i + 1,
475
          existingPoints[i].label);
476
      }
477
    }
478
    this.cue.dwCuePoints = this.cue.points.length;
479
    if (this.cue.dwCuePoints) {
480
      this.cue.chunkId = 'cue ';
481
    } else {
482
      this.cue.chunkId = '';
483
      this.clearLISTadtl_();
484
    }
485
  }
486
487
  /**
488
   * Update the label of a cue point.
489
   * @param {number} pointIndex The ID of the cue point.
490
   * @param {string} label The new text for the label.
491
   */
492
  updateLabel(pointIndex, label) {
493
    /** @type {?number} */
494
    let adtlIndex = this.getAdtlChunk_();
495
    if (adtlIndex !== null) {
496
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
497
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
498
            pointIndex) {
499
          this.LIST[adtlIndex].subChunks[i].value = label;
500
        }
501
      }
502
    }
503
  }
504
505
  /**
506
   * Make the file 16-bit if it is not.
507
   * @private
508
   */
509
  assure16Bit_() {
510
    this.assureUncompressed_();
511
    if (this.bitDepth != '16') {
512
      this.toBitDepth('16');
513
    }
514
  }
515
516
  /**
517
   * Uncompress the samples in case of a compressed file.
518
   * @private
519
   */
520
  assureUncompressed_() {
521
    if (this.bitDepth == '8a') {
522
      this.fromALaw();
523
    } else if(this.bitDepth == '8m') {
524
      this.fromMuLaw();
525
    } else if (this.bitDepth == '4') {
526
      this.fromIMAADPCM();
527
    }
528
  }
529
  
530
  /**
531
   * Set up the WaveFile object from a byte buffer.
532
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
533
   * @private
534
   */
535
  interleave_(samples) {
536
    if (samples.length > 0) {
537
      if (samples[0].constructor === Array) {
538
        /** @type {!Array<number>} */
539
        let finalSamples = [];
540
        for (let i=0; i < samples[0].length; i++) {
541
          for (let j=0; j < samples.length; j++) {
542
            finalSamples.push(samples[j][i]);
543
          }
544
        }
545
        samples = finalSamples;
546
      }
547
    }
548
    return samples;
549
  }
550
551
  /**
552
   * Push a new cue point in this.cue.points.
553
   * @param {number} position The position in milliseconds.
554
   * @param {number} dwName the dwName of the cue point
555
   * @private
556
   */
557
  setCuePoint_(position, dwName, label) {
558
    this.cue.points.push({
559
      dwName: dwName,
560
      dwPosition: position,
561
      fccChunk: 'data',
562
      dwChunkStart: 0,
563
      dwBlockStart: 0,
564
      dwSampleOffset: position,
565
    });
566
    this.setLabl_(dwName, label);
567
  }
568
569
  /**
570
   * Return an array with the position of all cue points in the file.
571
   * @return {!Array<!Object>}
572
   * @private
573
   */
574
  getCuePoints_() {
575
    /** @type {!Array<!Object>} */
576
    let points = [];
577
    for (let i=0; i<this.cue.points.length; i++) {
578
      points.push({
579
        dwPosition: this.cue.points[i].dwPosition,
580
        label: this.getLabelForCuePoint_(
581
          this.cue.points[i].dwName)});
582
    }
583
    return points;
584
  }
585
586
  /**
587
   * Return the label of a cue point.
588
   * @param {number} pointDwName The ID of the cue point.
589
   * @return {string}
590
   * @private
591
   */
592
  getLabelForCuePoint_(pointDwName) {
593
    /** @type {?number} */
594
    let adtlIndex = this.getAdtlChunk_();
595
    if (adtlIndex !== null) {
596
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
597
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
598
            pointDwName) {
599
          return this.LIST[adtlIndex].subChunks[i].value;
600
        }
601
      }
602
    }
603
    return '';
604
  }
605
606
  /**
607
   * Clear any LIST chunk labeled as 'adtl'.
608
   * @private
609
   */
610
  clearLISTadtl_() {
611
    for (let i=0; i<this.LIST.length; i++) {
612
      if (this.LIST[i].format == 'adtl') {
613
        this.LIST.splice(i);
614
      }
615
    }
616
  }
617
618
  /**
619
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
620
   * @param {number} dwName The ID of the cue point.
621
   * @param {string} label The label for the cue point.
622
   * @private
623
   */
624
  setLabl_(dwName, label) {
625
    /** @type {?number} */
626
    let adtlIndex = this.getAdtlChunk_();
627
    if (adtlIndex === null) {
628
      this.LIST.push({
629
        chunkId: 'LIST',
630
        chunkSize: 4,
631
        format: 'adtl',
632
        subChunks: []});
633
      adtlIndex = this.LIST.length - 1;
634
    }
635
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
636
  }
637
638
  /**
639
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
640
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
641
   * @param {number} dwName The ID of the cue point.
642
   * @param {string} label The label for the cue point.
643
   * @private
644
   */
645
  setLabelText_(adtlIndex, dwName, label) {
646
    this.LIST[adtlIndex].subChunks.push({
647
      chunkId: 'labl',
648
      chunkSize: label.length,
649
      dwName: dwName,
650
      value: label
651
    });
652
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
653
  }
654
655
  /**
656
   * Return the index of the 'adtl' LIST in this.LIST.
657
   * @return {?number}
658
   * @private
659
   */
660
  getAdtlChunk_() {
661
    for (let i=0; i<this.LIST.length; i++) {
662
      if(this.LIST[i].format == 'adtl') {
663
        return i;
664
      }
665
    }
666
    return null;
667
  }
668
669
  /**
670
   * Return the index of a tag in a FILE chunk.
671
   * @param {string} tag The tag name.
672
   * @return {!Object<string, ?number>}
673
   *    Object.LIST is the INFO index in LIST
674
   *    Object.TAG is the tag index in the INFO
675
   * @private
676
   */
677
  getTagIndex_(tag) {
678
    /** @type {!Object<string, ?number>} */
679
    let index = {LIST: null, TAG: null};
680
    for (let i=0; i<this.LIST.length; i++) {
681
      if (this.LIST[i].format == 'INFO') {
682
        index.LIST = i;
683
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
684
          if (this.LIST[i].subChunks[j].chunkId == tag) {
685
            index.TAG = j;
686
            break;
687
          }
688
        }
689
        break;
690
      }
691
    }
692
    return index;
693
  }
694
695
  /**
696
   * Fix a RIFF tag format if possible, throw an error otherwise.
697
   * @param {string} tag The tag name.
698
   * @return {string} The tag name in proper fourCC format.
699
   * @private
700
   */
701
  fixTagName_(tag) {
702
    if (tag.constructor !== String) {
703
      throw new Error('Invalid tag name.');
704
    } else if(tag.length < 4) {
705
      for (let i=0; i<4-tag.length; i++) {
706
        tag += ' ';
707
      }
708
    }
709
    return tag;
710
  }
711
712
  /**
713
   * Validate the header of the file.
714
   * @throws {Error} If any property of the object appears invalid.
715
   * @private
716
   */
717
  validateHeader_() {
718
    this.validateBitDepth_();
719
    this.validateNumChannels_();
720
    this.validateSampleRate_();
721
  }
722
723
  /**
724
   * Validate the bit depth.
725
   * @return {boolean} True is the bit depth is valid.
726
   * @throws {Error} If bit depth is invalid.
727
   * @private
728
   */
729
  validateBitDepth_() {
730
    if (!AUDIO_FORMATS[this.bitDepth]) {
731
      if (parseInt(this.bitDepth, 10) > 8 &&
732
          parseInt(this.bitDepth, 10) < 54) {
733
        return true;
734
      }
735
      throw new Error('Invalid bit depth.');
736
    }
737
    return true;
738
  }
739
740
  /**
741
   * Validate the number of channels.
742
   * @return {boolean} True is the number of channels is valid.
743
   * @throws {Error} If the number of channels is invalid.
744
   * @private
745
   */
746
  validateNumChannels_() {
747
    /** @type {number} */
748
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
749
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
750
      throw new Error('Invalid number of channels.');
751
    }
752
    return true;
753
  }
754
755
  /**
756
   * Validate the sample rate value.
757
   * @return {boolean} True is the sample rate is valid.
758
   * @throws {Error} If the sample rate is invalid.
759
   * @private
760
   */
761
  validateSampleRate_() {
762
    /** @type {number} */
763
    let byteRate = this.fmt.numChannels *
764
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
765
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
766
      throw new Error('Invalid sample rate.');
767
    }
768
    return true;
769
  }
770
}
771