Passed
Push — master ( b3668a...bde98c )
by Rafael S.
01:32
created

index.js   A

Size

Lines of Code 501

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
nc 1
dl 0
loc 501
rs 10
noi 10

1 Function

Rating   Name   Duplication   Size   Complexity  
B ➔ ??? 0 29 2
1
/*!
2
 * wavefile
3
 * Read & write wave files with 8, 16, 24, 32 PCM, 32 IEEE & 64-bit data.
4
 * Copyright (c) 2017 Rafael da Silva Rocha. MIT License.
5
 * https://github.com/rochars/wavefile
6
 *
7
 */
8
9
const byteData = require("byte-data");
10
const wavefileheader = require("./src/wavefileheader");
11
12
/**
13
 * A wave file.
14
 */
15
class WaveFile extends wavefileheader.WaveFileHeader {
16
17
    /**
18
     * @param {Uint8Array} bytes The file bytes.
19
     * @param {boolean} enforceFact True if it should throw a error
20
     *      if no "fact" chunk is found.
21
     * @param {boolean} enforceBext True if it should throw a error
22
     *      if no "bext" chunk is found.
23
     */
24
    constructor(bytes, enforceFact=false, enforceBext=false) {
25
        super();
26
        /** @type {boolean} */
27
        this.isFromScratch_ = false;
28
        /** @type {boolean} */
29
        this.enforceFact = enforceFact;
30
        /** @type {boolean} */
31
        this.enforceBext = enforceBext;
32
        /**
33
         * Error messages.
34
         * @enum {string}
35
         */
36
        this.WaveErrors = {
37
            "format": "Not a supported format.",
38
            "wave": "Could not find the 'WAVE' chunk",
39
            "fmt ": "Could not find the 'fmt ' chunk",
40
            "data": "Could not find the 'data' chunk",
41
            "fact": "Could not find the 'fact' chunk",
42
            "bext": "Could not find the 'bext' chunk",
43
            "bitDepth": "Invalid bit depth.",
44
            "numChannels": "Invalid number of channels.",
45
            "sampleRate": "Invalid sample rate."
46
        };
47
        this.samples_ = [];
48
        this.bytes_ = [];
49
        if(bytes) {
50
            this.fromBuffer(bytes);
51
        }
52
    }
53
54
    /**
55
     * Create a WaveFile object based on the arguments passed.
56
     * @param {number} numChannels The number of channels
57
     *     (Ints like 1 for mono, 2 stereo and so on).
58
     * @param {number} sampleRate The sample rate.
59
     *     Integer numbers like 8000, 44100, 48000, 96000, 192000.
60
     * @param {string} bitDepth The audio bit depth.
61
     *     One of "8", "16", "24", "32", "32f", "64".
62
     * @param {!Array<number>} samples Array of samples to be written.
63
     *     Samples must be in the correct range according to the bit depth.
64
     *     Samples of multi-channel data .
65
     */
66
    fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) {
67
        if (!options.container) {
68
            options.container = "RIFF";
69
        }
70
        this.isFromScratch_ = true;
71
        let bytes = parseInt(bitDepth, 10) / 8;
72
        this.chunkSize = 36 + samples.length * bytes;
73
        this.subChunk1Size = 16;
74
        this.byteRate = (numChannels * bytes) * sampleRate;
75
        this.blockAlign = numChannels * bytes;
76
        this.chunkId = options.container;
77
        this.format = "WAVE";
78
        this.subChunk1Id = "fmt ";
79
        this.audioFormat = this.headerFormats_[bitDepth];
80
        this.numChannels = numChannels;
81
        this.sampleRate = sampleRate;
82
        this.bitsPerSample = parseInt(bitDepth, 10);
83
        this.subChunk2Id = "data";
84
        this.subChunk2Size = samples.length * bytes;
85
        this.samples_ = samples;
86
        this.bitDepth_ = bitDepth;
87
    }
88
89
    /**
90
     * Read a wave file from a byte buffer.
91
     * @param {Uint8Array} bytes The buffer.
92
     */
