Passed
Push — master ( 29aa6e...c898d2 )
by Rafael S.
02:29
created

WaveFileCreator.getSamples   A

Complexity

Conditions 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 15
rs 10
c 0
b 0
f 0
cc 2
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 WaveFileCreator class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileParser } from './wavefile-parser';
31
import { interleave, deInterleave } from './interleave';
32
import dwChannelMask from './dw-channel-mask';
33
import validateNumChannels from './validate-num-channels'; 
34
import validateSampleRate from './validate-sample-rate';
35
import { packArrayTo, unpackArrayTo, packTo, unpack } from 'byte-data';
36
37
/**
38
 * A class to read, write and create wav files.
39
 * @extends WaveFileParser
40
 * @ignore
41
 */
42
export class WaveFileCreator extends WaveFileParser {
43
44
  constructor() {
45
    super();
46
    /**
47
     * The bit depth code according to the samples.
48
     * @type {string}
49
     */
50
    this.bitDepth = '0';
51
    /**
52
     * @type {{be: boolean, bits: number, fp: boolean, signed: boolean}}
53
     * @protected
54
     */
55
    this.dataType = {bits: 0, be: false, signed: false, fp: false};
56
    /**
57
     * Audio formats.
58
     * Formats not listed here should be set to 65534,
59
     * the code for WAVE_FORMAT_EXTENSIBLE
60
     * @enum {number}
61
     * @protected
62
     */
63
    this.WAV_AUDIO_FORMATS = {
64
      '4': 17,
65
      '8': 1,
66
      '8a': 6,
67
      '8m': 7,
68
      '16': 1,
69
      '24': 1,
70
      '32': 1,
71
      '32f': 3,
72
      '64': 3
73
    };
74
  }
75
76
  /**
77
   * Set up the WaveFileCreator object based on the arguments passed.
78
   * Existing chunks are reset.
79
   * @param {number} numChannels The number of channels.
80
   * @param {number} sampleRate The sample rate.
81
   *    Integers like 8000, 44100, 48000, 96000, 192000.
82
   * @param {string} bitDepthCode The audio bit depth code.
83
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
84
   *    or any value between '8' and '32' (like '12').
85
   * @param {
86
   *      !Array<number> |
87
   *      !Array<Array<number>> |
88
   *      !TypedArray |
89
   *      !Array<TypedArray>
90
   *    } samples The samples.
91
   * @param {?Object} options Optional. Used to force the container
92
   *    as RIFX with {'container': 'RIFX'}
93
   * @throws {Error} If any argument does not meet the criteria.
94
   */
95
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
96
    // reset all chunks
97
    this.clearHeaders();
98
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
99
  }
100
101
  /**
102
   * Set up the WaveFileParser object from a byte buffer.
103
   * @param {!Uint8Array} wavBuffer The buffer.
104
   * @param {boolean=} samples True if the samples should be loaded.
105
   * @throws {Error} If container is not RIFF, RIFX or RF64.
106
   * @throws {Error} If format is not WAVE.
107
   * @throws {Error} If no 'fmt ' chunk is found.
108
   * @throws {Error} If no 'data' chunk is found.
109
   */
110
  fromBuffer(wavBuffer, samples=true) {
111
    super.fromBuffer(wavBuffer, samples);
112
    this.bitDepthFromFmt_();
113
    this.updateDataType_();
114
  }
115
116
  /**
117
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
118
   * The return value of this method can be written straight to disk.
119
   * @return {!Uint8Array} A wav file.
120
   * @throws {Error} If bit depth is invalid.
121
   * @throws {Error} If the number of channels is invalid.
122
   * @throws {Error} If the sample rate is invalid.
123
   */
124
  toBuffer() {
125
    this.validateWavHeader_();
126
    return super.toBuffer();
127
  }
128
129
  /**
130
   * Return the samples packed in a Float64Array.
131
   * @param {?boolean} interleaved True to return interleaved samples,
132
   *   false to return the samples de-interleaved. Defaults to false.
133
   * @return {!Float64Array|Array<Float64Array>} the samples.
134
   */
135
  getSamples(interleaved=false) {
136
    /**
137
     * A Float64Array created with a size to match the
138
     * the length of the samples.
139
     * @type {!Float64Array}
140
     */
141
    let samples = new Float64Array(
142
      this.data.samples.length / (this.dataType.bits / 8));
143
    // Unpack all the samples
144
    unpackArrayTo(this.data.samples, this.dataType, samples);
145
    if (!interleaved && this.fmt.numChannels > 1) {
146
      return deInterleave(samples, this.fmt.numChannels);
147
    }
148
    return samples;
149
  }
150
151
  /**
152
   * Return the sample at a given index.
153
   * @param {number} index The sample index.
154
   * @return {number} The sample.
155
   * @throws {Error} If the sample index is off range.
156
   */
157
  getSample(index) {
158
    index = index * (this.dataType.bits / 8);
159
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
160
      throw new Error('Range error');
161
    }
162
    return unpack(
163
      this.data.samples.slice(index, index + this.dataType.bits / 8),
164
      this.dataType);
165
  }
