Passed
Push — master ( 29f5b0...4c43c3 )
by Rafael S.
02:58
created

WaveFileCueEditor.clearLISTadtl_   A

Complexity

Conditions 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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