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

WaveFile   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 390
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 54
eloc 178
dl 0
loc 390
rs 6.4799
c 0
b 0
f 0

21 Functions

Rating   Name   Duplication   Size   Complexity  
A setCuePoint_ 0 11 1
A getCuePoints_ 0 11 2
A getTagIndex_ 0 17 5
A getLabelForCuePoint_ 0 13 4
A fromBase64 0 3 1
A fromDataURI 0 3 1
A updateLabel 0 12 4
A listTags 0 13 3
B setCuePoint 0 35 5
A deleteCuePoint 0 24 4
A toDataURI 0 3 1
A getLISTINFOIndex_ 0 11 3
A toBase64 0 5 1
A setLabl_ 0 13 3
A setLabelText_ 0 9 1
A clearLISTadtl_ 0 7 3
A getTag 0 8 2
A listCuePoints 0 9 2
A getAdtlChunk_ 0 8 3
A deleteTag 0 9 2
A setTag 0 25 3

How to fix   Complexity   

Complexity

Complex classes like WaveFile 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 WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import {encode, decode} from 'base64-arraybuffer-es6';
33
import WaveFileConverter from './lib/wavefile-converter';
34
import fixRIFFTag from './lib/fix-riff-tag';
35
36
/**
37
 * A class to manipulate wav files.
38
 * @extends WaveFileConverter
39
 */
40
export default class WaveFile extends WaveFileConverter {
41
42
  /**
43
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
44
   * @param {string} base64String A .wav file as a base64 string.
45
   * @throws {Error} If any property of the object appears invalid.
46
   */
47
  fromBase64(base64String) {
48
    this.fromBuffer(new Uint8Array(decode(base64String)));
49
  }
50
51
  /**
52
   * Return a base64 string representig the WaveFile object as a .wav file.
53
   * @return {string} A .wav file as a base64 string.
54
   * @throws {Error} If any property of the object appears invalid.
55
   */
56
  toBase64() {
57
    /** @type {!Uint8Array} */
58
    let buffer = this.toBuffer();
59
    return encode(buffer, 0, buffer.length);
60
  }
61
62
  /**
63
   * Return a DataURI string representig the WaveFile object as a .wav file.
64
   * The return of this method can be used to load the audio in browsers.
65
   * @return {string} A .wav file as a DataURI.
66
   * @throws {Error} If any property of the object appears invalid.
67
   */
68
  toDataURI() {
69
    return 'data:audio/wav;base64,' + this.toBase64();
70
  }
71
72
  /**
73
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
74
   * @param {string} dataURI A .wav file as DataURI.
75
   * @throws {Error} If any property of the object appears invalid.
76
   */
77
  fromDataURI(dataURI) {
78
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
79
  }
80
81
  /**
82
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
83
   * then it is created. It if exists, it is overwritten.
84
   * @param {string} tag The tag name.
85
   * @param {string} value The tag value.
86
   * @throws {Error} If the tag name is not valid.
87
   */
88
  setTag(tag, value) {
89
    tag = fixRIFFTag(tag);
90
    /** @type {!Object} */
91
    let index = this.getTagIndex_(tag);
92
    if (index.TAG !== null) {
93
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
94
        value.length + 1;
95
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
96
    } else if (index.LIST !== null) {
97
      this.LIST[index.LIST].subChunks.push({
98
        chunkId: tag,
99
        chunkSize: value.length + 1,
100
        value: value});
101
    } else {
102
      this.LIST.push({
103
        chunkId: 'LIST',
104
        chunkSize: 8 + value.length + 1,
105
        format: 'INFO',
106
        subChunks: []});
107
      this.LIST[this.LIST.length - 1].subChunks.push({
108
        chunkId: tag,
109
        chunkSize: value.length + 1,
110
        value: value});
111
    }
112
  }
113
114
  /**
115
   * Return the value of a RIFF tag in the INFO chunk.
116
   * @param {string} tag The tag name.
117
   * @return {?string} The value if the tag is found, null otherwise.
118
   */
119
  getTag(tag) {
120
    /** @type {!Object} */
121
    let index = this.getTagIndex_(tag);
122
    if (index.TAG !== null) {
123
      return this.LIST[index.LIST].subChunks[index.TAG].value;
124
    }
125
    return null;
126
  }
127
128
  /**
129
   * Return a Object<tag, value> with the RIFF tags in the file.
130
   * @return {!Object<string, string>} The file tags.
131
   */
132
  listTags() {
133
    /** @type {?number} */
134
    let index = this.getLISTINFOIndex_();
135
    /** @type {!Object} */
136
    let tags = {};
137
    if (index !== null) {
138
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
139
        tags[this.LIST[index].subChunks[i].chunkId] =
140
          this.LIST[index].subChunks[i].value;
141
      }
142
    }
143
    return tags;
144
  }
