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