Completed
Push — master ( 084255...8f04c7 )
by Rafael S.
02:42
created

WaveFileParser.getLISTSubChunksBytes_   A

Complexity

Conditions 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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