166
167
  /**
168
   * Set the sample at a given index.
169
   * @param {number} index The sample index.
170
   * @param {number} sample The sample.
171
   * @throws {Error} If the sample index is off range.
172
   */
173
  setSample(index, sample) {
174
    index = index * (this.dataType.bits / 8);
175
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
176
      throw new Error('Range error');
177
    }
178
    packTo(sample, this.dataType, this.data.samples, index);
179
  }
180
181
  /**
182
   * Return the value of the iXML chunk.
183
   * @return {string} The contents of the iXML chunk.
184
   */
185
  getiXML() {
186
    return this.iXML.value;
187
  }
188
189
  /**
190
   * Set the value of the iXML chunk.
191
   * @param {string} iXMLValue The value for the iXML chunk.
192
   * @throws {TypeError} If the value is not a string.
193
   */
194
  setiXML(iXMLValue) {
195
    if (typeof iXMLValue !== 'string') {
196
      throw new TypeError('iXML value must be a string.');
197
    }
198
    this.iXML.value = iXMLValue;
199
    this.iXML.chunkId = 'iXML';
200
  }
201
202
  /**
203
   * Get the value of the _PMX chunk.
204
   * @return {string} The contents of the _PMX chunk.
205
   */
206
  get_PMX() {
207
    return this._PMX.value;
208
  }
209
210
  /**
211
   * Set the value of the _PMX chunk.
212
   * @param {string} _PMXValue The value for the _PMX chunk.
213
   * @throws {TypeError} If the value is not a string.
214
   */
215
  set_PMX(_PMXValue) {
216
    if (typeof _PMXValue !== 'string') {
217
      throw new TypeError('_PMX value must be a string.');
218
    }
219
    this._PMX.value = _PMXValue;
220
    this._PMX.chunkId = '_PMX';
221
  }
222
223
  /**
224
   * Set up the WaveFileCreator object based on the arguments passed.
225
   * @param {number} numChannels The number of channels.
226
   * @param {number} sampleRate The sample rate.
227
   *    Integers like 8000, 44100, 48000, 96000, 192000.
228
   * @param {string} bitDepthCode The audio bit depth code.
229
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
230
   *    or any value between '8' and '32' (like '12').
231
   * @param {
232
   *      !Array<number> |
233
   *      !Array<Array<number>> |
234
   *      !TypedArray |
235
   *      !Array<TypedArray>
236
   *    } samples The samples.
237
   * @param {?Object} options Optional. Used to force the container
238
   *    as RIFX with {'container': 'RIFX'}
239
   * @throws {Error} If any argument does not meet the criteria.
240
   * @private
241
   */
242
  newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
243
    if (!options.container) {
244
      options.container = 'RIFF';
245
    }
246
    this.container = options.container;
247
    this.bitDepth = bitDepthCode;
248
    samples = interleave(samples);
249
    this.updateDataType_();
250
    /** @type {number} */
251
    let numBytes = this.dataType.bits / 8;
252
    this.data.samples = new Uint8Array(samples.length * numBytes);
253
    packArrayTo(samples, this.dataType, this.data.samples);
254
    this.makeWavHeader_(
255
      bitDepthCode, numChannels, sampleRate,
256
      numBytes, this.data.samples.length, options);
257
    this.data.chunkId = 'data';
258
    this.data.chunkSize = this.data.samples.length;
259
    this.validateWavHeader_();
260
  }
261
262
  /**
263
   * Define the header of a wav file.
264
   * @param {string} bitDepthCode The audio bit depth
265
   * @param {number} numChannels The number of channels
266
   * @param {number} sampleRate The sample rate.
267
   * @param {number} numBytes The number of bytes each sample use.
268
   * @param {number} samplesLength The length of the samples in bytes.
269
   * @param {!Object} options The extra options, like container defintion.
270
   * @private
271
   */
