Completed
Push — master ( 419e18...d611af )
by Rafael S.
02:06
created

index.js ➔ setSample   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 1
dl 0
loc 7
rs 10
nop 2
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,
37
  unpack, packTo} from './vendor/byte-data.js';
38
import makeWavHeader from './lib/make-wav-header.js';
39
import validateWavHeader from './lib/validate-wav-header';
40
import {riffChunks, findChunk_} from './vendor/riff-chunks.js';
41
import writeWavBuffer from './lib/wav-buffer-writer.js';
42
import readWavBuffer from './lib/wav-buffer-reader.js';
43
import WavBuffer from './lib/wav-buffer.js';
44
45
/**
46
 * Class representing a wav file.
47
 * @extends WavBuffer
48
 * @ignore
49
 */
50
export default class WaveFile extends WavBuffer {
51
52
  /**
53
   * @param {?Uint8Array} bytes A wave file buffer.
54
   * @throws {Error} If no 'RIFF' chunk is found.
55
   * @throws {Error} If no 'fmt ' chunk is found.
56
   * @throws {Error} If no 'data' chunk is found.
57
   */
58
  constructor(bytes=null) {
59
    super();
60
    /**
61
     * The bit depth code according to the samples.
62
     * @type {string}
63
     */
64
    this.bitDepth = '0';
65
    /**
66
     * @type {!Object}
67
     * @private
68
     */
69
    this.dataType = {};
70
    // Load a file from the buffer if one was passed
71
    // when creating the object
72
    if (bytes) {
73
      this.fromBuffer(bytes);
74
    }
75
  }
76
77
  /**
78
   * Return the sample at a given index.
79
   * @param {number} index The sample index.
80
   * @return {number} The sample.
81
   * @throws {Error} If the sample index is off range.
82
   */
83
  getSample(index) {
84
    /** @type {number} */
85
    let sample;
0 ignored issues
show
Unused Code introduced by
The variable sample seems to be never used. Consider removing it.
Loading history...
86
    index = index * (this.dataType.bits / 8);
87
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
88
      throw new Error('Range error');
89
    }
90
    return unpack(
91
      this.data.samples.slice(index, index + this.dataType.bits / 8),
92
      this.dataType);
93
  }
94
95
  /**
96
   * Set the sample at a given index.
97
   * @param {number} index The sample index.
98
   * @param {number} sample The sample.
99
   * @throws {Error} If the sample index is off range.
100
   */
101
  setSample(index, sample) {
102
    index = index * (this.dataType.bits / 8);
103
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
104
      throw new Error('Range error');
105
    }
106
    packTo(sample, this.dataType, this.data.samples, index);
107
  }
108
109
  /**
110
   * Set up the WaveFile object based on the arguments passed.
111
   * @param {number} numChannels The number of channels
112
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
113
   * @param {number} sampleRate The sample rate.
114
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
115
   * @param {string} bitDepthCode The audio bit depth code.
116
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
117
   *    or any value between '8' and '32' (like '12').
118
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
119
   *    The samples. Must be in the correct range according to the bit depth.
120
   * @param {?Object} options Optional. Used to force the container
121
   *    as RIFX with {'container': 'RIFX'}
122
   * @throws {Error} If any argument does not meet the criteria.
123
   */
124
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
125
    if (!options.container) {
126
      options.container = 'RIFF';
127
    }
128
    this.container = options.container;
129
    this.bitDepth = bitDepthCode;
130
    samples = this.interleave_(samples);
131
    this.updateDataType_();
132
    /** @type {number} */
133
    let numBytes = this.dataType.bits / 8;
134
    this.data.samples = new Uint8Array(samples.length * numBytes);
135
    packArrayTo(samples, this.dataType, this.data.samples);
136
    /** @type {!Object} */
137
    let header = makeWavHeader(
138
      bitDepthCode, numChannels, sampleRate,
139
      numBytes, this.data.samples.length, options);
140
    this.clearHeader_();
141
    this.chunkSize = header.chunkSize;
142
    this.format = header.format;
143
    this.fmt = header.fmt;
144
    if (header.fact) {
145
      this.fact = header.fact;
146
    }
147
    this.data.chunkId = 'data';
148
    this.data.chunkSize = this.data.samples.length;
149
    validateWavHeader(this);
150
  }
