Passed
Push — master ( 471f05...29aa6e )
by Rafael S.
02:48
created

WaveFileMetaEditor   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 525
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 73
eloc 206
dl 0
loc 525
rs 2.56
c 0
b 0
f 0

18 Functions

Rating   Name   Duplication   Size   Complexity  
A getTag 0 8 2
A deleteTag 0 9 2
A listTags 0 13 3
A setTag 0 25 3
A setCuePoint_ 0 11 5
A deleteCuePoint 0 21 4
A getCuePoints_ 0 18 3
B setLtxtChunk_ 0 15 6
A listCuePoints 0 25 3
A setLabelText_ 0 9 1
A getLISTINFOIndex_ 0 11 3
A updateLabel 0 12 4
A setLabl_ 0 23 3
A getTagIndex_ 0 17 5
B setCuePoint 0 93 8
A getAdtlChunk_ 0 8 3
D getDataForCuePoint_ 0 34 12
A clearLISTadtl_ 0 7 3

How to fix   Complexity   

Complexity

Complex classes like WaveFileMetaEditor 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 WaveFileMetaEditor class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileCreator } from './wavefile-creator';
31
import fixRIFFTag from './fix-riff-tag';
32
33
/**
34
 * A class to edit meta information in wav files.
35
 * @extends WaveFileCreator
36
 * @ignore
37
 */
38
export class WaveFileMetaEditor extends WaveFileCreator {
39
40
  /**
41
   * Return the value of a RIFF tag in the INFO chunk.
42
   * @param {string} tag The tag name.
43
   * @return {?string} The value if the tag is found, null otherwise.
44
   */
45
  getTag(tag) {
46
    /** @type {!Object} */
47
    let index = this.getTagIndex_(tag);
48
    if (index.TAG !== null) {
49
      return this.LIST[index.LIST].subChunks[index.TAG].value;
50
    }
51
    return null;
52
  }
53
54
  /**
55
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
56
   * then it is created. It if exists, it is overwritten.
57
   * @param {string} tag The tag name.
58
   * @param {string} value The tag value.
59
   * @throws {Error} If the tag name is not valid.
60
   */
61
  setTag(tag, value) {
62
    tag = fixRIFFTag(tag);
63
    /** @type {!Object} */
64
    let index = this.getTagIndex_(tag);
65
    if (index.TAG !== null) {
66
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
67
        value.length + 1;
68
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
69
    } else if (index.LIST !== null) {
70
      this.LIST[index.LIST].subChunks.push({
71
        chunkId: tag,
72
        chunkSize: value.length + 1,
73
        value: value});
74
    } else {
75
      this.LIST.push({
76
        chunkId: 'LIST',
77
        chunkSize: 8 + value.length + 1,
78
        format: 'INFO',
79
        subChunks: []});
80
      this.LIST[this.LIST.length - 1].subChunks.push({
81
        chunkId: tag,
82
        chunkSize: value.length + 1,
83
        value: value});
84
    }
85
  }
86
87
  /**
88
   * Remove a RIFF tag from the INFO chunk.
89
   * @param {string} tag The tag name.
90
   * @return {boolean} True if a tag was deleted.
91
   */
92
  deleteTag(tag) {
93
    /** @type {!Object} */
94
    let index = this.getTagIndex_(tag);
95
    if (index.TAG !== null) {
96
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
97
      return true;
98
    }
99
    return false;
100
  }
101
102
  /**
103
   * Return a Object<tag, value> with the RIFF tags in the file.
104
   * @return {!Object<string, string>} The file tags.
105
   */
106
  listTags() {
107
    /** @type {?number} */
108
    let index = this.getLISTINFOIndex_();
109
    /** @type {!Object} */
110
    let tags = {};
111
    if (index !== null) {
112
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
113
        tags[this.LIST[index].subChunks[i].chunkId] =
114
          this.LIST[index].subChunks[i].value;
115
      }
116
    }
117
    return tags;
118
  }
