Passed
Branch wavefile-reader (40d3f0)
by Rafael S.
02:22
created

lib/wavefile-parser.js   C

Complexity

Total Complexity 55
Complexity/F 2.62

Size

Lines of Code 507
Function Count 21

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 55
eloc 273
mnd 34
bc 34
fnc 21
dl 0
loc 507
rs 6
bpm 1.619
cpm 2.619
noi 1
c 0
b 0
f 0

21 Functions

Rating   Name   Duplication   Size   Complexity  
A WaveFileParser.toBuffer 0 4 1
A WaveFileParser.validateWavHeader 0 10 3
A WaveFileParser.getFmtExtensionBytes_ 0 24 5
A WaveFileParser.writeWavBuffer_ 0 35 3
A WaveFileParser.fromBuffer 0 4 1
A WaveFileParser.getLtxtChunkBytes_ 0 13 1
A WaveFileParser.getSmplLoopsBytes_ 0 14 2
A WaveFileParser.getCueBytes_ 0 14 2
A WaveFileParser.enforceBext_ 0 13 5
A WaveFileParser.getLISTBytes_ 0 15 2
A WaveFileParser.getCuePointsBytes_ 0 14 2
A WaveFileParser.getBextBytes_ 0 29 2
A WaveFileParser.validateBitDepth_ 0 10 3
A WaveFileParser.getSmplBytes_ 0 22 2
A WaveFileParser.getJunkBytes_ 0 11 2
A WaveFileParser.getFmtBytes_ 0 17 2
A WaveFileParser.bitDepthFromFmt_ 0 11 4
A WaveFileParser.getDs64Bytes_ 0 22 2
A WaveFileParser.constructor 0 29 2
A WaveFileParser.getFactBytes_ 0 11 2
B WaveFileParser.getLISTSubChunksBytes_ 0 33 7

How to fix   Complexity   

Complexity

Complex classes like lib/wavefile-parser.js 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 WaveFileParser class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import WaveFileReader from './wavefile-reader';
33
import writeString from './write-string';
34
import validateNumChannels from './validate-num-channels'; 
35
import validateSampleRate from './validate-sample-rate';
36
import {unpack, packTo, packStringTo, packString, pack} from 'byte-data';
0 ignored issues
show
Unused Code introduced by
The variable unpack seems to be never used. Consider removing it.
Loading history...
37
38
/**
39
 * A class to read and write wav files.
40
 * @extends WaveFileReader
41
 */
42
export default class WaveFileParser extends WaveFileReader {
43
44
  /**
45
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
46
   * @throws {Error} If container is not RIFF, RIFX or RF64.
47
   * @throws {Error} If format is not WAVE.
48
   * @throws {Error} If no 'fmt ' chunk is found.
49
   * @throws {Error} If no 'data' chunk is found.
50
   */
51
  constructor(wavBuffer=null) {
52
    super(wavBuffer);
53
    /**
54
     * The bit depth code according to the samples.
55
     * @type {string}
56
     */
57
    this.bitDepth = '0';
58
    /**
59
     * Audio formats.
60
     * Formats not listed here should be set to 65534,
61
     * the code for WAVE_FORMAT_EXTENSIBLE
62
     * @enum {number}
63
     * @protected
64
     */
65
    this.WAV_AUDIO_FORMATS = {
66
      '4': 17,
67
      '8': 1,
68
      '8a': 6,
69
      '8m': 7,
70
      '16': 1,
71
      '24': 1,
72
      '32': 1,
73
      '32f': 3,
74
      '64': 3
75
    };
76
    if (wavBuffer) {
77
      this.bitDepthFromFmt_();
78
    }
79
  }
80
81
  /**
82
   * Set up the WaveFileParser object from a byte buffer.
83
   * @param {!Uint8Array} wavBuffer The buffer.
84
   * @param {boolean=} samples True if the samples should be loaded.
85
   * @throws {Error} If container is not RIFF, RIFX or RF64.
86
   * @throws {Error} If format is not WAVE.
87
   * @throws {Error} If no 'fmt ' chunk is found.
88
   * @throws {Error} If no 'data' chunk is found.
89
   */
90
  fromBuffer(wavBuffer, samples=true) {
91
    super.fromBuffer(wavBuffer, samples);
92
    this.bitDepthFromFmt_();
93
  }
94
95
  /**
96
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
97
   * The return value of this method can be written straight to disk.
98
   * @return {!Uint8Array} A wav file.
99
   * @throws {Error} If bit depth is invalid.
100
   * @throws {Error} If the number of channels is invalid.
101
   * @throws {Error} If the sample rate is invalid.
102
   */
103
  toBuffer() {
104
    this.validateWavHeader();
105
    return this.writeWavBuffer_();
106
  }
107
108
  /**
109
   * Validate the header of the file.
110
   * @throws {Error} If bit depth is invalid.
111
   * @throws {Error} If the number of channels is invalid.
112
   * @throws {Error} If the sample rate is invalid.
113
   * @ignore
114
   * @protected
115
   */
116
  validateWavHeader() {
117
    this.validateBitDepth_();
118
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
119
      throw new Error('Invalid number of channels.');
120
    }
121
    if (!validateSampleRate(
122
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
123
      throw new Error('Invalid sample rate.');
124
    }
125
  }
126
127
  /**
128
   * Set the string code of the bit depth based on the 'fmt ' chunk.
129
   * @private
130
   */
131
  bitDepthFromFmt_() {
132
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
133
      this.bitDepth = '32f';
134
    } else if (this.fmt.audioFormat === 6) {
135
      this.bitDepth = '8a';
136
    } else if (this.fmt.audioFormat === 7) {
137
      this.bitDepth = '8m';
138
    } else {
139
      this.bitDepth = this.fmt.bitsPerSample.toString();
140
    }
141
  }