93
    fromBuffer(bytes) {
94
        this.isFromScratch_ = false;
95
        this.readRIFFChunk_(bytes);
96
        this.readWAVEChunk_(bytes);
97
        this.readFmtChunk_(bytes);
98
        this.readFactChunk_(bytes);
99
        this.readBextChunk_(bytes);
100
        this.readDataChunk_(bytes);
101
    }
102
103
    /**
104
     * Turn the WaveFile object into a byte buffer.
105
     * @return {Uint8Array}
106
     */
107
    toBuffer() {
108
        this.checkWriteInput_(this.numChannels, this.sampleRate, this.bitDepth_);
109
        this.samplesToBytes_();
110
        return new Uint8Array(this.createWaveFile_());
111
    }
112
113
    /**
114
     * Turn the file to RIFF.
115
     * All values will be little-endian when writing.
116
     */
117
    toRIFF() {
118
        this.chunkId = "RIFF";
119
    }
120
121
    /**
122
     * Turn the file to RIFX.
123
     * All values but FourCCs will be big-endian when writing.
124
     */
125
    toRIFX() {
126
        this.chunkId = "RIFX";
127
    }
128
129
    /**
130
     * TODO this should be a external module.
131
     * Change the bit depth of the data.
132
     * @param {string} bitDepth The new bit depth of the data.
133
     *      One of "8", "16", "24", "32", "32f", "64"
134
     */
135
    toBitDepth(bitDepth) {
136
        if (bitDepth == this.bitDepth_) {
137
            return;
138
        }
139
        let originalBitDepth = this.bitDepth_;
140
        this.bitDepth_ = bitDepth;
141
        try {
142
            this.validateBitDepth_();
143
        } catch(err) {
144
            this.bitDepth_ = originalBitDepth;
145
            throw err;
146
        }
147
        let len = this.samples_.length;
148
        let newSamples = [];
149
150
        // Get the max values for both original and target bit depth
151
        let oldMaxValue =
152
            parseInt((byteData.BitDepthMaxValues[parseInt(originalBitDepth, 10)]) / 2, 10);
153
        let newMaxValue =
154
            parseInt((byteData.BitDepthMaxValues[parseInt(this.bitDepth_, 10)]) / 2, 10);
155
156
        // needs dithering if the target bit depth
157
        // is lower than the original bit depth
158
        if (parseInt(this.bitDepth_, 10) < parseInt(originalBitDepth)) {
159
            // TODO: dithering
160
        }
161
162
        for (let i=0; i<len;i++) {
163
            
164
            // 8-bit samples are unsigned;
165
            // They are signed here before conversion
166
            // (other bit depths are all signed)
167
            if (originalBitDepth == "8") {
168
                this.samples_[i] -= 128;
169
            }
170
171
            // If it is a float-to-float or int-to-float conversion then
172
            // the samples in the target bit depth will be normalized in the
173
            // -1.0 to 1.0 range; there is no need to multiply
174
            if (this.bitDepth_ == "32f" || this.bitDepth_ == "64") {
175
                if (originalBitDepth == "32f" || originalBitDepth == "64") {
176
                    newSamples.push(this.samples_[i]);
177
                } else {
178
                    if (this.samples_[i] > 0) {
179
                        newSamples.push(this.samples_[i] / (oldMaxValue - 1));
180
                    } else {
181
                        newSamples.push(this.samples_[i] / oldMaxValue);
182
                    }
183
                }
184
185
            // If it is a float-to-int or int-to-int conversion then the
186
            // samples will be de-normalized according to the bit depth
187
            }else {
188
                
189
                // If the original samples are float, then they are already
190
                // normalized between -1.0 and 1.0; All that is need is to
191
                // multiply the sample values by the new bit depth max value
192
                if (originalBitDepth == "32f" || originalBitDepth == "64" ) {
193
                    if (this.samples_[i] > 0) {
194
                        newSamples.push((this.samples_[i] * newMaxValue) -1);
195
                    } else {
196
                        newSamples.push(this.samples_[i] * newMaxValue);
197
                    }
198
199
                // If the original samples are integers, then they need to be
200
                // divided by the maximum values of its original bit depth
201
                // (to normalize them between -1.0 and .10) and then multiply
202
                // them by the new bit depth max value
203
                } else {
204
                    if (this.samples_[i] > 0) {
205
                        newSamples.push(
206
                            parseInt((this.samples_[i] / (oldMaxValue - 1)) * newMaxValue - 1, 10)
207
                        );
208
                    } else {
209
                        newSamples.push(
210
                            parseInt((this.samples_[i] / oldMaxValue) * newMaxValue, 10)
211
                        );
212
                    }
213
                }
214
                
215
                // Make the samples unsigned if the target bit depth is "8"
216
                if (this.bitDepth_ == "8") {
217
                    newSamples[i] += 128;
218
                }
219
            }  
220
        }
221
        // recreate the file with the new samples
222
        // and the new bit depth
223
        this.fromScratch(
224
            this.numChannels,
225
            this.sampleRate,
226
            this.bitDepth_,
227
            newSamples,
228
            {"container": this.chunkId}
229
        );
230
    }
