Passed
Branch wavefile-reader (40d3f0)
by Rafael S.
02:22
created

WaveFileCreator.getSample   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 9
rs 10
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 {packArrayTo, packTo, unpack} from 'byte-data';
36
37
/**
38
 * A class to read, write and create wav files.
39
 * @extends WaveFileParser
40
 */
41
export default class WaveFileCreator extends WaveFileParser {
42
43
  /**
44
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
45
   * @throws {Error} If container is not RIFF, RIFX or RF64.
46
   * @throws {Error} If format is not WAVE.
47
   * @throws {Error} If no 'fmt ' chunk is found.
48
   * @throws {Error} If no 'data' chunk is found.
49
   */
50
  constructor(wavBuffer=null) {
51
    super(wavBuffer);
52
    /**
53
     * @type {!Object}
54
     * @protected
55
     */
56
    this.dataType = {};
57
    if (wavBuffer) {
58
      this.updateDataType();
59
    }
60
  }
61
62
  /**
63
   * Set up the WaveFileCreator object based on the arguments passed.
64
   * @param {number} numChannels The number of channels
65
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
66
   * @param {number} sampleRate The sample rate.
67
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
68
   * @param {string} bitDepthCode The audio bit depth code.
69
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
70
   *    or any value between '8' and '32' (like '12').
71
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
72
   *    The samples. Must be in the correct range according to the bit depth.
73
   * @param {?Object} options Optional. Used to force the container
74
   *    as RIFX with {'container': 'RIFX'}
75
   * @throws {Error} If any argument does not meet the criteria.
76
   */
77
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
78
    if (!options.container) {
79
      options.container = 'RIFF';
80
    }
81
    this.container = options.container;
82
    this.bitDepth = bitDepthCode;
83
    samples = interleave(samples);
84
    this.updateDataType();
85
    /** @type {number} */
86
    let numBytes = this.dataType.bits / 8;
87
    this.data.samples = new Uint8Array(samples.length * numBytes);
88
    packArrayTo(samples, this.dataType, this.data.samples);
89
    this.clearHeader();
90
    this.makeWavHeader_(
91
      bitDepthCode, numChannels, sampleRate,
92
      numBytes, this.data.samples.length, options);
93
    this.data.chunkId = 'data';
94
    this.data.chunkSize = this.data.samples.length;
95
    this.validateWavHeader();
96
  }
97
98
  /**
99
   * Set up the WaveFileParser object from a byte buffer.
100
   * @param {!Uint8Array} wavBuffer The buffer.
101
   * @param {boolean=} samples True if the samples should be loaded.
102
   * @throws {Error} If container is not RIFF, RIFX or RF64.
103
   * @throws {Error} If format is not WAVE.
104
   * @throws {Error} If no 'fmt ' chunk is found.
105
   * @throws {Error} If no 'data' chunk is found.
106
   */
107
  fromBuffer(wavBuffer, samples=true) {
108
    super.fromBuffer(wavBuffer, samples);
109
    this.updateDataType();
110
  }
111
112
    /**
113
   * Return the sample at a given index.
114
   * @param {number} index The sample index.
115
   * @return {number} The sample.
116
   * @throws {Error} If the sample index is off range.
117
   */
118
  getSample(index) {
119
    index = index * (this.dataType.bits / 8);
120
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
121
      throw new Error('Range error');
122
    }
123
    return unpack(
124
      this.data.samples.slice(index, index + this.dataType.bits / 8),
125
      this.dataType);
126
  }
127
128
  /**
129
   * Set the sample at a given index.
130
   * @param {number} index The sample index.
131
   * @param {number} sample The sample.
132
   * @throws {Error} If the sample index is off range.
133
   */
134
  setSample(index, sample) {
135
    index = index * (this.dataType.bits / 8);
136
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
137
      throw new Error('Range error');
138
    }
139
    packTo(sample, this.dataType, this.data.samples, index);
140
  }
141
142
  /**
143
   * Update the type definition used to read and write the samples.
144
   * @protected
145
   */
146
  updateDataType() {
147
    this.dataType = {
148
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
149
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
150
      signed: this.bitDepth != '8',
151
      be: this.container == 'RIFX'
152
    };
153
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
154
      this.dataType.bits = 8;
155
      this.dataType.signed = false;
156
    }
157
  }
158
159
  /**
160
   * Define the header of a wav file.
161
   * @param {string} bitDepthCode The audio bit depth
162
   * @param {number} numChannels The number of channels
163
   * @param {number} sampleRate The sample rate.
164
   * @param {number} numBytes The number of bytes each sample use.
165
   * @param {number} samplesLength The length of the samples in bytes.
166
   * @param {!Object} options The extra options, like container defintion.
167
   * @private
168
   */