119
120
  /**
121
   * Return an array with all cue points in the file, in the order they appear
122
   * in the file.
123
   * Objects representing cue points/regions look like this:
124
   *   {
125
   *     position: 500, // the position in milliseconds
126
   *     label: 'cue marker 1',
127
   *     end: 1500, // the end position in milliseconds
128
   *     dwName: 1,
129
   *     dwPosition: 0,
130
   *     fccChunk: 'data',
131
   *     dwChunkStart: 0,
132
   *     dwBlockStart: 0,
133
   *     dwSampleOffset: 22050, // the position as a sample offset
134
   *     dwSampleLength: 3646827, // length as a sample count, 0 if not a region
135
   *     dwPurposeID: 544106354,
136
   *     dwCountry: 0,
137
   *     dwLanguage: 0,
138
   *     dwDialect: 0,
139
   *     dwCodePage: 0,
140
   *   }
141
   * @return {!Array<Object>}
142
   */
143
  listCuePoints() {
144
    /** @type {!Array<!Object>} */
145
    let points = this.getCuePoints_();
146
    for (let i = 0, len = points.length; i < len; i++) {
147
148
      // Add attrs that should exist in the object
149
      points[i].position =
150
        (points[i].dwSampleOffset / this.fmt.sampleRate) * 1000;
151
152
      // If it is a region, calc the end
153
      // position in milliseconds
154
      if (points[i].dwSampleLength) {
155
        points[i].end =
156
          (points[i].dwSampleLength / this.fmt.sampleRate) * 1000;
157
        points[i].end += points[i].position;
158
      // If its not a region, end should be null
159
      } else {
160
        points[i].end = null;
161
      }
162
163
      // Remove attrs that should not go in the results
164
      delete points[i].value;
165
    }
166
    return points;
167
  }
168
169
  /**
170
   * Create a cue point in the wave file.
171
   * @param {!{
172
   *   position: number,
173
   *   label: ?string,
174
   *   end: ?number,
175
   *   dwPurposeID: ?number,
176
   *   dwCountry: ?number,
177
   *   dwLanguage: ?number,
178
   *   dwDialect: ?number,
179
   *   dwCodePage: ?number
180
   * }} pointData A object with the data of the cue point.
181
   *
182
   * # Only required attribute to create a cue point:
183
   * pointData.position: The position of the point in milliseconds
184
   *
185
   * # Optional attribute for cue points:
186
   * pointData.label: A string label for the cue point
187
   *
188
   * # Extra data used for regions
189
   * pointData.end: A number representing the end of the region,
190
   *   in milliseconds, counting from the start of the file. If
191
   *   no end attr is specified then no region is created.
192
   *
193
   * # You may also specify the following attrs for regions, all optional:
194
   * pointData.dwPurposeID
195
   * pointData.dwCountry
196
   * pointData.dwLanguage
197
   * pointData.dwDialect
198
   * pointData.dwCodePage
199
   */