145
146
  /**
147
   * Remove a RIFF tag from the INFO chunk.
148
   * @param {string} tag The tag name.
149
   * @return {boolean} True if a tag was deleted.
150
   */
151
  deleteTag(tag) {
152
    /** @type {!Object} */
153
    let index = this.getTagIndex_(tag);
154
    if (index.TAG !== null) {
155
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
156
      return true;
157
    }
158
    return false;
159
  }
160
161
  /**
162
   * Create a cue point in the wave file.
163
   * @param {number} position The cue point position in milliseconds.
164
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
165
   */
166
  setCuePoint(position, labl='') {
167
    this.cue.chunkId = 'cue ';
168
    position = (position * this.fmt.sampleRate) / 1000;
169
    /** @type {!Array<!Object>} */
170
    let existingPoints = this.getCuePoints_();
171
    this.clearLISTadtl_();
172
    /** @type {number} */
173
    let len = this.cue.points.length;
174
    this.cue.points = [];
175
    /** @type {boolean} */
176
    let hasSet = false;
177
    if (len === 0) {
178
      this.setCuePoint_(position, 1, labl);
179
    } else {
180
      for (let i = 0; i < len; i++) {
181
        if (existingPoints[i].dwPosition > position && !hasSet) {
182
          this.setCuePoint_(position, i + 1, labl);
183
          this.setCuePoint_(
184
            existingPoints[i].dwPosition,
185
            i + 2,
186
            existingPoints[i].label);
187
          hasSet = true;
188
        } else {
189
          this.setCuePoint_(
190
            existingPoints[i].dwPosition,
191
            i + 1,
192
            existingPoints[i].label);
193
        }
194
      }
195
      if (!hasSet) {
196
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
197
      }
198
    }
199
    this.cue.dwCuePoints = this.cue.points.length;
200
  }
201
202
  /**
203
   * Remove a cue point from a wave file.
204
   * @param {number} index the index of the point. First is 1,
205
   *    second is 2, and so on.
206
   */
207
  deleteCuePoint(index) {
208
    this.cue.chunkId = 'cue ';
209
    /** @type {!Array<!Object>} */
210
    let existingPoints = this.getCuePoints_();
211
    this.clearLISTadtl_();
212
    /** @type {number} */
213
    let len = this.cue.points.length;
214
    this.cue.points = [];
215
    for (let i = 0; i < len; i++) {
216
      if (i + 1 !== index) {
217
        this.setCuePoint_(
218
          existingPoints[i].dwPosition,
219
          i + 1,
220
          existingPoints[i].label);
221
      }
222
    }
223
    this.cue.dwCuePoints = this.cue.points.length;
224
    if (this.cue.dwCuePoints) {
225
      this.cue.chunkId = 'cue ';
226
    } else {
227
      this.cue.chunkId = '';
228
      this.clearLISTadtl_();
229
    }
230
  }
231
232
  /**
233
   * Return an array with all cue points in the file, in the order they appear
234
   * in the file.
235
   * The difference between this method and using the list in WaveFile.cue
236
   * is that the return value of this method includes the position in
237
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
238
   * @return {!Array<!Object>}
239
   */
240
  listCuePoints() {
241
    /** @type {!Array<!Object>} */
242
    let points = this.getCuePoints_();
243
    for (let i = 0, len = points.length; i < len; i++) {
244
      points[i].milliseconds =
245
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
246
    }
247
    return points;
248
  }
249
250
  /**
251
   * Update the label of a cue point.
252
   * @param {number} pointIndex The ID of the cue point.
253
   * @param {string} label The new text for the label.
254
   */
255
  updateLabel(pointIndex, label) {
256
    /** @type {?number} */
257
    let cIndex = this.getAdtlChunk_();
258
    if (cIndex !== null) {
259
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
260
        if (this.LIST[cIndex].subChunks[i].dwName ==
261
            pointIndex) {
262
          this.LIST[cIndex].subChunks[i].value = label;
263
        }
264
      }
265
    }
266
  }
267
268
  /**
269
   * Push a new cue point in this.cue.points.
270
   * @param {number} position The position in milliseconds.
271
   * @param {number} dwName the dwName of the cue point
272
   * @private
273
   */
