Passed
Push — master ( 6ca9b2...ebd43e )
by Rafael S.
04:26
created

WaveFileMetaEditor.getLabelForCuePoint_   A

Complexity

Conditions 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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