200
  setCuePoint(pointData) {
201
    this.cue.chunkId = 'cue ';
202
203
    // label attr should always exist
204
    if (!pointData.label) {
205
      pointData.label = '';
206
    }
207
208
    /**
209
     * Load the existing points before erasing
210
     * the LIST 'adtl' chunk and the cue attr
211
     * @type {!Array<!Object>}
212
     */
213
    let existingPoints = this.getCuePoints_();
214
215
    // Clear any LIST labeled 'adtl'
216
    // The LIST chunk should be re-written
217
    // after the new cue point is created
218
    this.clearLISTadtl_();
219
220
    /**
221
     * Get the current len of the cue points
222
     * in the file before erasing the cue attr
223
     * @type {number}
224
     */
225
    let len = this.cue.points.length;
226
227
    // Erase this.cue so it can be re-written
228
    // after the point is added
229
    this.cue.points = [];
230
231
    /**
232
     * Cue position param is informed in milliseconds,
233
     * here its value is converted to the sample offset
234
     * @type {number}
235
     */
236
    pointData.dwSampleOffset =
237
      (pointData.position * this.fmt.sampleRate) / 1000;
238
    /**
239
     * end param is informed in milliseconds, counting
240
     * from the start of the file.
241
     * here its value is converted to the sample length
242
     * of the region.
243
     * @type {number}
244
     */
245
    pointData.dwSampleLength = 0;
246
    if (pointData.end) {
247
      pointData.dwSampleLength = 
248
        ((pointData.end * this.fmt.sampleRate) / 1000) -
249
        pointData.dwSampleOffset;
250
    }
251
252
    // If there were no cue points in the file,
253
    // insert the new cue point as the first
254
    if (len === 0) {
255
      this.setCuePoint_(pointData, 1);
256
257
    // If the file already had cue points, This new one
258
    // must be added in the list according to its position.
259
    } else {
260
261
      /** @type {boolean} */
262
      let hasSet = false;
263
264
      // Iterate over the cue points that existed
265
      // before this one was added
266
      for (let i = 0; i < len; i++) {
267
268
        // If the new point is located before this original point
269
        // and the new point have not been created, create the
270
        // new point and then the original point
271
        if (existingPoints[i].dwSampleOffset > 
272
          pointData.dwSampleOffset && !hasSet) {
273
          // create the new point
274
          this.setCuePoint_(pointData, i + 1);
275
276
          // create the original point
277
          this.setCuePoint_(existingPoints[i], i + 2);
278
          hasSet = true;
279
280
        // Otherwise, re-create the original point
281
        } else {
282
          this.setCuePoint_(existingPoints[i], hasSet ? i + 2 : i + 1);
283
        }
284
      }
285
      // If no point was created in the above loop,
286
      // create the new point as the last one
287
      if (!hasSet) {
288
        this.setCuePoint_(pointData, this.cue.points.length + 1);
289
      }
290
    }
291
    this.cue.dwCuePoints = this.cue.points.length;
292
  }
293
294
  /**
295
   * Remove a cue point from a wave file.
296
   * @param {number} index the index of the point. First is 1,
297
   *    second is 2, and so on.
298
   */
299
  deleteCuePoint(index) {
300
    this.cue.chunkId = 'cue ';
301
    /** @type {!Array<!Object>} */
302
    let existingPoints = this.getCuePoints_();
303
    this.clearLISTadtl_();
304
    /** @type {number} */
305
    let len = this.cue.points.length;
306
    this.cue.points = [];
307
    for (let i = 0; i < len; i++) {
308
      if (i + 1 !== index) {
309
        this.setCuePoint_(existingPoints[i], i + 1);
310
      }
311
    }
312
    this.cue.dwCuePoints = this.cue.points.length;
313
    if (this.cue.dwCuePoints) {
314
      this.cue.chunkId = 'cue ';
315
    } else {
316
      this.cue.chunkId = '';
317
      this.clearLISTadtl_();
318
    }
319
  }
320
321
  /**
322
   * Update the label of a cue point.
323
   * @param {number} pointIndex The ID of the cue point.
324
   * @param {string} label The new text for the label.
325
   */
326
  updateLabel(pointIndex, label) {
327
    /** @type {?number} */
328
    let cIndex = this.getAdtlChunk_();
329
    if (cIndex !== null) {
330
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
331
        if (this.LIST[cIndex].subChunks[i].dwName ==
332
            pointIndex) {
333
          this.LIST[cIndex].subChunks[i].value = label;
334
        }
335
      }
336
    }
337
  }
338
339
  /**
340
   * Return an array with all cue points in the file, in the order they appear
341
   * in the file.
342
   * @return {!Array<!Object>}
343
   * @private
344
   */
345
  getCuePoints_() {
346
    /** @type {!Array<!Object>} */
347
    let points = [];
348
    for (let i = 0; i < this.cue.points.length; i++) {
349
      /** @type {!Object} */
350
      let chunk = this.cue.points[i];
351
      /** @type {!Object} */
352
      let pointData = this.getDataForCuePoint_(chunk.dwName);
353
      pointData.label = pointData.value ? pointData.value : '';
354
      pointData.dwPosition = chunk.dwPosition;
355
      pointData.fccChunk = chunk.fccChunk;
356
      pointData.dwChunkStart = chunk.dwChunkStart;
357
      pointData.dwBlockStart = chunk.dwBlockStart;
358
      pointData.dwSampleOffset = chunk.dwSampleOffset;
359
      points.push(pointData);
360
    }
361
    return points;
362
  }