142
143
  /**
144
   * Return a .wav file byte buffer with the data from the WaveFileParser object.
145
   * The return value of this method can be written straight to disk.
146
   * @return {!Uint8Array} The wav file bytes.
147
   * @private
148
   */
149
  writeWavBuffer_() {
150
    this.uInt16.be = this.container === 'RIFX';
151
    this.uInt32.be = this.uInt16.be;
152
    /** @type {!Array<!Array<number>>} */
153
    let fileBody = [
154
      this.getJunkBytes_(),
155
      this.getDs64Bytes_(),
156
      this.getBextBytes_(),
157
      this.getFmtBytes_(),
158
      this.getFactBytes_(),
159
      packString(this.data.chunkId),
160
      pack(this.data.samples.length, this.uInt32),
161
      this.data.samples,
162
      this.getCueBytes_(),
163
      this.getSmplBytes_(),
164
      this.getLISTBytes_()
165
    ];
166
    /** @type {number} */
167
    let fileBodyLength = 0;
168
    for (let i=0; i<fileBody.length; i++) {
169
      fileBodyLength += fileBody[i].length;
170
    }
171
    /** @type {!Uint8Array} */
172
    let file = new Uint8Array(fileBodyLength + 12);
173
    /** @type {number} */
174
    let index = 0;
175
    index = packStringTo(this.container, file, index);
176
    index = packTo(fileBodyLength + 4, this.uInt32, file, index);
177
    index = packStringTo(this.format, file, index);
178
    for (let i=0; i<fileBody.length; i++) {
179
      file.set(fileBody[i], index);
180
      index += fileBody[i].length;
181
    }
182
    return file;
183
  }
184
185
  /**
186
   * Return the bytes of the 'bext' chunk.
187
   * @private
188
   */
189
  getBextBytes_() {
190
    /** @type {!Array<number>} */
191
    let bytes = [];
192
    this.enforceBext_();
193
    if (this.bext.chunkId) {
194
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
195
      bytes = bytes.concat(
196
        packString(this.bext.chunkId),
197
        pack(602 + this.bext.codingHistory.length, this.uInt32),
198
        writeString(this.bext.description, 256),
199
        writeString(this.bext.originator, 32),
200
        writeString(this.bext.originatorReference, 32),
201
        writeString(this.bext.originationDate, 10),
202
        writeString(this.bext.originationTime, 8),
203
        pack(this.bext.timeReference[0], this.uInt32),
204
        pack(this.bext.timeReference[1], this.uInt32),
205
        pack(this.bext.version, this.uInt16),
206
        writeString(this.bext.UMID, 64),
207
        pack(this.bext.loudnessValue, this.uInt16),
208
        pack(this.bext.loudnessRange, this.uInt16),
209
        pack(this.bext.maxTruePeakLevel, this.uInt16),
210
        pack(this.bext.maxMomentaryLoudness, this.uInt16),
211
        pack(this.bext.maxShortTermLoudness, this.uInt16),
212
        writeString(this.bext.reserved, 180),
213
        writeString(
214
          this.bext.codingHistory, this.bext.codingHistory.length));
215
    }
216
    return bytes;
217
  }