231
232
    /**
233
     * Interleave multi-channel samples.
234
     */
235
    interleave() {
236
        let finalSamples = [];
237
        let i;
238
        let j;
239
        let numChannels = this.samples_[0].length;
240
        for (i = 0; i < numChannels; i++) {
241
            for (j = 0; j < this.samples_.length; j++) {
242
                finalSamples.push(this.samples_[j][i]);
243
            }
244
        }
245
        this.samples_ = finalSamples;
246
    }
247
248
    /**
249
     * De-interleave samples into multiple channels.
250
     */
251
    deInterleave() {
252
        let finalSamples = [];
253
        let i;
254
        for (i = 0; i < this.numChannels; i++) {
255
            finalSamples[i] = [];
256
        }
257
        i = 0;
258
        let j;
259
        while (i < this.samples_.length) {
260
            for (j = 0; j < this.numChannels; j++) {
261
                finalSamples[j].push(this.samples_[i+j]);
262
            }
263
            i += j;
264
        }
265
        this.samples_ = finalSamples;
266
    }
267
268
    /**
269
     * Read the RIFF chunk a wave file.
270
     * @param {Uint8Array} bytes an array representing the wave file.
271
     * @throws {Error} If no "RIFF" chunk is found.
272
     */
273
    readRIFFChunk_(bytes) {
274
        this.chunkId = byteData.fromBytes(bytes.slice(0, 4),
275
            8, {"char": true});
276
        if (this.chunkId != "RIFF" && this.chunkId != "RIFX") {
277
            throw Error(this.WaveErrors.format);
278
        }
279
        this.chunkSize = byteData.fromBytes(
280
            bytes.slice(4, 8), 32, {"be": this.chunkId == "RIFX"})[0];
281
    }
282
283
    /**
284
     * Read the WAVE chunk of a wave file.
285
     * @param {Uint8Array} bytes an array representing the wave file.
286
     * @throws {Error} If no "WAVE" chunk is found.
287
     */
288
    readWAVEChunk_(bytes) {
289
        let start = byteData.findString(bytes, "WAVE");
290
        if (start === -1) {
291
            throw Error(this.WaveErrors.wave);
292
        }
293
        this.format = "WAVE";
294
    }
295
296
    /**
297
     * Read the "fmt " chunk of a wave file.
298
     * @param {Uint8Array} bytes an array representing the wave file.
299
     * @throws {Error} If no "fmt " chunk is found.
300
     */
301
    readFmtChunk_(bytes) {
302
        let start = byteData.findString(bytes, "fmt ");
303
        if (start === -1) {
304
            throw Error(this.WaveErrors["fmt "]);
305
        }
306
        let options = {"be": this.chunkId == "RIFX"};
307
        this.subChunk1Id = "fmt ";
308
        this.subChunk1Size = byteData.fromBytes(
309
            bytes.slice(start + 4, start + 8), 32, options)[0];
310
        this.audioFormat = byteData.fromBytes(
311
            bytes.slice(start + 8, start + 10), 16, options)[0];
312
        this.numChannels = byteData.fromBytes(
313
            bytes.slice(start + 10, start + 12), 16, options)[0];
314
        this.sampleRate = byteData.fromBytes(
315
            bytes.slice(start + 12, start + 16), 32, options)[0];
316
        this.byteRate = byteData.fromBytes(
317
            bytes.slice(start + 16, start + 20), 32, options)[0];
318
        this.blockAlign = byteData.fromBytes(
319
            bytes.slice(start + 20, start + 22), 16, options)[0];
320
        this.bitsPerSample = byteData.fromBytes(
321
            bytes.slice(start + 22, start + 24), 16, options)[0];
322
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
323
            this.bitDepth_ = "32f";
324
        }else {
325
            this.bitDepth_ = this.bitsPerSample.toString();
326
        }