151
152
  /**
153
   * Set up the WaveFile object from a byte buffer.
154
   * @param {!Uint8Array} bytes The buffer.
155
   * @param {boolean=} samples True if the samples should be loaded.
156
   * @throws {Error} If container is not RIFF, RIFX or RF64.
157
   * @throws {Error} If no 'fmt ' chunk is found.
158
   * @throws {Error} If no 'data' chunk is found.
159
   */
160
  fromBuffer(bytes, samples=true) {
161
    this.clearHeader_();
162
    readWavBuffer(bytes, samples, this);
163
    this.bitDepthFromFmt_();
164
    this.updateDataType_();
165
  }
166
167
  /**
168
   * Return a byte buffer representig the WaveFile object as a .wav file.
169
   * The return value of this method can be written straight to disk.
170
   * @return {!Uint8Array} A .wav file.
171
   * @throws {Error} If any property of the object appears invalid.
172
   */
173
  toBuffer() {
174
    validateWavHeader(this);
175
    return writeWavBuffer(this);
176
  }
177
178
  /**
179
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
180
   * @param {string} base64String A .wav file as a base64 string.
181
   * @throws {Error} If any property of the object appears invalid.
182
   */
183
  fromBase64(base64String) {
184
    this.fromBuffer(new Uint8Array(decode(base64String)));
185
  }
186
187
  /**
188
   * Return a base64 string representig the WaveFile object as a .wav file.
189
   * @return {string} A .wav file as a base64 string.
190
   * @throws {Error} If any property of the object appears invalid.
191
   */
192
  toBase64() {
193
    /** @type {!Uint8Array} */
194
    let buffer = this.toBuffer();
195
    return encode(buffer, 0, buffer.length);
196
  }
197
198
  /**
199
   * Return a DataURI string representig the WaveFile object as a .wav file.
200
   * The return of this method can be used to load the audio in browsers.
201
   * @return {string} A .wav file as a DataURI.
202
   * @throws {Error} If any property of the object appears invalid.
203
   */
204
  toDataURI() {
205
    return 'data:audio/wav;base64,' + this.toBase64();
206
  }
207
208
  /**
209
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
210
   * @param {string} dataURI A .wav file as DataURI.
211
   * @throws {Error} If any property of the object appears invalid.
212
   */
213
  fromDataURI(dataURI) {
214
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
215
  }
216
217
  /**
218
   * Force a file as RIFF.
219
   */
220
  toRIFF() {
221
    this.fromScratch(
222
      this.fmt.numChannels,
223
      this.fmt.sampleRate,
224
      this.bitDepth,
225
      unpackArray(this.data.samples, this.dataType));
226
  }
227
228
  /**
229
   * Force a file as RIFX.
230
   */
231
  toRIFX() {
232
    this.fromScratch(
233
      this.fmt.numChannels,
234
      this.fmt.sampleRate,
235
      this.bitDepth,
236
      unpackArray(this.data.samples, this.dataType),
237
      {container: 'RIFX'});
238
  }
239
240
  /**
241
   * Change the bit depth of the samples.
242
   * @param {string} newBitDepth The new bit depth of the samples.
243
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
244
   * @param {boolean} changeResolution A boolean indicating if the
245
   *    resolution of samples should be actually changed or not.
246
   * @throws {Error} If the bit depth is not valid.
247
   */