218
219
  /**
220
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
221
   * @private
222
   */
223
  enforceBext_() {
224
    for (let prop in this.bext) {
225
      if (this.bext.hasOwnProperty(prop)) {
226
        if (this.bext[prop] && prop != 'timeReference') {
227
          this.bext.chunkId = 'bext';
228
          break;
229
        }
230
      }
231
    }
232
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
233
      this.bext.chunkId = 'bext';
234
    }
235
  }
236
237
  /**
238
   * Return the bytes of the 'ds64' chunk.
239
   * @return {!Array<number>} The 'ds64' chunk bytes.
240
   * @private
241
   */
242
  getDs64Bytes_() {
243
    /** @type {!Array<number>} */
244
    let bytes = [];
245
    if (this.ds64.chunkId) {
246
      bytes = bytes.concat(
247
        packString(this.ds64.chunkId),
248
        pack(this.ds64.chunkSize, this.uInt32),
249
        pack(this.ds64.riffSizeHigh, this.uInt32),
250
        pack(this.ds64.riffSizeLow, this.uInt32),
251
        pack(this.ds64.dataSizeHigh, this.uInt32),
252
        pack(this.ds64.dataSizeLow, this.uInt32),
253
        pack(this.ds64.originationTime, this.uInt32),
254
        pack(this.ds64.sampleCountHigh, this.uInt32),
255
        pack(this.ds64.sampleCountLow, this.uInt32));
256
    }
257
    //if (this.ds64.tableLength) {
258
    //  ds64Bytes = ds64Bytes.concat(
259
    //    pack(this.ds64.tableLength, this.uInt32),
260
    //    this.ds64.table);
261
    //}
262
    return bytes;
263
  }
264
265
  /**
266
   * Return the bytes of the 'cue ' chunk.
267
   * @return {!Array<number>} The 'cue ' chunk bytes.
268
   * @private
269
   */
270
  getCueBytes_() {
271
    /** @type {!Array<number>} */
272
    let bytes = [];
273
    if (this.cue.chunkId) {
274
      /** @type {!Array<number>} */
275
      let cuePointsBytes = this.getCuePointsBytes_();
276
      bytes = bytes.concat(
277
        packString(this.cue.chunkId),
278
        pack(cuePointsBytes.length + 4, this.uInt32),
279
        pack(this.cue.dwCuePoints, this.uInt32),
280
        cuePointsBytes);
281
    }
282
    return bytes;
283
  }
284
285
  /**
286
   * Return the bytes of the 'cue ' points.
287
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
288
   * @private
289
   */
290
  getCuePointsBytes_() {
291
    /** @type {!Array<number>} */
292
    let points = [];
293
    for (let i=0; i<this.cue.dwCuePoints; i++) {
294
      points = points.concat(
295
        pack(this.cue.points[i].dwName, this.uInt32),
296
        pack(this.cue.points[i].dwPosition, this.uInt32),
297
        packString(this.cue.points[i].fccChunk),
298
        pack(this.cue.points[i].dwChunkStart, this.uInt32),
299
        pack(this.cue.points[i].dwBlockStart, this.uInt32),
300
        pack(this.cue.points[i].dwSampleOffset, this.uInt32));
301
    }
302
    return points;
303
  }
304
305
  /**
306
   * Return the bytes of the 'smpl' chunk.
307
   * @return {!Array<number>} The 'smpl' chunk bytes.
308
   * @private
309
   */
