Passed
Push — master ( 6ca9b2...ebd43e )
by Rafael S.
04:26
created

WaveFileReader   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 574
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 41
eloc 275
dl 0
loc 574
rs 9.1199
c 0
b 0
f 0

16 Functions

Rating   Name   Duplication   Size   Complexity  
B constructor 0 207 1
A readSmplChunk_ 0 28 3
A readBextChunk_ 0 27 2
B readLISTSubChunks_ 0 31 5
A fromBuffer 0 17 2
A readDs64Chunk_ 0 26 3
A readFmtExtension_ 0 16 4
A readFmtChunk_ 0 18 2
A readCueChunk_ 0 20 3
A clearHeader 0 6 1
A readLISTChunk_ 0 19 4
A readJunkChunk_ 0 13 2
A readDataChunk_ 0 15 3
A readFactChunk_ 0 10 2
A readZSTR_ 0 9 3
A readUInt16_ 0 6 1

How to fix   Complexity   

Complexity

Complex classes like WaveFileReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/*
2
 * Copyright (c) 2017-2019 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 WaveFileReader class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import RIFFFile from './riff-file';
33
import {unpackString, unpack} from 'byte-data';
34
35
/**
36
 * A class to read wav files.
37
 */
38
export default class WaveFileReader extends RIFFFile {
39
40
  constructor() {
41
    super();
42
    // Include 'RF64' as a supported container format
43
    this.supported_containers.push('RF64');
44
    /**
45
     * The data of the 'fmt' chunk.
46
     * @type {!Object<string, *>}
47
     */
48
    this.fmt = {
49
      /** @type {string} */
50
      chunkId: '',
51
      /** @type {number} */
52
      chunkSize: 0,
53
      /** @type {number} */
54
      audioFormat: 0,
55
      /** @type {number} */
56
      numChannels: 0,
57
      /** @type {number} */
58
      sampleRate: 0,
59
      /** @type {number} */
60
      byteRate: 0,
61
      /** @type {number} */
62
      blockAlign: 0,
63
      /** @type {number} */
64
      bitsPerSample: 0,
65
      /** @type {number} */
66
      cbSize: 0,
67
      /** @type {number} */
68
      validBitsPerSample: 0,
69
      /** @type {number} */
70
      dwChannelMask: 0,
71
      /**
72
       * 4 32-bit values representing a 128-bit ID
73
       * @type {!Array<number>}
74
       */
75
      subformat: []
76
    };
77
    /**
78
     * The data of the 'fact' chunk.
79
     * @type {!Object<string, *>}
80
     */
81
    this.fact = {
82
      /** @type {string} */
83
      chunkId: '',
84
      /** @type {number} */
85
      chunkSize: 0,
86
      /** @type {number} */
87
      dwSampleLength: 0
88
    };
89
    /**
90
     * The data of the 'cue ' chunk.
91
     * @type {!Object<string, *>}
92
     */
93
    this.cue = {
94
      /** @type {string} */
95
      chunkId: '',
96
      /** @type {number} */
97
      chunkSize: 0,
98
      /** @type {number} */
99
      dwCuePoints: 0,
100
      /** @type {!Array<!Object>} */
101
      points: [],
102
    };
103
    /**
104
     * The data of the 'smpl' chunk.
105
     * @type {!Object<string, *>}
106
     */
107
    this.smpl = {
108
      /** @type {string} */
109
      chunkId: '',
110
      /** @type {number} */
111
      chunkSize: 0,
112
      /** @type {number} */
113
      dwManufacturer: 0,
114
      /** @type {number} */
115
      dwProduct: 0,
116
      /** @type {number} */
117
      dwSamplePeriod: 0,
118
      /** @type {number} */
119
      dwMIDIUnityNote: 0,
120
      /** @type {number} */
121
      dwMIDIPitchFraction: 0,
122
      /** @type {number} */
123
      dwSMPTEFormat: 0,
124
      /** @type {number} */
125
      dwSMPTEOffset: 0,
126
      /** @type {number} */
127
      dwNumSampleLoops: 0,
128
      /** @type {number} */
129
      dwSamplerData: 0,
130
      /** @type {!Array<!Object>} */
131
      loops: []
132
    };
133
    /**
134
     * The data of the 'bext' chunk.
135
     * @type {!Object<string, *>}
136
     */
137
    this.bext = {
138
      /** @type {string} */
139
      chunkId: '',
140
      /** @type {number} */
141
      chunkSize: 0,
142
      /** @type {string} */
143
      description: '', //256
144
      /** @type {string} */
145
      originator: '', //32
146
      /** @type {string} */
147
      originatorReference: '', //32
148
      /** @type {string} */
149
      originationDate: '', //10
150
      /** @type {string} */
151
      originationTime: '', //8
152
      /**
153
       * 2 32-bit values, timeReference high and low
154
       * @type {!Array<number>}
155
       */
156
      timeReference: [0, 0],
157
      /** @type {number} */
158
      version: 0, //WORD
159
      /** @type {string} */
160
      UMID: '', // 64 chars
161
      /** @type {number} */
162
      loudnessValue: 0, //WORD
163
      /** @type {number} */
164
      loudnessRange: 0, //WORD
165
      /** @type {number} */
166
      maxTruePeakLevel: 0, //WORD
167
      /** @type {number} */
168
      maxMomentaryLoudness: 0, //WORD
169
      /** @type {number} */
170
      maxShortTermLoudness: 0, //WORD
171
      /** @type {string} */
172
      reserved: '', //180
173
      /** @type {string} */
174
      codingHistory: '' // string, unlimited
175
    };
176
    /**
177
     * The data of the 'ds64' chunk.
178
     * Used only with RF64 files.
179
     * @type {!Object<string, *>}
180
     */
181
    this.ds64 = {
182
      /** @type {string} */
183
      chunkId: '',
184
      /** @type {number} */
185
      chunkSize: 0,
186
      /** @type {number} */
187
      riffSizeHigh: 0, // DWORD
188
      /** @type {number} */
189
      riffSizeLow: 0, // DWORD
190
      /** @type {number} */
191
      dataSizeHigh: 0, // DWORD
192
      /** @type {number} */
193
      dataSizeLow: 0, // DWORD
194
      /** @type {number} */
195
      originationTime: 0, // DWORD
196
      /** @type {number} */
197
      sampleCountHigh: 0, // DWORD
198
      /** @type {number} */
199
      sampleCountLow: 0 // DWORD
200
      /** @type {number} */
201
      //'tableLength': 0, // DWORD
202
      /** @type {!Array<number>} */
203
      //'table': []
204
    };
205
    /**
206
     * The data of the 'data' chunk.
207
     * @type {!Object<string, *>}
208
     */
209
    this.data = {
210
      /** @type {string} */
211
      chunkId: '',
212
      /** @type {number} */
213
      chunkSize: 0,
214
      /** @type {!Uint8Array} */
215
      samples: new Uint8Array(0)
216
    };
217
    /**
218
     * The data of the 'LIST' chunks.
219
     * Each item in this list look like this:
220
     *  {
221
     *      chunkId: '',
222
     *      chunkSize: 0,
223
     *      format: '',
224
     *      subChunks: []
225
     *   }
226
     * @type {!Array<!Object>}
227
     */
228
    this.LIST = [];
229
    /**
230
     * The data of the 'junk' chunk.
231
     * @type {!Object<string, *>}
232
     */
233
    this.junk = {
234
      /** @type {string} */
235
      chunkId: '',
236
      /** @type {number} */
237
      chunkSize: 0,
238
      /** @type {!Array<number>} */
239
      chunkData: []
240
    };
241
    /**
242
     * @type {!Object}
243
     * @protected
244
     */
245
    this.uInt16 = {bits: 16, be: false};
246
  }
247
248
  /**
249
   * Set up the WaveFileReader object from a byte buffer.
250
   * @param {!Uint8Array} wavBuffer The buffer.
251
   * @param {boolean=} samples True if the samples should be loaded.
252
   * @throws {Error} If container is not RIFF, RIFX or RF64.
253
   * @throws {Error} If format is not WAVE.
254
   * @throws {Error} If no 'fmt ' chunk is found.
255
   * @throws {Error} If no 'data' chunk is found.
256
   */
257
  fromBuffer(wavBuffer, samples=true) {
258
    this.clearHeader();
259
    this.setSignature(wavBuffer);
260
    this.uInt16.be = this.uInt32.be;
261
    if (this.format != 'WAVE') {
262
      throw Error('Could not find the "WAVE" format identifier');
263
    }
264
    this.readDs64Chunk_(wavBuffer);
265
    this.readFmtChunk_(wavBuffer);
266
    this.readFactChunk_(wavBuffer);
267
    this.readBextChunk_(wavBuffer);
268
    this.readCueChunk_(wavBuffer);
269
    this.readSmplChunk_(wavBuffer);
270
    this.readDataChunk_(wavBuffer, samples);
271
    this.readJunkChunk_(wavBuffer);
272
    this.readLISTChunk_(wavBuffer);
273
  }
274
275
  /**
276
   * Reset some attributes of the object.
277
   * @protected
278
   * @ignore
279
   */
280
  clearHeader() {
281
    this.fmt.cbSize = 0;
282
    this.fmt.validBitsPerSample = 0;
283
    this.fact.chunkId = '';
284
    this.ds64.chunkId = '';
285
  }
286
287
  /**
288
   * Read the 'fmt ' chunk of a wave file.
289
   * @param {!Uint8Array} buffer The wav file buffer.
290
   * @throws {Error} If no 'fmt ' chunk is found.
291
   * @private
292
   */
293
  readFmtChunk_(buffer) {
294
    /** @type {?Object} */
295
    let chunk = this.findChunk('fmt ');
296
    if (chunk) {
297
      this.head = chunk.chunkData.start;
298
      this.fmt.chunkId = chunk.chunkId;
299
      this.fmt.chunkSize = chunk.chunkSize;
300
      this.fmt.audioFormat = this.readUInt16_(buffer);
301
      this.fmt.numChannels = this.readUInt16_(buffer);
302
      this.fmt.sampleRate = this.readUInt32(buffer);
303
      this.fmt.byteRate = this.readUInt32(buffer);
304
      this.fmt.blockAlign = this.readUInt16_(buffer);
305
      this.fmt.bitsPerSample = this.readUInt16_(buffer);
306
      this.readFmtExtension_(buffer);
307
    } else {
308
      throw Error('Could not find the "fmt " chunk');
309
    }
310
  }
311
312
  /**
313
   * Read the 'fmt ' chunk extension.
314
   * @param {!Uint8Array} buffer The wav file buffer.
315
   * @private
316
   */
317
  readFmtExtension_(buffer) {
318
    if (this.fmt.chunkSize > 16) {
319
      this.fmt.cbSize = this.readUInt16_(buffer);
320
      if (this.fmt.chunkSize > 18) {
321
        this.fmt.validBitsPerSample = this.readUInt16_(buffer);
322
        if (this.fmt.chunkSize > 20) {
323
          this.fmt.dwChannelMask = this.readUInt32(buffer);
324
          this.fmt.subformat = [
325
            this.readUInt32(buffer),
326
            this.readUInt32(buffer),
327
            this.readUInt32(buffer),
328
            this.readUInt32(buffer)];
329
        }
330
      }
331
    }
332
  }
333
334
  /**
335
   * Read the 'fact' chunk of a wav file.
336
   * @param {!Uint8Array} buffer The wav file buffer.
337
   * @private
338
   */
339
  readFactChunk_(buffer) {
340
    /** @type {?Object} */
341
    let chunk = this.findChunk('fact');
342
    if (chunk) {
343
      this.head = chunk.chunkData.start;
344
      this.fact.chunkId = chunk.chunkId;
345
      this.fact.chunkSize = chunk.chunkSize;
346
      this.fact.dwSampleLength = this.readUInt32(buffer);
347
    }
348
  }
349
350
  /**
351
   * Read the 'cue ' chunk of a wave file.
352
   * @param {!Uint8Array} buffer The wav file buffer.
353
   * @private
354
   */
355
  readCueChunk_(buffer) {
356
    /** @type {?Object} */
357
    let chunk = this.findChunk('cue ');
358
    if (chunk) {
359
      this.head = chunk.chunkData.start;
360
      this.cue.chunkId = chunk.chunkId;
361
      this.cue.chunkSize = chunk.chunkSize;
362
      this.cue.dwCuePoints = this.readUInt32(buffer);
363
      for (let i = 0; i < this.cue.dwCuePoints; i++) {
364
        this.cue.points.push({
365
          dwName: this.readUInt32(buffer),
366
          dwPosition: this.readUInt32(buffer),
367
          fccChunk: this.readString(buffer, 4),
368
          dwChunkStart: this.readUInt32(buffer),
369
          dwBlockStart: this.readUInt32(buffer),
370
          dwSampleOffset: this.readUInt32(buffer),
371
        });
372
      }
373
    }
374
  }
375
376
  /**
377
   * Read the 'smpl' chunk of a wave file.
378
   * @param {!Uint8Array} buffer The wav file buffer.
379
   * @private
380
   */
381
  readSmplChunk_(buffer) {
382
    /** @type {?Object} */
383
    let chunk = this.findChunk('smpl');
384
    if (chunk) {
385
      this.head = chunk.chunkData.start;
386
      this.smpl.chunkId = chunk.chunkId;
387
      this.smpl.chunkSize = chunk.chunkSize;
388
      this.smpl.dwManufacturer = this.readUInt32(buffer);
389
      this.smpl.dwProduct = this.readUInt32(buffer);
390
      this.smpl.dwSamplePeriod = this.readUInt32(buffer);
391
      this.smpl.dwMIDIUnityNote = this.readUInt32(buffer);
392
      this.smpl.dwMIDIPitchFraction = this.readUInt32(buffer);
393
      this.smpl.dwSMPTEFormat = this.readUInt32(buffer);
394
      this.smpl.dwSMPTEOffset = this.readUInt32(buffer);
395
      this.smpl.dwNumSampleLoops = this.readUInt32(buffer);
396
      this.smpl.dwSamplerData = this.readUInt32(buffer);
397
      for (let i = 0; i < this.smpl.dwNumSampleLoops; i++) {
398
        this.smpl.loops.push({
399
          dwName: this.readUInt32(buffer),
400
          dwType: this.readUInt32(buffer),
401
          dwStart: this.readUInt32(buffer),
402
          dwEnd: this.readUInt32(buffer),
403
          dwFraction: this.readUInt32(buffer),
404
          dwPlayCount: this.readUInt32(buffer),
405
        });
406
      }
407
    }
408
  }
409
410
  /**
411
   * Read the 'data' chunk of a wave file.
412
   * @param {!Uint8Array} buffer The wav file buffer.
413
   * @param {boolean} samples True if the samples should be loaded.
414
   * @throws {Error} If no 'data' chunk is found.
415
   * @private
416
   */
417
  readDataChunk_(buffer, samples) {
418
    /** @type {?Object} */
419
    let chunk = this.findChunk('data');
420
    if (chunk) {
421
      this.data.chunkId = 'data';
422
      this.data.chunkSize = chunk.chunkSize;
423
      if (samples) {
424
        this.data.samples = buffer.slice(
425
          chunk.chunkData.start,
426
          chunk.chunkData.end);
427
      }
428
    } else {
429
      throw Error('Could not find the "data" chunk');
430
    }
431
  }
432
433
  /**
434
   * Read the 'bext' chunk of a wav file.
435
   * @param {!Uint8Array} buffer The wav file buffer.
436
   * @private
437
   */
438
  readBextChunk_(buffer) {
439
    /** @type {?Object} */
440
    let chunk = this.findChunk('bext');
441
    if (chunk) {
442
      this.head = chunk.chunkData.start;
443
      this.bext.chunkId = chunk.chunkId;
444
      this.bext.chunkSize = chunk.chunkSize;
445
      this.bext.description = this.readString(buffer, 256);
446
      this.bext.originator = this.readString(buffer, 32);
447
      this.bext.originatorReference = this.readString(buffer, 32);
448
      this.bext.originationDate = this.readString(buffer, 10);
449
      this.bext.originationTime = this.readString(buffer, 8);
450
      this.bext.timeReference = [
451
        this.readUInt32(buffer),
452
        this.readUInt32(buffer)];
453
      this.bext.version = this.readUInt16_(buffer);
454
      this.bext.UMID = this.readString(buffer, 64);
455
      this.bext.loudnessValue = this.readUInt16_(buffer);
456
      this.bext.loudnessRange = this.readUInt16_(buffer);
457
      this.bext.maxTruePeakLevel = this.readUInt16_(buffer);
458
      this.bext.maxMomentaryLoudness = this.readUInt16_(buffer);
459
      this.bext.maxShortTermLoudness = this.readUInt16_(buffer);
460
      this.bext.reserved = this.readString(buffer, 180);
461
      this.bext.codingHistory = this.readString(
462
        buffer, this.bext.chunkSize - 602);
463
    }
464
  }
465
466
  /**
467
   * Read the 'ds64' chunk of a wave file.
468
   * @param {!Uint8Array} buffer The wav file buffer.
469
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
470
   * @private
471
   */
472
  readDs64Chunk_(buffer) {
473
    /** @type {?Object} */
474
    let chunk = this.findChunk('ds64');
475
    if (chunk) {
476
      this.head = chunk.chunkData.start;
477
      this.ds64.chunkId = chunk.chunkId;
478
      this.ds64.chunkSize = chunk.chunkSize;
479
      this.ds64.riffSizeHigh = this.readUInt32(buffer);
480
      this.ds64.riffSizeLow = this.readUInt32(buffer);
481
      this.ds64.dataSizeHigh = this.readUInt32(buffer);
482
      this.ds64.dataSizeLow = this.readUInt32(buffer);
483
      this.ds64.originationTime = this.readUInt32(buffer);
484
      this.ds64.sampleCountHigh = this.readUInt32(buffer);
485
      this.ds64.sampleCountLow = this.readUInt32(buffer);
486
      //if (wav.ds64.chunkSize > 28) {
487
      //  wav.ds64.tableLength = unpack(
488
      //    chunkData.slice(28, 32), uInt32_);
489
      //  wav.ds64.table = chunkData.slice(
490
      //     32, 32 + wav.ds64.tableLength);
491
      //}
492
    } else {
493
      if (this.container == 'RF64') {
494
        throw Error('Could not find the "ds64" chunk');
495
      }
496
    }
497
  }
498
499
  /**
500
   * Read the 'LIST' chunks of a wave file.
501
   * @param {!Uint8Array} buffer The wav file buffer.
502
   * @private
503
   */
504
  readLISTChunk_(buffer) {
505
    /** @type {?Object} */
506
    let listChunks = this.findChunk('LIST', true);
507
    if (listChunks !== null) {
508
      for (let j=0; j < listChunks.length; j++) {
509
        /** @type {!Object} */
510
        let subChunk = listChunks[j];
511
        this.LIST.push({
512
          chunkId: subChunk.chunkId,
513
          chunkSize: subChunk.chunkSize,
514
          format: subChunk.format,
515
          subChunks: []});
516
        for (let x=0; x<subChunk.subChunks.length; x++) {
517
          this.readLISTSubChunks_(subChunk.subChunks[x],
518
            subChunk.format, buffer);
519
        }
520
      }
521
    }
522
  }
523
524
  /**
525
   * Read the sub chunks of a 'LIST' chunk.
526
   * @param {!Object} subChunk The 'LIST' subchunks.
527
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
528
   * @param {!Uint8Array} buffer The wav file buffer.
529
   * @private
530
   */
531
  readLISTSubChunks_(subChunk, format, buffer) {
532
    if (format == 'adtl') {
533
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
534
        this.head = subChunk.chunkData.start;
535
        /** @type {!Object<string, string|number>} */
536
        let item = {
537
          chunkId: subChunk.chunkId,
538
          chunkSize: subChunk.chunkSize,
539
          dwName: this.readUInt32(buffer)
540
        };
541
        if (subChunk.chunkId == 'ltxt') {
542
          item.dwSampleLength = this.readUInt32(buffer);
543
          item.dwPurposeID = this.readUInt32(buffer);
544
          item.dwCountry = this.readUInt16_(buffer);
545
          item.dwLanguage = this.readUInt16_(buffer);
546
          item.dwDialect = this.readUInt16_(buffer);
547
          item.dwCodePage = this.readUInt16_(buffer);
548
        }
549
        item.value = this.readZSTR_(buffer, this.head);
550
        this.LIST[this.LIST.length - 1].subChunks.push(item);
551
      }
552
    // RIFF INFO tags like ICRD, ISFT, ICMT
553
    } else if(format == 'INFO') {
554
      this.head = subChunk.chunkData.start;
555
      this.LIST[this.LIST.length - 1].subChunks.push({
556
        chunkId: subChunk.chunkId,
557
        chunkSize: subChunk.chunkSize,
558
        value: this.readZSTR_(buffer, this.head)
559
      });
560
    }
561
  }