248
  toBitDepth(newBitDepth, changeResolution=true) {
249
    /** @type {string} */
250
    let toBitDepth = newBitDepth;
251
    /** @type {string} */
252
    let thisBitDepth = this.bitDepth;
253
    if (!changeResolution) {
254
      if (newBitDepth != '32f') {
255
        toBitDepth = this.dataType.bits.toString();
256
      }
257
      thisBitDepth = this.dataType.bits;
258
    }
259
    this.assureUncompressed_();
260
    /** @type {number} */
261
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
262
    /** @type {!Float64Array} */
263
    let typedSamplesInput = new Float64Array(sampleCount + 1);
264
    /** @type {!Float64Array} */
265
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
266
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
267
    bitDepthLib(
268
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
269
    this.fromScratch(
270
      this.fmt.numChannels,
271
      this.fmt.sampleRate,
272
      newBitDepth,
273
      typedSamplesOutput,
274
      {container: this.correctContainer_()});
275
  }
276
277
  /**
278
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
279
   * @throws {Error} If sample rate is not 8000.
280
   * @throws {Error} If number of channels is not 1.
281
   */
282
  toIMAADPCM() {
283
    if (this.fmt.sampleRate !== 8000) {
284
      throw new Error(
285
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
286
    } else if (this.fmt.numChannels !== 1) {
287
      throw new Error(
288
        'Only mono files can be compressed as IMA-ADPCM.');
289
    } else {
290
      this.assure16Bit_();
291
      let output = new Int16Array(this.data.samples.length / 2);
292
      unpackArrayTo(this.data.samples, this.dataType, output);
293
      this.fromScratch(
294
        this.fmt.numChannels,
295
        this.fmt.sampleRate,
296
        '4',
297
        imaadpcm.encode(output),
298
        {container: this.correctContainer_()});
299
    }
300
  }
301
302
  /**
303
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
304
   * @param {string} bitDepthCode The new bit depth of the samples.
305
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
306
   *    Optional. Default is 16.
307
   */
308
  fromIMAADPCM(bitDepthCode='16') {
309
    this.fromScratch(
310
      this.fmt.numChannels,
311
      this.fmt.sampleRate,
312
      '16',
313
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
314
      {container: this.correctContainer_()});
315
    if (bitDepthCode != '16') {
316
      this.toBitDepth(bitDepthCode);
317
    }
318
  }
319
320
  /**
321
   * Encode a 16-bit wave file as 8-bit A-Law.
322
   */
323
  toALaw() {
324
    this.assure16Bit_();
325
    let output = new Int16Array(this.data.samples.length / 2);
326
    unpackArrayTo(this.data.samples, this.dataType, output);
327
    this.fromScratch(
328
      this.fmt.numChannels,
329
      this.fmt.sampleRate,
330
      '8a',
331
      alawmulaw.alaw.encode(output),
332
      {container: this.correctContainer_()});
333
  }
334
335
  /**
336
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
337
   * @param {string} bitDepthCode The new bit depth of the samples.
338
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
339
   *    Optional. Default is 16.
340
   */
341
  fromALaw(bitDepthCode='16') {
342
    this.fromScratch(
343
      this.fmt.numChannels,
344
      this.fmt.sampleRate,
345
      '16',
346
      alawmulaw.alaw.decode(this.data.samples),
347
      {container: this.correctContainer_()});
348
    if (bitDepthCode != '16') {
349
      this.toBitDepth(bitDepthCode);
350
    }
351
  }
352
353
  /**
354
   * Encode 16-bit wave file as 8-bit mu-Law.
355
   */
356
  toMuLaw() {
357
    this.assure16Bit_();
358
    let output = new Int16Array(this.data.samples.length / 2);
359
    unpackArrayTo(this.data.samples, this.dataType, output);
360
    this.fromScratch(
361
      this.fmt.numChannels,
362
      this.fmt.sampleRate,
363
      '8m',
364
      alawmulaw.mulaw.encode(output),
365
      {container: this.correctContainer_()});
366
  }
367
368
  /**
369
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
370
   * @param {string} bitDepthCode The new bit depth of the samples.
371
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
372
   *    Optional. Default is 16.
373
   */
374
  fromMuLaw(bitDepthCode='16') {
375
    this.fromScratch(
376
      this.fmt.numChannels,
377
      this.fmt.sampleRate,
378
      '16',
379
      alawmulaw.mulaw.decode(this.data.samples),
380
      {container: this.correctContainer_()});
381
    if (bitDepthCode != '16') {
382
      this.toBitDepth(bitDepthCode);
383
    }
384
  }
385
386
  /**
387
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
388
   * then it is created. It if exists, it is overwritten.
389
   * @param {string} tag The tag name.
390
   * @param {string} value The tag value.
391
   * @throws {Error} If the tag name is not valid.
392
   */
393
  setTag(tag, value) {
394
    tag = this.fixTagName_(tag);
395
    /** @type {!Object} */
396
    let index = this.getTagIndex_(tag);
397
    if (index.TAG !== null) {
398
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
399
        value.length + 1;
400
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
401
    } else if (index.LIST !== null) {
402
      this.LIST[index.LIST].subChunks.push({
403
        chunkId: tag,
404
        chunkSize: value.length + 1,
405
        value: value});
406
    } else {
407
      this.LIST.push({
408
        chunkId: 'LIST',
409
        chunkSize: 8 + value.length + 1,
410
        format: 'INFO',
411
        subChunks: []});
412
      this.LIST[this.LIST.length - 1].subChunks.push({
413
        chunkId: tag,
414
        chunkSize: value.length + 1,
415
        value: value});
416
    }
417
  }
418
419
  /**
420
   * Return the value of a RIFF tag in the INFO chunk.
421
   * @param {string} tag The tag name.
422
   * @return {?string} The value if the tag is found, null otherwise.
423
   */
424
  getTag(tag) {
425
    /** @type {!Object} */
426
    let index = this.getTagIndex_(tag);
427
    if (index.TAG !== null) {
428
      return this.LIST[index.LIST].subChunks[index.TAG].value;
429
    }
430
    return null;
431
  }
432
433
  /**
434
   * Return a Object<tag, value> with the RIFF tags in the file.
435
   * @return {!Object<string, string>} The file tags.
436
   */
437
  listTags() {
438
    /** @type {?number} */
439
    let index = this.getLISTINFOIndex_();
440
    /** @type {!Object} */
441
    let tags = {};
442
    if (index !== null) {
443
      for (let i=0; i<this.LIST[index].subChunks.length; i++) {
444
        tags[this.LIST[index].subChunks[i].chunkId] =
445
          this.LIST[index].subChunks[i].value;
446
      }
447
    }
448
    return tags;
449
  }
450
451
  /**
452
   * Remove a RIFF tag in the INFO chunk.
453
   * @param {string} tag The tag name.
454
   * @return {boolean} True if a tag was deleted.
455
   */
456
  deleteTag(tag) {
457
    /** @type {!Object} */
458
    let index = this.getTagIndex_(tag);
459
    if (index.TAG !== null) {
460
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
461
      return true;
462
    }
463
    return false;
464
  }
465
466
  /**
467
   * Create a cue point in the wave file.
468
   * @param {number} position The cue point position in milliseconds.
469
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
470
   */
471
  setCuePoint(position, labl='') {
472
    this.cue.chunkId = 'cue ';
473
    position = (position * this.fmt.sampleRate) / 1000;
474
    /** @type {!Array<!Object>} */
475
    let existingPoints = this.getCuePoints_();
476
    this.clearLISTadtl_();
477
    /** @type {number} */
478
    let len = this.cue.points.length;
479
    this.cue.points = [];
480
    /** @type {boolean} */
481
    let hasSet = false;
482
    if (len === 0) {
483
      this.setCuePoint_(position, 1, labl);
484
    } else {
485
      for (let i=0; i<len; i++) {
486
        if (existingPoints[i].dwPosition > position && !hasSet) {
487
          this.setCuePoint_(position, i + 1, labl);
488
          this.setCuePoint_(
489
            existingPoints[i].dwPosition,
490
            i + 2,
491
            existingPoints[i].label);
492
          hasSet = true;
493
        } else {
494
          this.setCuePoint_(
495
            existingPoints[i].dwPosition,
496
            i + 1,
497
            existingPoints[i].label);
498
        }
499
      }
500
      if (!hasSet) {
501
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
502
      }
503
    }
504
    this.cue.dwCuePoints = this.cue.points.length;
505
  }
506
507
  /**
508
   * Remove a cue point from a wave file.
509
   * @param {number} index the index of the point. First is 1,
510
   *    second is 2, and so on.
511
   */
512
  deleteCuePoint(index) {
513
    this.cue.chunkId = 'cue ';
514
    /** @type {!Array<!Object>} */
515
    let existingPoints = this.getCuePoints_();
516
    this.clearLISTadtl_();
517
    /** @type {number} */
518
    let len = this.cue.points.length;
519
    this.cue.points = [];
520
    for (let i=0; i<len; i++) {
521
      if (i + 1 !== index) {
522
        this.setCuePoint_(
523
          existingPoints[i].dwPosition,
524
          i + 1,
525
          existingPoints[i].label);
526
      }
527
    }
528
    this.cue.dwCuePoints = this.cue.points.length;
529
    if (this.cue.dwCuePoints) {
530
      this.cue.chunkId = 'cue ';
531
    } else {
532
      this.cue.chunkId = '';
533
      this.clearLISTadtl_();
534
    }
535
  }
536
537
  /**
538
   * Return an array with all cue points in the file, in the order they appear
539
   * in the file.
540
   * The difference between this method and using the list in WaveFile.cue
541
   * is that the return value of this method includes the position in
542
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
543
   * @return {!Array<!Object>}
544
   */
545
  listCuePoints() {
546
    /** @type {!Array<!Object>} */
547
    let points = this.getCuePoints_();
548
    for (let i=0; i<points.length; i++) {
549
      points[i].milliseconds =
550
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
551
    }
552
    return points;
553
  }
554
555
  /**
556
   * Update the label of a cue point.
557
   * @param {number} pointIndex The ID of the cue point.
558
   * @param {string} label The new text for the label.
559
   */
560
  updateLabel(pointIndex, label) {
561
    /** @type {?number} */
562
    let adtlIndex = this.getAdtlChunk_();
563
    if (adtlIndex !== null) {
564
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
565
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
566
            pointIndex) {
567
          this.LIST[adtlIndex].subChunks[i].value = label;
568
        }
569
      }
570
    }
571
  }
