Passed
Branch v11-no-dependencies (fcadbf)
by Rafael S.
06:36
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 { encode as encodeIMAADPCM,
0 ignored issues
show
Unused Code introduced by
The variable encode seems to be never used. Consider removing it.
Loading history...
32
  decode as decodeIMAADPCM} from './codecs/imaadpcm';
0 ignored issues
show
Unused Code introduced by
The variable decode seems to be never used. Consider removing it.
Loading history...
33
import {encode as encodeALaw, decode as decodeALaw} from './codecs/alaw';
0 ignored issues
show
introduced by
The variable encode already seems to be declared on line 31. Are you sure you want to import using encode as name?
Loading history...
introduced by
The variable decode already seems to be declared on line 32. Are you sure you want to import using decode as name?
Loading history...
34
import {encode as encodeMuLaw, decode as decodeMuLaw} from './codecs/mulaw';
35
import { unpackArrayTo } from './parsers/binary';
36
import { WaveFileCueEditor } from './wavefile-cue-editor';
37
import { validateSampleRate } from './validators/validate-sample-rate';
38
import { resample } from './resampler';
39
40
/**
41
 * A class to convert wav files to other types of wav files.
42
 * @extends WaveFileCueEditor
43
 * @ignore
44
 */
45
export class WaveFileConverter extends WaveFileCueEditor {
46
47
  /**
48
   * Force a file as RIFF.
49
   */
50
  toRIFF() {
51
    /** @type {!Float64Array} */
52
    let output = new Float64Array(
53
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
54
    unpackArrayTo(this.data.samples, this.dataType, output,
55
      0, this.data.samples.length);
56
    this.fromExisting_(
57
      this.fmt.numChannels,
58
      this.fmt.sampleRate,
59
      this.bitDepth,
60
      output,
61
      {container: 'RIFF'});
62
  }
63
64
  /**
65
   * Force a file as RIFX.
66
   */
67
  toRIFX() {
68
    /** @type {!Float64Array} */
69
    let output = new Float64Array(
70
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
71
    unpackArrayTo(this.data.samples, this.dataType, output,
72
      0, this.data.samples.length);
73
    this.fromExisting_(
74
      this.fmt.numChannels,
75
      this.fmt.sampleRate,
76
      this.bitDepth,
77
      output,
78
      {container: 'RIFX'});
79
  }
80
81
  /**
82
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
83
   * @throws {Error} If sample rate is not 8000.
84
   * @throws {Error} If number of channels is not 1.
85
   */
86
  toIMAADPCM() {
87
    if (this.fmt.sampleRate !== 8000) {
88
      throw new Error(
89
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
90
    } else if (this.fmt.numChannels !== 1) {
91
      throw new Error(
92
        'Only mono files can be compressed as IMA-ADPCM.');
93
    } else {
94
      this.assure16Bit_();
95
      /** @type {!Int16Array} */
96
      let output = new Int16Array(
97
        outputSize_(this.data.samples.length, 2));
98
      unpackArrayTo(this.data.samples, this.dataType, output,
99
        0, this.data.samples.length);
100
      this.fromExisting_(
101
        this.fmt.numChannels,
102
        this.fmt.sampleRate,
103
        '4',
104
        encodeIMAADPCM(output),
105
        {container: this.correctContainer_()});
106
    }
107
  }
108
109
  /**
110
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
111
   * @param {string=} [bitDepthCode='16'] The new bit depth of the samples.
112
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
113
   */
114
  fromIMAADPCM(bitDepthCode='16') {
115
    this.fromExisting_(
116
      this.fmt.numChannels,
117
      this.fmt.sampleRate,
118
      '16',
119
      decodeIMAADPCM(this.data.samples, this.fmt.blockAlign),
120
      {container: this.correctContainer_()});
121
    if (bitDepthCode != '16') {
122
      this.toBitDepth(bitDepthCode);
123
    }
124
  }
125
126
  /**
127
   * Encode a 16-bit wave file as 8-bit A-Law.
128
   */
129
  toALaw() {
130
    this.assure16Bit_();
131
    /** @type {!Int16Array} */
132
    let output = new Int16Array(
133
      outputSize_(this.data.samples.length, 2));
134
    unpackArrayTo(this.data.samples, this.dataType, output,
135
        0, this.data.samples.length);
136
    this.fromExisting_(
137
      this.fmt.numChannels,
138
      this.fmt.sampleRate,
139
      '8a',
140
      encodeALaw(output),
141
      {container: this.correctContainer_()});
142
  }
143
144
  /**
145
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
146
   * @param {string=} [bitDepthCode='16'] The new bit depth of the samples.
147
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
148
   */
149
  fromALaw(bitDepthCode='16') {
150
    this.fromExisting_(
151
      this.fmt.numChannels,
152
      this.fmt.sampleRate,
153
      '16',
154
      decodeALaw(this.data.samples),
155
      {container: this.correctContainer_()});
156
    if (bitDepthCode != '16') {
157
      this.toBitDepth(bitDepthCode);
158
    }
159
  }
160
161
  /**
162
   * Encode 16-bit wave file as 8-bit mu-Law.
163
   */
164
  toMuLaw() {
165
    this.assure16Bit_();
166
    /** @type {!Int16Array} */
167
    let output = new Int16Array(
168
      outputSize_(this.data.samples.length, 2));
169
    unpackArrayTo(this.data.samples, this.dataType, output,
170
        0, this.data.samples.length);
171
    this.fromExisting_(
172
      this.fmt.numChannels,
173
      this.fmt.sampleRate,
174
      '8m',
175
      encodeMuLaw(output),
176
      {container: this.correctContainer_()});
177
  }
178
179
  /**
180
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
181
   * @param {string=} [bitDepthCode='16'] The new bit depth of the samples.
182
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
183
   */
184
  fromMuLaw(bitDepthCode='16') {
185
    this.fromExisting_(
186
      this.fmt.numChannels,
187
      this.fmt.sampleRate,
188
      '16',
189
      decodeMuLaw(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=true] 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=} options The extra configuration, if needed.
243
   */
244
  toSampleRate(sampleRate, options) {
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, options);
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, options));
258
      }
259
    }
260
    // Recreate the file
261
    this.fromExisting_(
262
      this.fmt.numChannels, sampleRate, this.bitDepth, newSamples,
263
      {'container': this.correctContainer_()});
264
  }