274
  setCuePoint_(position, dwName, label) {
275
    this.cue.points.push({
276
      dwName: dwName,
277
      dwPosition: position,
278
      fccChunk: 'data',
279
      dwChunkStart: 0,
280
      dwBlockStart: 0,
281
      dwSampleOffset: position,
282
    });
283
    this.setLabl_(dwName, label);
284
  }
285
286
  /**
287
   * Return an array with all cue points in the file, in the order they appear
288
   * in the file.
289
   * @return {!Array<!Object>}
290
   * @private
291
   */
292
  getCuePoints_() {
293
    /** @type {!Array<!Object>} */
294
    let points = [];
295
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
296
      points.push({
297
        dwPosition: this.cue.points[i].dwPosition,
298
        label: this.getLabelForCuePoint_(
299
          this.cue.points[i].dwName)});
300
    }
301
    return points;
302
  }
303
304
  /**
305
   * Return the label of a cue point.
306
   * @param {number} pointDwName The ID of the cue point.
307
   * @return {string}
308
   * @private
309
   */
310
  getLabelForCuePoint_(pointDwName) {
311
    /** @type {?number} */
312
    let cIndex = this.getAdtlChunk_();
313
    if (cIndex !== null) {
314
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
315
        if (this.LIST[cIndex].subChunks[i].dwName ==
316
            pointDwName) {
317
          return this.LIST[cIndex].subChunks[i].value;
318
        }
319
      }
320
    }
321
    return '';
322
  }
323
324
  /**
325
   * Clear any LIST chunk labeled as 'adtl'.
326
   * @private
327
   */
328
  clearLISTadtl_() {
329
    for (let i = 0, len = this.LIST.length; i < len; i++) {
330
      if (this.LIST[i].format == 'adtl') {
331
        this.LIST.splice(i);
332
      }
333
    }
334
  }
335
336
  /**
337
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
338
   * @param {number} dwName The ID of the cue point.
339
   * @param {string} label The label for the cue point.
340
   * @private
341
   */
342
  setLabl_(dwName, label) {
343
    /** @type {?number} */
344
    let adtlIndex = this.getAdtlChunk_();
345
    if (adtlIndex === null) {
346
      this.LIST.push({
347
        chunkId: 'LIST',
348
        chunkSize: 4,
349
        format: 'adtl',
350
        subChunks: []});
351
      adtlIndex = this.LIST.length - 1;
352
    }
353
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
354
  }
355
356
  /**
357
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
358
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
359
   * @param {number} dwName The ID of the cue point.
360
   * @param {string} label The label for the cue point.
361
   * @private
362
   */
363
  setLabelText_(adtlIndex, dwName, label) {
364
    this.LIST[adtlIndex].subChunks.push({
365
      chunkId: 'labl',
366
      chunkSize: label.length,
367
      dwName: dwName,
368
      value: label
369
    });
370
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
371
  }
372
373
  /**
374
   * Return the index of the 'adtl' LIST in this.LIST.
375
   * @return {?number}
376
   * @private
377
   */
378
  getAdtlChunk_() {
379
    for (let i = 0, len = this.LIST.length; i < len; i++) {
380
      if (this.LIST[i].format == 'adtl') {
381
        return i;
382
      }
383
    }
384
    return null;
385
  }
386
387
  /**
388
   * Return the index of the INFO chunk in the LIST chunk.
389
   * @return {?number} the index of the INFO chunk.
390
   * @private
391
   */
392
  getLISTINFOIndex_() {
393
    /** @type {?number} */
394
    let index = null;
395
    for (let i = 0, len = this.LIST.length; i < len; i++) {
396
      if (this.LIST[i].format === 'INFO') {
397
        index = i;
398
        break;
399
      }
400
    }
401
    return index;
402
  }
403
404
  /**
405
   * Return the index of a tag in a FILE chunk.
406
   * @param {string} tag The tag name.
407
   * @return {!Object<string, ?number>}
408
   *    Object.LIST is the INFO index in LIST
409
   *    Object.TAG is the tag index in the INFO
410
   * @private
411
   */
412
  getTagIndex_(tag) {
413
    /** @type {!Object<string, ?number>} */
414
    let index = {LIST: null, TAG: null};
415
    for (let i = 0, len = this.LIST.length; i < len; i++) {
416
      if (this.LIST[i].format == 'INFO') {
417
        index.LIST = i;
418
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
419
          if (this.LIST[i].subChunks[j].chunkId == tag) {
420
            index.TAG = j;
421
            break;
422
          }
423
        }
424
        break;
425
      }
426
    }
427
    return index;
428
  }
429
}
430