327
    }
328
329
    /**
330
     * Read the "fact" chunk of a wave file.
331
     * @param {Uint8Array} bytes an array representing the wave file.
332
     * @throws {Error} If no "fact" chunk is found.
333
     */
334
    readFactChunk_(bytes) {
335
        let start = byteData.findString(bytes, "fact");
336
        if (start === -1 && this.enforceFact) {
337
            throw Error(this.WaveErrors.fact);
338
        }else if (start > -1) {
339
            this.factChunkId = "fact";
340
            //this.factChunkSize = byteData.uIntFrom4Bytes(
341
            //    bytes.slice(start + 4, start + 8));
342
            //this.dwSampleLength = byteData.uIntFrom4Bytes(
343
            //    bytes.slice(start + 8, start + 12));
344
        }
345
    }
346
347
    /**
348
     * Read the "bext" chunk of a wave file.
349
     * @param {Uint8Array} bytes an array representing the wave file.
350
     * @throws {Error} If no "bext" chunk is found.
351
     */
352
    readBextChunk_(bytes) {
353
        let start = byteData.findString(bytes, "bext");
354
        if (start === -1 && this.enforceBext) {
355
            throw Error(this.WaveErrors.bext);
356
        }else if (start > -1){
357
            this.bextChunkId = "bext";
358
        }
359
    }
360
361
    /**
362
     * Read the "data" chunk of a wave file.
363
     * @param {Uint8Array} bytes an array representing the wave file.
364
     * @throws {Error} If no "data" chunk is found.
365
     */
366
    readDataChunk_(bytes) {
367
        let start = byteData.findString(bytes, "data");
368
        if (start === -1) {
369
            throw Error(this.WaveErrors.data);
370
        }
371
        this.subChunk2Id = "data";
372
        this.subChunk2Size = byteData.fromBytes(
373
            bytes.slice(start + 4, start + 8),
374
            32,
375
            {"be": this.chunkId == "RIFX"})[0];
376
        this.samplesFromBytes_(bytes, start);
377
    }
378
379
    /**
380
     * Find and return the start offset of the data chunk on a wave file.
381
     * @param {Uint8Array} bytes Array of bytes representing the wave file.
382
     * @param {number} start The offset to start reading.
383
     */
384
    samplesFromBytes_(bytes, start) {
385
        let params = {
386
            "signed": this.bitsPerSample == 8 ? false : true,
387
            "be": this.chunkId == "RIFX"
388
        };
389
        if (this.bitsPerSample == 32 && this.audioFormat == 3) {
390
            params.float = true;
391
        }
392
        let samples = bytes.slice(start + 8, start + 8 + this.subChunk2Size);
393
        if (this.bitsPerSample == 4) {
394
            this.samples_ = byteData.fromBytes(samples, 8, params);
395
        } else {
396
            this.samples_ = byteData.fromBytes(samples, this.bitsPerSample, params);
397
        }
398
    }
399
400
    /**
401
     * Validate the input for wav writing.
402
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
403
     *     Should be a int greater than zero smaller than the
404
     *     channel limit according to the bit depth.
405
     * @param {number} sampleRate The sample rate.
0 ignored issues
show
Documentation introduced by
The parameter sampleRate does not exist. Did you maybe forget to remove this comment?
Loading history...
406
     *     Should be a int greater than zero smaller than the
407
     *     channel limit according to the bit depth and number of channels.
408
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
409
     *     Should be one of "8", "16", "24", "32", "32f", "64".
410
     * @throws {Error} If any argument does not meet the criteria.
411
     */
