Completed
Push — master ( 4f3f4b...826326 )
by Rafael S.
03:11
created

wavefile-converter.js ➔ outputSize_   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
dl 0
loc 8
rs 10
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 WaveFileConverter class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { changeBitDepth } from 'bitdepth';
31
import * as imaadpcm from 'imaadpcm';
32
import * as alawmulaw from 'alawmulaw';
33
import { unpackArrayTo } from 'byte-data';
34
import { WaveFileCueEditor } from './wavefile-cue-editor';
35
import { validateSampleRate } from './validators/validate-sample-rate';
36
import { resample } from './resampler';
37
38
/**
39
 * A class to convert wav files to other types of wav files.
40
 * @extends WaveFileCueEditor
41
 * @ignore
42
 */
43
export class WaveFileConverter extends WaveFileCueEditor {
44
45
  /**
46
   * Force a file as RIFF.
47
   */
48
  toRIFF() {
49
    /** @type {!Float64Array} */
50
    let output = new Float64Array(
51
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
52
    unpackArrayTo(this.data.samples, this.dataType, output,
53
      0, this.data.samples.length, false, false);
54
    this.fromExisting_(
55
      this.fmt.numChannels,
56
      this.fmt.sampleRate,
57
      this.bitDepth,
58
      output);
59
  }
60
61
  /**
62
   * Force a file as RIFX.
63
   */
64
  toRIFX() {
65
    /** @type {!Float64Array} */
66
    let output = new Float64Array(
67
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
68
    unpackArrayTo(this.data.samples, this.dataType, output,
69
      0, this.data.samples.length, false, false);
70
    this.fromExisting_(
71
      this.fmt.numChannels,
72
      this.fmt.sampleRate,
73
      this.bitDepth,
74
      output,
75
      {container: 'RIFX'});
76
  }
77
78
  /**
79
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
80
   * @throws {Error} If sample rate is not 8000.
81
   * @throws {Error} If number of channels is not 1.
82
   */
83
  toIMAADPCM() {
84
    if (this.fmt.sampleRate !== 8000) {
85
      throw new Error(
86
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
87
    } else if (this.fmt.numChannels !== 1) {
88
      throw new Error(
89
        'Only mono files can be compressed as IMA-ADPCM.');
90
    } else {
91
      this.assure16Bit_();
92
      /** @type {!Int16Array} */
93
      let output = new Int16Array(
94
        outputSize_(this.data.samples.length, 2));
95
      unpackArrayTo(this.data.samples, this.dataType, output,
96
        0, this.data.samples.length, false, false);
97
      this.fromExisting_(
98
        this.fmt.numChannels,
99
        this.fmt.sampleRate,
100
        '4',
101
        imaadpcm.encode(output),
102
        {container: this.correctContainer_()});
103
    }
104
  }
105
106
  /**
107
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
108
   * @param {string} bitDepthCode The new bit depth of the samples.
109
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
110
   *    Optional. Default is 16.
111
   */
112
  fromIMAADPCM(bitDepthCode='16') {
113
    this.fromExisting_(
114
      this.fmt.numChannels,
115
      this.fmt.sampleRate,
116
      '16',
117
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
118
      {container: this.correctContainer_()});
119
    if (bitDepthCode != '16') {
120
      this.toBitDepth(bitDepthCode);
121
    }
122
  }
123
124
  /**
125
   * Encode a 16-bit wave file as 8-bit A-Law.
126
   */
127
  toALaw() {
128
    this.assure16Bit_();
129
    /** @type {!Int16Array} */
130
    let output = new Int16Array(
131
      outputSize_(this.data.samples.length, 2));
132
    unpackArrayTo(this.data.samples, this.dataType, output,
133
        0, this.data.samples.length, false, false);
134
    this.fromExisting_(
135
      this.fmt.numChannels,
136
      this.fmt.sampleRate,
137
      '8a',
138
      alawmulaw.alaw.encode(output),
139
      {container: this.correctContainer_()});
140
  }
141
142
  /**
143
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
144
   * @param {string} bitDepthCode The new bit depth of the samples.
145
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
146
   *    Optional. Default is 16.
147
   */
148
  fromALaw(bitDepthCode='16') {
149
    this.fromExisting_(
150
      this.fmt.numChannels,
151
      this.fmt.sampleRate,
152
      '16',
153
      alawmulaw.alaw.decode(this.data.samples),
154
      {container: this.correctContainer_()});
155
    if (bitDepthCode != '16') {
156
      this.toBitDepth(bitDepthCode);
157
    }
158
  }
159
160
  /**
161
   * Encode 16-bit wave file as 8-bit mu-Law.
162
   */
163
  toMuLaw() {
164
    this.assure16Bit_();
165
    /** @type {!Int16Array} */
166
    let output = new Int16Array(
167
      outputSize_(this.data.samples.length, 2));
168
    unpackArrayTo(this.data.samples, this.dataType, output,
169
        0, this.data.samples.length, false, false);
170
    this.fromExisting_(
171
      this.fmt.numChannels,
172
      this.fmt.sampleRate,
173
      '8m',
174
      alawmulaw.mulaw.encode(output),
175
      {container: this.correctContainer_()});
176
  }
177
178
  /**
179
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
180
   * @param {string} bitDepthCode The new bit depth of the samples.
181
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
182
   *    Optional. Default is 16.
183
   */
184
  fromMuLaw(bitDepthCode='16') {
185
    this.fromExisting_(
186
      this.fmt.numChannels,
187
      this.fmt.sampleRate,
188
      '16',
189
      alawmulaw.mulaw.decode(this.data.samples),
190
      {container: this.correctContainer_()});
191
    if (bitDepthCode != '16') {
192
      this.toBitDepth(bitDepthCode);
193
    }
194
  }
195
196
  /**
197
   * Change the bit depth of the samples.
198
   * @param {string} newBitDepth The new bit depth of the samples.
199
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
200
   * @param {boolean} changeResolution A boolean indicating if the
201
   *    resolution of samples should be actually changed or not.
202
   * @throws {Error} If the bit depth is not valid.
203
   */
204
  toBitDepth(newBitDepth, changeResolution=true) {
205
    /** @type {string} */
206
    let toBitDepth = newBitDepth;
207
    /** @type {string} */
208
    let thisBitDepth = this.bitDepth;
209
    if (!changeResolution) {
210
      if (newBitDepth != '32f') {
211
        toBitDepth = this.dataType.bits.toString();
212
      }
213
      thisBitDepth = '' + this.dataType.bits;
214
    }
215
    // If the file is compressed, make it
216
    // PCM before changing the bit depth
217
    this.assureUncompressed_();
218
    /**
219
     * The original samples, interleaved.
220
     * @type {!Array|!TypedArray}
221
     */
222
    let samples = this.getSamples(true);
223
    /**
224
     * The container for the new samples.
225
     * @type {!Float64Array}
226
     */
227
    let newSamples = new Float64Array(samples.length);
228
    // Change the bit depth
229
    changeBitDepth(samples, thisBitDepth, newSamples, toBitDepth);
230
    // Re-create the file
231
    this.fromExisting_(
232
      this.fmt.numChannels,
233
      this.fmt.sampleRate,
234
      newBitDepth,
235
      newSamples,
236
      {container: this.correctContainer_()});
237
  }
238
239
  /**
240
   * Convert the sample rate of the file.
241
   * @param {number} sampleRate The target sample rate.
242
   * @param {?Object} details The extra configuration, if needed.
243
   */
244
  toSampleRate(sampleRate, details={}) {
245
    this.validateResample_(sampleRate);
246
    /** @type {!Array|!TypedArray} */
247
    let samples = this.getSamples();
248
    /** @type {!Array|!Float64Array} */
249
    let newSamples = [];
250
    // Mono files
251
    if (samples.constructor === Float64Array) {
252
      newSamples = resample(samples, this.fmt.sampleRate, sampleRate, details);
253
    // Multi-channel files
254
    } else {
255
      for (let i = 0; i < samples.length; i++) {
256
        newSamples.push(resample(
257
          samples[i], this.fmt.sampleRate, sampleRate, details));
258
      }
259
    }
260
    // Truncate samples
261
    if (this.bitDepth !== '64' && this.bitDepth !== '32f') {
262
      // Truncate samples in mono files
263
      if (newSamples[0].constructor === Number) {
264
        truncateIntSamples(newSamples, this.dataType.bits);
265
      // Truncate samples in multi-channel files
266
      } else {
267
        for (let i = 0; i < newSamples.length; i++) {
268
          truncateIntSamples(newSamples[i], this.dataType.bits);
269
        }
270
      }
271
    }
272
    // Recreate the file
273
    this.fromExisting_(
274
      this.fmt.numChannels, sampleRate, this.bitDepth, newSamples,
275
      {'container': this.correctContainer_()});
276
  }
277
278
  /**
279
   * Validate the conditions for resampling.
280
   * @param {number} sampleRate The target sample rate.
281
   * @throws {Error} If the file cant be resampled.
282
   * @private
283
   */
284
  validateResample_(sampleRate) {
285
    if (!validateSampleRate(
286
        this.fmt.numChannels, this.fmt.bitsPerSample, sampleRate)) {
287
      throw new Error('Invalid sample rate.');
288
    } else if (['4','8a','8m'].indexOf(this.bitDepth) > -1) {
289
      throw new Error(
290
        'wavefile can\'t change the sample rate of compressed files.');
291
    }
292
  }
293
294
  /**
295
   * Make the file 16-bit if it is not.
296
   * @private
297
   */
298
  assure16Bit_() {
299
    this.assureUncompressed_();
300
    if (this.bitDepth != '16') {
301
      this.toBitDepth('16');
302
    }
303
  }
304
305
  /**
306
   * Uncompress the samples in case of a compressed file.
307
   * @private
308
   */
309
  assureUncompressed_() {
310
    if (this.bitDepth == '8a') {
311
      this.fromALaw();
312
    } else if (this.bitDepth == '8m') {
313
      this.fromMuLaw();
314
    } else if (this.bitDepth == '4') {
315
      this.fromIMAADPCM();
316
    }
317
  }
318
319
  /**
320
   * Return 'RIFF' if the container is 'RF64', the current container name
321
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
322
   * @return {string}
323
   * @private
324
   */
325
  correctContainer_() {
326
    return this.container == 'RF64' ? 'RIFF' : this.container;
327
  }
328
329
  /**
330
   * Set up the WaveFileCreator object based on the arguments passed.
331
   * This method only reset the fmt , fact, ds64 and data chunks.
332
   * @param {number} numChannels The number of channels
333
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
334
   * @param {number} sampleRate The sample rate.
335
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
336
   * @param {string} bitDepthCode The audio bit depth code.
337
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
338
   *    or any value between '8' and '32' (like '12').
339
   * @param {!Array|!TypedArray} samples
340
   *    The samples. Must be in the correct range according to the bit depth.
341
   * @param {?Object} options Optional. Used to force the container
342
   *    as RIFX with {'container': 'RIFX'}
343
   * @throws {Error} If any argument does not meet the criteria.
344
   * @private
345
   */
346
  fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
347
    let tmpWav = new WaveFileCueEditor();
348
    Object.assign(this.fmt, tmpWav.fmt);
349
    Object.assign(this.fact, tmpWav.fact);
350
    Object.assign(this.ds64, tmpWav.ds64);
351
    Object.assign(this.data, tmpWav.data);
352
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
353
  }
354
}
355
356
/**
357
 * Return the size in bytes of the output sample array when applying
358
 * compression to 16-bit samples.
359
 * @return {number}
360
 * @private
361
 */
362
function outputSize_(byteLen, byteOffset) {
363
  /** @type {number} */
364
  let outputSize = byteLen / byteOffset;
365
  if (outputSize % 2) {
366
    outputSize++;
367
  }
368
  return outputSize;
369
}
370
371
/**
372
 * Clamp integer samples.
373
 * @param {!(Array<number>|TypedArray)} samples The samples to round.
374
 * @param {number} bits The number of bits.
375
 * @private
376
 */
377
function truncateIntSamples(samples, bits) {
378
  let max = bits === 8 ? 255 : Math.pow(2, bits) / 2 - 1;
379
  let min = bits === 8 ? 0 : -max - 1;
380
  for (let i = 0, len = samples.length; i < len; i++) {
381
    samples[i] = Math.round(samples[i]);
382
    if (samples[i] > max) {
383
      samples[i] = max;
384
    } else if (samples[i] < min) {
385
      samples[i] = min;
386
    }
387
  }
388
}
389