169
  makeWavHeader_(
170
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
171
    if (bitDepthCode == '4') {
172
      this.createADPCMHeader_(
173
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
174
175
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
176
      this.createALawMulawHeader_(
177
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
178
179
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
180
        numChannels > 2) {
181
      this.createExtensibleHeader_(
182
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
183
184
    } else {
185
      this.createPCMHeader_(
186
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
187
    }
188
  }
189
190
  /**
191
   * Create the header of a linear PCM wave file.
192
   * @param {string} bitDepthCode The audio bit depth
193
   * @param {number} numChannels The number of channels
194
   * @param {number} sampleRate The sample rate.
195
   * @param {number} numBytes The number of bytes each sample use.
196
   * @param {number} samplesLength The length of the samples in bytes.
197
   * @param {!Object} options The extra options, like container defintion.
198
   * @private
199
   */
200
  createPCMHeader_(
201
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
202
    this.container = options.container;
203
    this.chunkSize = 36 + samplesLength;
204
    this.format = 'WAVE';
205
    this.bitDepth = bitDepthCode;
206
    this.fmt = {
207
      chunkId: 'fmt ',
208
      chunkSize: 16,
209
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
210
      numChannels: numChannels,
211
      sampleRate: sampleRate,
212
      byteRate: (numChannels * numBytes) * sampleRate,
213
      blockAlign: numChannels * numBytes,
214
      bitsPerSample: parseInt(bitDepthCode, 10),
215
      cbSize: 0,
216
      validBitsPerSample: 0,
217
      dwChannelMask: 0,
218
      subformat: []
219
    };
220
  }
221
222
  /**
223
   * Create the header of a ADPCM wave file.
224
   * @param {string} bitDepthCode The audio bit depth
225
   * @param {number} numChannels The number of channels
226
   * @param {number} sampleRate The sample rate.
227
   * @param {number} numBytes The number of bytes each sample use.
228
   * @param {number} samplesLength The length of the samples in bytes.
229
   * @param {!Object} options The extra options, like container defintion.
230
   * @private
231
   */
232
  createADPCMHeader_(
233
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
234
    this.createPCMHeader_(
235
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
236
    this.chunkSize = 40 + samplesLength;
237
    this.fmt.chunkSize = 20;
238
    this.fmt.byteRate = 4055;
239
    this.fmt.blockAlign = 256;
240
    this.fmt.bitsPerSample = 4;
241
    this.fmt.cbSize = 2;
242
    this.fmt.validBitsPerSample = 505;
243
    this.fact = {
244
      chunkId: 'fact',
245
      chunkSize: 4,
246
      dwSampleLength: samplesLength * 2
247
    };
248
  }
249
250
  /**
251
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
252
   * @param {string} bitDepthCode The audio bit depth
253
   * @param {number} numChannels The number of channels
254
   * @param {number} sampleRate The sample rate.
255
   * @param {number} numBytes The number of bytes each sample use.
256
   * @param {number} samplesLength The length of the samples in bytes.
257
   * @param {!Object} options The extra options, like container defintion.
258
   * @private
259
   */
260
  createExtensibleHeader_(
261
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
262
    this.createPCMHeader_(
263
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
264
    this.chunkSize = 36 + 24 + samplesLength;
265
    this.fmt.chunkSize = 40;
266
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
267
    this.fmt.cbSize = 22;
268
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
269
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
270
    // subformat 128-bit GUID as 4 32-bit values
271
    // only supports uncompressed integer PCM samples
272
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
273
  }
274
275
  /**
276
   * Create the header of mu-Law and A-Law wave files.
277
   * @param {string} bitDepthCode The audio bit depth
278
   * @param {number} numChannels The number of channels
279
   * @param {number} sampleRate The sample rate.
280
   * @param {number} numBytes The number of bytes each sample use.
281
   * @param {number} samplesLength The length of the samples in bytes.
282
   * @param {!Object} options The extra options, like container defintion.
283
   * @private
284
   */
285
  createALawMulawHeader_(
286
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
287
    this.createPCMHeader_(
288
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
289
    this.chunkSize = 40 + samplesLength;
290
    this.fmt.chunkSize = 20;
291
    this.fmt.cbSize = 2;
292
    this.fmt.validBitsPerSample = 8;
293
    this.fact = {
294
      chunkId: 'fact',
295
      chunkSize: 4,
296
      dwSampleLength: samplesLength
297
    };
298
  }
299
}
300