310
  getSmplBytes_() {
311
    /** @type {!Array<number>} */
312
    let bytes = [];
313
    if (this.smpl.chunkId) {
314
      /** @type {!Array<number>} */
315
      let smplLoopsBytes = this.getSmplLoopsBytes_();
316
      bytes = bytes.concat(
317
        packString(this.smpl.chunkId),
318
        pack(smplLoopsBytes.length + 36, this.uInt32),
319
        pack(this.smpl.dwManufacturer, this.uInt32),
320
        pack(this.smpl.dwProduct, this.uInt32),
321
        pack(this.smpl.dwSamplePeriod, this.uInt32),
322
        pack(this.smpl.dwMIDIUnityNote, this.uInt32),
323
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32),
324
        pack(this.smpl.dwSMPTEFormat, this.uInt32),
325
        pack(this.smpl.dwSMPTEOffset, this.uInt32),
326
        pack(this.smpl.dwNumSampleLoops, this.uInt32),
327
        pack(this.smpl.dwSamplerData, this.uInt32),
328
        smplLoopsBytes);
329
    }
330
    return bytes;
331
  }
332
333
  /**
334
   * Return the bytes of the 'smpl' loops.
335
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
336
   * @private
337
   */
338
  getSmplLoopsBytes_() {
339
    /** @type {!Array<number>} */
340
    let loops = [];
341
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
342
      loops = loops.concat(
343
        pack(this.smpl.loops[i].dwName, this.uInt32),
344
        pack(this.smpl.loops[i].dwType, this.uInt32),
345
        pack(this.smpl.loops[i].dwStart, this.uInt32),
346
        pack(this.smpl.loops[i].dwEnd, this.uInt32),
347
        pack(this.smpl.loops[i].dwFraction, this.uInt32),
348
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32));
349
    }
350
    return loops;
351
  }
352
353
  /**
354
   * Return the bytes of the 'fact' chunk.
355
   * @return {!Array<number>} The 'fact' chunk bytes.
356
   * @private
357
   */
358
  getFactBytes_() {
359
    /** @type {!Array<number>} */
360
    let bytes = [];
361
    if (this.fact.chunkId) {
362
      bytes = bytes.concat(
363
        packString(this.fact.chunkId),
364
        pack(this.fact.chunkSize, this.uInt32),
365
        pack(this.fact.dwSampleLength, this.uInt32));
366
    }
367
    return bytes;
368
  }
369
370
  /**
371
   * Return the bytes of the 'fmt ' chunk.
372
   * @return {!Array<number>} The 'fmt' chunk bytes.
373
   * @throws {Error} if no 'fmt ' chunk is present.
374
   * @private
375
   */
376
  getFmtBytes_() {
377
    /** @type {!Array<number>} */
378
    let fmtBytes = [];
379
    if (this.fmt.chunkId) {
380
      return fmtBytes.concat(
381
        packString(this.fmt.chunkId),
382
        pack(this.fmt.chunkSize, this.uInt32),
383
        pack(this.fmt.audioFormat, this.uInt16),
384
        pack(this.fmt.numChannels, this.uInt16),
385
        pack(this.fmt.sampleRate, this.uInt32),
386
        pack(this.fmt.byteRate, this.uInt32),
387
        pack(this.fmt.blockAlign, this.uInt16),
388
        pack(this.fmt.bitsPerSample, this.uInt16),
389
        this.getFmtExtensionBytes_());
390
    }
391
    throw Error('Could not find the "fmt " chunk');
392
  }
393
394
  /**
395
   * Return the bytes of the fmt extension fields.
396
   * @return {!Array<number>} The fmt extension bytes.
397
   * @private
398
   */
399
  getFmtExtensionBytes_() {
400
    /** @type {!Array<number>} */
401
    let extension = [];
402
    if (this.fmt.chunkSize > 16) {
403
      extension = extension.concat(
404
        pack(this.fmt.cbSize, this.uInt16));
405
    }
406
    if (this.fmt.chunkSize > 18) {
407
      extension = extension.concat(
408
        pack(this.fmt.validBitsPerSample, this.uInt16));
409
    }
410
    if (this.fmt.chunkSize > 20) {
411
      extension = extension.concat(
412
        pack(this.fmt.dwChannelMask, this.uInt32));
413
    }
414
    if (this.fmt.chunkSize > 24) {
415
      extension = extension.concat(
416
        pack(this.fmt.subformat[0], this.uInt32),
417
        pack(this.fmt.subformat[1], this.uInt32),
418
        pack(this.fmt.subformat[2], this.uInt32),
419
        pack(this.fmt.subformat[3], this.uInt32));
420
    }
421
    return extension;
422
  }