572
573
  /**
574
   * Set the string code of the bit depth based on the 'fmt ' chunk.
575
   * @private
576
   */
577
  bitDepthFromFmt_() {
578
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
579
      this.bitDepth = '32f';
580
    } else if (this.fmt.audioFormat === 6) {
581
      this.bitDepth = '8a';
582
    } else if (this.fmt.audioFormat === 7) {
583
      this.bitDepth = '8m';
584
    } else {
585
      this.bitDepth = this.fmt.bitsPerSample.toString();
586
    }
587
  }
588
  
589
  /**
590
   * Push a new cue point in this.cue.points.
591
   * @param {number} position The position in milliseconds.
592
   * @param {number} dwName the dwName of the cue point
593
   * @private
594
   */
595
  setCuePoint_(position, dwName, label) {
596
    this.cue.points.push({
597
      dwName: dwName,
598
      dwPosition: position,
599
      fccChunk: 'data',
600
      dwChunkStart: 0,
601
      dwBlockStart: 0,
602
      dwSampleOffset: position,
603
    });
604
    this.setLabl_(dwName, label);
605
  }
606
607
  /**
608
   * Return an array with all cue points in the file, in the order they appear
609
   * in the file.
610
   * @return {!Array<!Object>}
611
   * @private
612
   */
