Completed
Branch master (6708a8)
by Rafael S.
07:49
created

WaveFileCreator.fromExisting_   A

Complexity

Conditions 2

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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