423
424
  /**
425
   * Return the bytes of the 'LIST' chunk.
426
   * @return {!Array<number>} The 'LIST' chunk bytes.
427
   * @private
428
   */
429
  getLISTBytes_() {
430
    /** @type {!Array<number>} */
431
    let bytes = [];
432
    for (let i=0; i<this.LIST.length; i++) {
433
      /** @type {!Array<number>} */
434
      let subChunksBytes = this.getLISTSubChunksBytes_(
435
          this.LIST[i].subChunks, this.LIST[i].format);
436
      bytes = bytes.concat(
437
        packString(this.LIST[i].chunkId),
438
        pack(subChunksBytes.length + 4, this.uInt32),
439
        packString(this.LIST[i].format),
440
        subChunksBytes);
441
    }
442
    return bytes;
443
  }
444
445
  /**
446
   * Return the bytes of the sub chunks of a 'LIST' chunk.
447
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
448
   * @param {string} format The format of the 'LIST' chunk.
449
   *    Currently supported values are 'adtl' or 'INFO'.
450
   * @return {!Array<number>} The sub chunk bytes.
451
   * @private
452
   */
453
  getLISTSubChunksBytes_(subChunks, format) {
454
    /** @type {!Array<number>} */
455
    let bytes = [];
456
    for (let i=0; i<subChunks.length; i++) {
457
      if (format == 'INFO') {
458
        bytes = bytes.concat(
459
          packString(subChunks[i].chunkId),
460
          pack(subChunks[i].value.length + 1, this.uInt32),
461
          writeString(
462
            subChunks[i].value, subChunks[i].value.length));
463
        bytes.push(0);
464
      } else if (format == 'adtl') {
465
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
466
          bytes = bytes.concat(
467
            packString(subChunks[i].chunkId),
468
            pack(
469
              subChunks[i].value.length + 4 + 1, this.uInt32),
470
            pack(subChunks[i].dwName, this.uInt32),
471
            writeString(
472
              subChunks[i].value,
473
              subChunks[i].value.length));
474
          bytes.push(0);
475
        } else if (subChunks[i].chunkId == 'ltxt') {
476
          bytes = bytes.concat(
477
            this.getLtxtChunkBytes_(subChunks[i]));
478
        }
479
      }
480
      if (bytes.length % 2) {
481
        bytes.push(0);
482
      }
483
    }
484
    return bytes;
485
  }
486
487
  /**
488
   * Return the bytes of a 'ltxt' chunk.
489
   * @param {!Object} ltxt the 'ltxt' chunk.
490
   * @private
491
   */
492
  getLtxtChunkBytes_(ltxt) {
493
    return [].concat(
494
      packString(ltxt.chunkId),
495
      pack(ltxt.value.length + 20, this.uInt32),
496
      pack(ltxt.dwName, this.uInt32),
497
      pack(ltxt.dwSampleLength, this.uInt32),
498
      pack(ltxt.dwPurposeID, this.uInt32),
499
      pack(ltxt.dwCountry, this.uInt16),
500
      pack(ltxt.dwLanguage, this.uInt16),
501
      pack(ltxt.dwDialect, this.uInt16),
502
      pack(ltxt.dwCodePage, this.uInt16),
503
      writeString(ltxt.value, ltxt.value.length));
504
  }
505
506
  /**
507
   * Return the bytes of the 'junk' chunk.
508
   * @private
509
   */
510
  getJunkBytes_() {
511
    /** @type {!Array<number>} */
512
    let bytes = [];
513
    if (this.junk.chunkId) {
514
      return bytes.concat(
515
        packString(this.junk.chunkId),
516
        pack(this.junk.chunkData.length, this.uInt32),
517
        this.junk.chunkData);
518
    }
519
    return bytes;
520
  }
521
522
  /**
523
   * Validate the bit depth.
524
   * @return {boolean} True is the bit depth is valid.
525
   * @throws {Error} If bit depth is invalid.
526
   * @private
527
   */
528
  validateBitDepth_() {
529
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
530
      if (parseInt(this.bitDepth, 10) > 8 &&
531
          parseInt(this.bitDepth, 10) < 54) {
532
        return true;
533
      }
534
      throw new Error('Invalid bit depth.');
535
    }
536
    return true;
537
  }
538
}
539