562
563
  /**
564
   * Read the 'junk' chunk of a wave file.
565
   * @param {!Uint8Array} buffer The wav file buffer.
566
   * @private
567
   */
568
  readJunkChunk_(buffer) {
569
    /** @type {?Object} */
570
    let chunk = this.findChunk('junk');
571
    if (chunk) {
572
      this.junk = {
573
        chunkId: chunk.chunkId,
574
        chunkSize: chunk.chunkSize,
575
        chunkData: [].slice.call(buffer.slice(
576
          chunk.chunkData.start,
577
          chunk.chunkData.end))
578
      };
579
    }
580
  }
581
582
  /**
583
   * Read bytes as a ZSTR string.
584
   * @param {!Uint8Array} bytes The bytes.
585
   * @param {number} index the index to start reading.
586
   * @return {string} The string.
587
   * @private
588
   */
589
  readZSTR_(bytes, index=0) {
590
    for (let i = index; i < bytes.length; i++) {
591
      this.head++;
592
      if (bytes[i] === 0) {
593
        break;
594
      }
595
    }
596
    return unpackString(bytes, index, this.head - 1);
597
  }
598
599
  /**
600
   * Read a number from a chunk.
601
   * @param {!Uint8Array} bytes The chunk bytes.
602
   * @return {number} The number.
603
   * @private
604
   */
605
  readUInt16_(bytes) {
606
    /** @type {number} */
607
    let value = unpack(bytes, this.uInt16, this.head);
608
    this.head += 2;
609
    return value;
610
  }
611
}
612