363
364
  /**
365
   * Return the associated data of a cue point.
366
   * @param {number} pointDwName The ID of the cue point.
367
   * @return {!Object}
368
   * @private
369
   */
370
  getDataForCuePoint_(pointDwName) {
371
    /** @type {?number} */
372
    let cIndex = this.getAdtlChunk_();
373
    /** @type {!Object} */
374
    let pointData = {};
375
    if (cIndex !== null) {
376
      // got through all chunks in the adtl LIST checking
377
      // for references to this cue point
378
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
379
        if (this.LIST[cIndex].subChunks[i].dwName ==
380
            pointDwName && this.LIST[cIndex].subChunks[i].chunkId) {
381
          /** @type {!Object} */
382
          let chunk = this.LIST[cIndex].subChunks[i];
383
          // Some chunks may reference the point but
384
          // have a empty text; this is to ensure that if
385
          // one chunk that reference the point has a text,
386
          // this value will be kept as the associated data label
387
          // for the cue point.
388
          // If different values are present, the last value found
389
          // will be considered the label for the cue point.
390
          pointData.value = chunk.value ? chunk.value : pointData.value;
391
          pointData.dwName = chunk.dwName ? chunk.dwName : 0;
392
          pointData.dwSampleLength =
393
            chunk.dwSampleLength ? chunk.dwSampleLength : 0;
394
          pointData.dwPurposeID = chunk.dwPurposeID ? chunk.dwPurposeID : 0;
395
          pointData.dwCountry = chunk.dwCountry ? chunk.dwCountry : 0;
396
          pointData.dwLanguage = chunk.dwLanguage ? chunk.dwLanguage : 0;
397
          pointData.dwDialect = chunk.dwDialect ? chunk.dwDialect : 0;
398
          pointData.dwCodePage = chunk.dwCodePage ? chunk.dwCodePage : 0;
399
        }
400
      }
401
    }
402
    return pointData;
403
  }
404
405
  /**
406
   * Return the index of the INFO chunk in the LIST chunk.
407
   * @return {?number} the index of the INFO chunk.
408
   * @private
409
   */
410
  getLISTINFOIndex_() {
411
    /** @type {?number} */
412
    let index = null;
413
    for (let i = 0, len = this.LIST.length; i < len; i++) {
414
      if (this.LIST[i].format === 'INFO') {
415
        index = i;
416
        break;
417
      }
418
    }
419
    return index;
420
  }
421
422
  /**
423
   * Return the index of the 'adtl' LIST in this.LIST.
424
   * @return {?number}
425
   * @private
426
   */
427
  getAdtlChunk_() {
428
    for (let i = 0, len = this.LIST.length; i < len; i++) {
429
      if (this.LIST[i].format == 'adtl') {
430
        return i;
431
      }
432
    }
433
    return null;
434
  }
435
436
  /**
437
   * Return the index of a tag in a FILE chunk.
438
   * @param {string} tag The tag name.
439
   * @return {!Object<string, ?number>}
440
   *    Object.LIST is the INFO index in LIST
441
   *    Object.TAG is the tag index in the INFO
442
   * @private
443
   */
444
  getTagIndex_(tag) {
445
    /** @type {!Object<string, ?number>} */
446
    let index = {LIST: null, TAG: null};
447
    for (let i = 0, len = this.LIST.length; i < len; i++) {
448
      if (this.LIST[i].format == 'INFO') {
449
        index.LIST = i;
450
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
451
          if (this.LIST[i].subChunks[j].chunkId == tag) {
452
            index.TAG = j;
453
            break;
454
          }
455
        }
456
        break;
457
      }
458
    }
459
    return index;
460
  }
461
462
  /**
463
   * Push a new cue point in this.cue.points.
464
   * @param {!Object} pointData A object with data of the cue point.
465
   * @param {number} dwName the dwName of the cue point
466
   * @private
467
   */
