Completed
Push — master ( a73567...f014e9 )
by Rafael S.
02:49
created

WaveFileCreator.getiXML   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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