613
  getCuePoints_() {
614
    /** @type {!Array<!Object>} */
615
    let points = [];
616
    for (let i=0; i<this.cue.points.length; i++) {
617
      points.push({
618
        dwPosition: this.cue.points[i].dwPosition,
619
        label: this.getLabelForCuePoint_(
620
          this.cue.points[i].dwName)});
621
    }
622
    return points;
623
  }
624
625
  /**
626
   * Return the label of a cue point.
627
   * @param {number} pointDwName The ID of the cue point.
628
   * @return {string}
629
   * @private
630
   */
631
  getLabelForCuePoint_(pointDwName) {
632
    /** @type {?number} */
633
    let adtlIndex = this.getAdtlChunk_();
634
    if (adtlIndex !== null) {
635
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
636
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
637
            pointDwName) {
638
          return this.LIST[adtlIndex].subChunks[i].value;
639
        }
640
      }
641
    }
642
    return '';
643
  }
644
645
  /**
646
   * Clear any LIST chunk labeled as 'adtl'.
647
   * @private
648
   */
649
  clearLISTadtl_() {
650
    for (let i=0; i<this.LIST.length; i++) {
651
      if (this.LIST[i].format == 'adtl') {
652
        this.LIST.splice(i);
653
      }
654
    }
655
  }
656
657
  /**
658
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
659
   * @param {number} dwName The ID of the cue point.
660
   * @param {string} label The label for the cue point.
661
   * @private
662
   */
663
  setLabl_(dwName, label) {
664
    /** @type {?number} */
665
    let adtlIndex = this.getAdtlChunk_();
666
    if (adtlIndex === null) {
667
      this.LIST.push({
668
        chunkId: 'LIST',
669
        chunkSize: 4,
670
        format: 'adtl',
671
        subChunks: []});
672
      adtlIndex = this.LIST.length - 1;
673
    }
674
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
675
  }
676
677
  /**
678
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
679
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
680
   * @param {number} dwName The ID of the cue point.
681
   * @param {string} label The label for the cue point.
682
   * @private
683
   */
684
  setLabelText_(adtlIndex, dwName, label) {
685
    this.LIST[adtlIndex].subChunks.push({
686
      chunkId: 'labl',
687
      chunkSize: label.length,
688
      dwName: dwName,
689
      value: label
690
    });
691
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
692
  }
693
694
  /**
695
   * Return the index of the 'adtl' LIST in this.LIST.
696
   * @return {?number}
697
   * @private
698
   */
