Completed
Push — master ( 317dbe...83670a )
by Rafael S.
03:21
created

lib/wavefile-parser.js   B

Complexity

Total Complexity 52
Complexity/F 2.6

Size

Lines of Code 470
Function Count 20

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 52
eloc 258
mnd 32
bc 32
fnc 20
dl 0
loc 470
rs 7.44
bpm 1.6
cpm 2.6
noi 0
c 0
b 0
f 0

20 Functions

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