Passed
Push — master ( 13b350...038bd4 )
by Rafael S.
03:46
created

WaveFileConverter.toSampleRate   A

Complexity

Conditions 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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