272
  makeWavHeader_(
273
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
274
    if (bitDepthCode == '4') {
275
      this.createADPCMHeader_(
276
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
277
278
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
279
      this.createALawMulawHeader_(
280
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
281
282
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
283
        numChannels > 2) {
284
      this.createExtensibleHeader_(
285
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
286
287
    } else {
288
      this.createPCMHeader_(
289
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
290
    }
291
  }
292
293
  /**
294
   * Create the header of a linear PCM wave file.
295
   * @param {string} bitDepthCode The audio bit depth
296
   * @param {number} numChannels The number of channels
297
   * @param {number} sampleRate The sample rate.
298
   * @param {number} numBytes The number of bytes each sample use.
299
   * @param {number} samplesLength The length of the samples in bytes.
300
   * @param {!Object} options The extra options, like container defintion.
301
   * @private
302
   */
303
  createPCMHeader_(
304
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
305
    this.container = options.container;
306
    this.chunkSize = 36 + samplesLength;
307
    this.format = 'WAVE';
308
    this.bitDepth = bitDepthCode;
309
    this.fmt = {
310
      chunkId: 'fmt ',
311
      chunkSize: 16,
312
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
313
      numChannels: numChannels,
314
      sampleRate: sampleRate,
315
      byteRate: (numChannels * numBytes) * sampleRate,
316
      blockAlign: numChannels * numBytes,
317
      bitsPerSample: parseInt(bitDepthCode, 10),
318
      cbSize: 0,
319
      validBitsPerSample: 0,
320
      dwChannelMask: 0,
321
      subformat: []
322
    };
323
  }
324
325
  /**
326
   * Create the header of a ADPCM wave file.
327
   * @param {string} bitDepthCode The audio bit depth
328
   * @param {number} numChannels The number of channels
329
   * @param {number} sampleRate The sample rate.
330
   * @param {number} numBytes The number of bytes each sample use.
331
   * @param {number} samplesLength The length of the samples in bytes.
332
   * @param {!Object} options The extra options, like container defintion.
333
   * @private
334
   */
335
  createADPCMHeader_(
336
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
337
    this.createPCMHeader_(
338
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
339
    this.chunkSize = 40 + samplesLength;
340
    this.fmt.chunkSize = 20;
341
    this.fmt.byteRate = 4055;
342
    this.fmt.blockAlign = 256;
343
    this.fmt.bitsPerSample = 4;
344
    this.fmt.cbSize = 2;
345
    this.fmt.validBitsPerSample = 505;
346
    this.fact = {
347
      chunkId: 'fact',
348
      chunkSize: 4,
349
      dwSampleLength: samplesLength * 2
350
    };
351
  }
352
353
  /**
354
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
355
   * @param {string} bitDepthCode The audio bit depth
356
   * @param {number} numChannels The number of channels
357
   * @param {number} sampleRate The sample rate.
358
   * @param {number} numBytes The number of bytes each sample use.
359
   * @param {number} samplesLength The length of the samples in bytes.
360
   * @param {!Object} options The extra options, like container defintion.
361
   * @private
362
   */
363
  createExtensibleHeader_(
364
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
365
    this.createPCMHeader_(
366
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
367
    this.chunkSize = 36 + 24 + samplesLength;
368
    this.fmt.chunkSize = 40;
369
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
370
    this.fmt.cbSize = 22;
371
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
372
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
373
    // subformat 128-bit GUID as 4 32-bit values
374
    // only supports uncompressed integer PCM samples
375
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
376
  }
377
378
  /**
379
   * Create the header of mu-Law and A-Law wave files.
380
   * @param {string} bitDepthCode The audio bit depth
381
   * @param {number} numChannels The number of channels
382
   * @param {number} sampleRate The sample rate.
383
   * @param {number} numBytes The number of bytes each sample use.
384
   * @param {number} samplesLength The length of the samples in bytes.
385
   * @param {!Object} options The extra options, like container defintion.
386
   * @private
387
   */
388
  createALawMulawHeader_(
389
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
390
    this.createPCMHeader_(
391
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
392
    this.chunkSize = 40 + samplesLength;
393
    this.fmt.chunkSize = 20;
394
    this.fmt.cbSize = 2;
395
    this.fmt.validBitsPerSample = 8;
396
    this.fact = {
397
      chunkId: 'fact',
398
      chunkSize: 4,
399
      dwSampleLength: samplesLength
400
    };
401
  }
402
403
  /**
404
   * Set the string code of the bit depth based on the 'fmt ' chunk.
405
   * @private
406
   */
407
  bitDepthFromFmt_() {
408
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
409
      this.bitDepth = '32f';
410
    } else if (this.fmt.audioFormat === 6) {
411
      this.bitDepth = '8a';
412
    } else if (this.fmt.audioFormat === 7) {
413
      this.bitDepth = '8m';
414
    } else {
415
      this.bitDepth = this.fmt.bitsPerSample.toString();
416
    }
417
  }
418
419
  /**
420
   * Validate the bit depth.
421
   * @return {boolean} True is the bit depth is valid.
422
   * @throws {Error} If bit depth is invalid.
423
   * @private
424
   */
425
  validateBitDepth_() {
426
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
427
      if (parseInt(this.bitDepth, 10) > 8 &&
428
          parseInt(this.bitDepth, 10) < 54) {
429
        return true;
430
      }
431
      throw new Error('Invalid bit depth.');
432
    }
433
    return true;
434
  }
435
436
  /**
437
   * Update the type definition used to read and write the samples.
438
   * @private
439
   */
440
  updateDataType_() {
441
    this.dataType = {
442
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
443
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
444
      signed: this.bitDepth != '8',
445
      be: this.container == 'RIFX'
446
    };
447
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
448
      this.dataType.bits = 8;
449
      this.dataType.signed = false;
450
    }
451
  }
452
453
  /**
454
   * Validate the header of the file.
455
   * @throws {Error} If bit depth is invalid.
456
   * @throws {Error} If the number of channels is invalid.
457
   * @throws {Error} If the sample rate is invalid.
458
   * @ignore
459
   * @private
460
   */
461
  validateWavHeader_() {
462
    this.validateBitDepth_();
463
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
464
      throw new Error('Invalid number of channels.');
465
    }
466
    if (!validateSampleRate(
467
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
468
      throw new Error('Invalid sample rate.');
469
    }
470
  }
471
}
472