468
  setCuePoint_(pointData, dwName) {
469
    this.cue.points.push({
470
      dwName: dwName,
471
      dwPosition: pointData.dwPosition ? pointData.dwPosition : 0,
472
      fccChunk: pointData.fccChunk ? pointData.fccChunk : 'data',
473
      dwChunkStart: pointData.dwChunkStart ? pointData.dwChunkStart : 0,
474
      dwBlockStart: pointData.dwBlockStart ? pointData.dwBlockStart : 0,
475
      dwSampleOffset: pointData.dwSampleOffset
476
    });
477
    this.setLabl_(pointData, dwName);
478
  }
479
480
  /**
481
   * Clear any LIST chunk labeled as 'adtl'.
482
   * @private
483
   */
484
  clearLISTadtl_() {
485
    for (let i = 0, len = this.LIST.length; i < len; i++) {
486
      if (this.LIST[i].format == 'adtl') {
487
        this.LIST.splice(i);
488
      }
489
    }
490
  }
491
492
  /**
493
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
494
   * This method creates a LIST adtl chunk in the file if one
495
   * is not present.
496
   * @param {!Object} pointData A object with data of the cue point.
497
   * @param {number} dwName The ID of the cue point.
498
   * @private
499
   */
500
  setLabl_(pointData, dwName) {
501
    /**
502
     * Get the index of the LIST chunk labeled as adtl.
503
     * A file can have many LIST chunks with unique labels.
504
     * @type {?number}
505
     */
506
    let adtlIndex = this.getAdtlChunk_();
507
    // If there is no adtl LIST, create one
508
    if (adtlIndex === null) {
509
      // Include a new item LIST chunk
510
      this.LIST.push({
511
        chunkId: 'LIST',
512
        chunkSize: 4,
513
        format: 'adtl',
514
        subChunks: []});
515
      // Get the index of the new LIST chunk
516
      adtlIndex = this.LIST.length - 1;
517
    }
518
    this.setLabelText_(adtlIndex, pointData, dwName);
519
    if (pointData.dwSampleLength) {
520
      this.setLtxtChunk_(adtlIndex, pointData, dwName);
521
    }
522
  }
523
524
  /**
525
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
526
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
527
   * @param {!Object} pointData A object with data of the cue point.
528
   * @param {number} dwName The ID of the cue point.
529
   * @private
530
   */
531
  setLabelText_(adtlIndex, pointData, dwName) {
532
    this.LIST[adtlIndex].subChunks.push({
533
      chunkId: 'labl',
534
      chunkSize: 4, // should be 4 + label length in bytes
535
      dwName: dwName,
536
      value: pointData.label
537
    });
538
    this.LIST[adtlIndex].chunkSize += 12; // should be 4 + label byte length
539
  }
540
  /**
541
   * Create a new 'ltxt' subchunk in a 'LIST' chunk of type 'adtl'.
542
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
543
   * @param {!Object} pointData A object with data of the cue point.
544
   * @param {number} dwName The ID of the cue point.
545
   * @private
546
   */
547
  setLtxtChunk_(adtlIndex, pointData, dwName) {
548
    this.LIST[adtlIndex].subChunks.push({
549
      chunkId: 'ltxt',
550
      chunkSize: 20,  // should be 12 + label byte length
551
      dwName: dwName,
552
      dwSampleLength: pointData.dwSampleLength,
553
      dwPurposeID: pointData.dwPurposeID ? pointData.dwPurposeID : 0,
554
      dwCountry: pointData.dwCountry ? pointData.dwCountry : 0,
555
      dwLanguage: pointData.dwLanguage ? pointData.dwLanguage : 0,
556
      dwDialect: pointData.dwDialect ? pointData.dwDialect : 0,
557
      dwCodePage: pointData.dwCodePage ? pointData.dwCodePage : 0,
558
      value: pointData.label // kept for compatibility
559
    });
560
    this.LIST[adtlIndex].chunkSize += 28;
561
  }
562
}
563