265
266
  /**
267
   * Validate the conditions for resampling.
268
   * @param {number} sampleRate The target sample rate.
269
   * @throws {Error} If the file cant be resampled.
270
   * @private
271
   */
272
  validateResample_(sampleRate) {
273
    if (!validateSampleRate(
274
        this.fmt.numChannels, this.fmt.bitsPerSample, sampleRate)) {
275
      throw new Error('Invalid sample rate.');
276
    } else if (['4','8a','8m'].indexOf(this.bitDepth) > -1) {
277
      throw new Error(
278
        'wavefile can\'t change the sample rate of compressed files.');
279
    }
280
  }
281
282
  /**
283
   * Make the file 16-bit if it is not.
284
   * @private
285
   */
286
  assure16Bit_() {
287
    this.assureUncompressed_();
288
    if (this.bitDepth != '16') {
289
      this.toBitDepth('16');
290
    }
291
  }
292
293
  /**
294
   * Uncompress the samples in case of a compressed file.
295
   * @private
296
   */
297
  assureUncompressed_() {
298
    if (this.bitDepth == '8a') {
299
      this.fromALaw();
300
    } else if (this.bitDepth == '8m') {
301
      this.fromMuLaw();
302
    } else if (this.bitDepth == '4') {
303
      this.fromIMAADPCM();
304
    }
305
  }
306
307
  /**
308
   * Return 'RIFF' if the container is 'RF64', the current container name
309
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
310
   * @return {string}
311
   * @private
312
   */
313
  correctContainer_() {
314
    return this.container == 'RF64' ? 'RIFF' : this.container;
315
  }
316
317
  /**
318
   * Set up the WaveFileCreator object based on the arguments passed.
319
   * This method only reset the fmt , fact, ds64 and data chunks.
320
   * @param {number} numChannels The number of channels
321
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
322
   * @param {number} sampleRate The sample rate.
323
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
324
   * @param {string} bitDepthCode The audio bit depth code.
325
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
326
   *    or any value between '8' and '32' (like '12').
327
   * @param {!(Array|TypedArray)} samples
328
   *    The samples. Must be in the correct range according to the bit depth.
329
   * @param {Object} options Used to define the container. Uses RIFF by default.
330
   * @throws {Error} If any argument does not meet the criteria.
331
   * @private
332
   */
333
  fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options) {
334
    /** @type {!Object} */
335
    let tmpWav = new WaveFileCueEditor();
336
    Object.assign(this.fmt, tmpWav.fmt);
337
    Object.assign(this.fact, tmpWav.fact);
338
    Object.assign(this.ds64, tmpWav.ds64);
339
    Object.assign(this.data, tmpWav.data);
340
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
341
  }
342
}
343
344
/**
345
 * Return the size in bytes of the output sample array when applying
346
 * compression to 16-bit samples.
347
 * @return {number}
348
 * @private
349
 */
350
function outputSize_(byteLen, byteOffset) {
351
  /** @type {number} */
352
  let outputSize = byteLen / byteOffset;
353
  if (outputSize % 2) {
354
    outputSize++;
355
  }
356
  return outputSize;
357
}
358