699
  getAdtlChunk_() {
700
    for (let i=0; i<this.LIST.length; i++) {
701
      if (this.LIST[i].format == 'adtl') {
702
        return i;
703
      }
704
    }
705
    return null;
706
  }
707
708
  /**
709
   * Return the index of the INFO chunk in the LIST chunk.
710
   * @return {?number} the index of the INFO chunk.
711
   * @private
712
   */
713
  getLISTINFOIndex_() {
714
    /** @type {?number} */
715
    let index = null;
716
    for (let i=0; i<this.LIST.length; i++) {
717
      if (this.LIST[i].format === 'INFO') {
718
        index = i;
719
        break;
720
      }
721
    }
722
    return index;
723
  }
724
725
  /**
726
   * Return the index of a tag in a FILE chunk.
727
   * @param {string} tag The tag name.
728
   * @return {!Object<string, ?number>}
729
   *    Object.LIST is the INFO index in LIST
730
   *    Object.TAG is the tag index in the INFO
731
   * @private
732
   */
733
  getTagIndex_(tag) {
734
    /** @type {!Object<string, ?number>} */
735
    let index = {LIST: null, TAG: null};
736
    for (let i=0; i<this.LIST.length; i++) {
737
      if (this.LIST[i].format == 'INFO') {
738
        index.LIST = i;
739
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
740
          if (this.LIST[i].subChunks[j].chunkId == tag) {
741
            index.TAG = j;
742
            break;
743
          }
744
        }
745
        break;
746
      }
747
    }
748
    return index;
749
  }
750
751
  /**
752
   * Fix a RIFF tag format if possible, throw an error otherwise.
753
   * @param {string} tag The tag name.
754
   * @return {string} The tag name in proper fourCC format.
755
   * @private
756
   */
757
  fixTagName_(tag) {
758
    if (tag.constructor !== String) {
759
      throw new Error('Invalid tag name.');
760
    } else if (tag.length < 4) {
761
      for (let i=0; i<4-tag.length; i++) {
762
        tag += ' ';
763
      }
764
    }
765
    return tag;
766
  }
767
768
  /**
769
   * Reset attributes that should emptied when a file is
770
   * created with the fromScratch() or fromBuffer() methods.
771
   * @private
772
   */
773
  clearHeader_() {
774
    this.fmt.cbSize = 0;
775
    this.fmt.validBitsPerSample = 0;
776
    this.fact.chunkId = '';
777
    this.ds64.chunkId = '';
778
  }
779
780
  /**
781
   * Make the file 16-bit if it is not.
782
   * @private
783
   */
784
  assure16Bit_() {
785
    this.assureUncompressed_();
786
    if (this.bitDepth != '16') {
787
      this.toBitDepth('16');
788
    }
789
  }
790
791
  /**
792
   * Uncompress the samples in case of a compressed file.
793
   * @private
794
   */
795
  assureUncompressed_() {
796
    if (this.bitDepth == '8a') {
797
      this.fromALaw();
798
    } else if (this.bitDepth == '8m') {
799
      this.fromMuLaw();
800
    } else if (this.bitDepth == '4') {
801
      this.fromIMAADPCM();
802
    }
803
  }
804
805
  /**
806
   * Set up the WaveFile object from a byte buffer.
807
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
808
   * @private
809
   */
810
  interleave_(samples) {
811
    if (samples.length > 0) {
812
      if (samples[0].constructor === Array) {
813
        /** @type {!Array<number>} */
814
        let finalSamples = [];
815
        for (let i=0; i < samples[0].length; i++) {
816
          for (let j=0; j < samples.length; j++) {
817
            finalSamples.push(samples[j][i]);
818
          }
819
        }
820
        samples = finalSamples;
821
      }
822
    }
823
    return samples;
824
  }
825
826
  /**
827
   * Update the type definition used to read and write the samples.
828
   * @private
829
   */
830
  updateDataType_() {
831
    /** @type {!Object} */
832
    this.dataType = {
833
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
834
      float: this.bitDepth == '32f' || this.bitDepth == '64',
835
      signed: this.bitDepth != '8',
836
      be: this.container == 'RIFX'
837
    };
838
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
839
      this.dataType.bits = 8;
840
      this.dataType.signed = false;
841
    }
842
  }
843
844
  /**
845
   * Return 'RIFF' if the container is 'RF64', the current container name
846
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
847
   * @return {string}
848
   * @private
849
   */
850
  correctContainer_() {
851
    return this.container == 'RF64' ? 'RIFF' : this.container;
852
  }
853
}
854