412
    checkWriteInput_() {
413
        this.validateBitDepth_();
414
        this.validateNumChannels_();
415
        this.validateSampleRate_();
416
    }
417
418
    /**
419
     * Validate the bit depth.
420
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
421
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
422
     *     Should be one of "8", "16", "24", "32", "32f", "64".
423
     * @throws {Error} If any argument does not meet the criteria.
424
     */
425
    validateBitDepth_() {
426
        if (!this.headerFormats_[this.bitDepth_]) {
427
            throw new Error(this.WaveErrors.bitDepth);
428
        }
429
        return true;
430
    }
431
432
    /**
433
     * Validate the sample rate value.
434
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
435
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
436
     *     Should be one of "8", "16", "24", "32", "32f", "64".
437
     * @throws {Error} If any argument does not meet the criteria.
438
     */
439
    validateNumChannels_() {
440
        let blockAlign = this.numChannels * this.bitsPerSample / 8;
441
        if (this.numChannels < 1 || blockAlign > 65535) {
442
            throw new Error(this.WaveErrors.numChannels);
443
        }
444
        return true;
445
    }
446
447
    /**
448
     * Validate the sample rate value.
449
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
450
     *     Should be a int greater than zero smaller than the
451
     *     channel limit according to the bit depth.
452
     * @param {number} sampleRate The sample rate.
0 ignored issues
show
Documentation introduced by
The parameter sampleRate does not exist. Did you maybe forget to remove this comment?
Loading history...
453
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
454
     *     Should be one of "8", "16", "24", "32", "32f", "64".
455
     * @throws {Error} If any argument does not meet the criteria.
456
     */
457
    validateSampleRate_() {
458
        let byteRate = this.numChannels *
459
            (this.bitsPerSample / 8) * this.sampleRate;
460
        if (this.sampleRate < 1 || byteRate > 4294967295) {
461
            throw new Error(this.WaveErrors.sampleRate);
462
        }
463
        return true;
464
    }
465
466
    /**
467
     * Split each sample into bytes.
468
     */
469
    samplesToBytes_() {
470
        let params = {"be": this.chunkId == "RIFX"};
471
        if (this.bitsPerSample == 32 && this.audioFormat == 3) {
472
            params.float = true;
473
        }
474
        let bitDepth = this.bitsPerSample == 4 ? 8 : this.bitsPerSample;
475
        this.bytes_ = byteData.toBytes(this.samples_, bitDepth, params);
476
        if (this.bytes_.length % 2) {
477
            this.bytes_.push(0);
478
        }
479
    }
480
481
    /**
482
     * Turn a WaveFile object into a file.
483
     * @return {Uint8Array} The wav file bytes.
484
     */
485
    createWaveFile_() {
486
        let factVal = [];
487
        if (this.factChunkId) {
488
            factVal = byteData.toBytes(this.factChunkId, 8, {"char": true});
489
        }
490
        let options = {"be": this.chunkId == "RIFX"};
491
        return byteData.toBytes(this.chunkId, 8, {"char": true}).concat(
492
            byteData.toBytes([this.chunkSize], 32, options),
493
            byteData.toBytes(this.format, 8, {"char": true}), 
494
            byteData.toBytes(this.subChunk1Id, 8, {"char": true}),
495
            byteData.toBytes([this.subChunk1Size], 32, options),
496
            byteData.toBytes([this.audioFormat], 16, options),
497
            byteData.toBytes([this.numChannels], 16, options),
498
            byteData.toBytes([this.sampleRate], 32, options),
499
            byteData.toBytes([this.byteRate], 32, options),
500
            byteData.toBytes([this.blockAlign], 16, options),
501
            byteData.toBytes([this.bitsPerSample], 16, options),
502
            factVal,
503
            byteData.toBytes(this.subChunk2Id, 8, {"char": true}),
504
            byteData.toBytes([this.subChunk2Size], 32, options),
505
            this.bytes_);
506
    }
507
}
508
509
module.exports.WaveFile = WaveFile;
510