Passed
Push — master ( 7571d8...92a305 )
by Rafael S.
01:49
created

index.js (2 issues)

Severity
1
/*!
2
 * wavefile
3
 * Read & write wave files with 4, 8, 11, 12, 16, 20, 24, 32 & 64-bit data.
4
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
5
 * https://github.com/rochars/wavefile
6
 *
7
 */
8
9
/** @private */
10
const WAVE_ERRORS = require("./src/wave-errors");
11
/** @private */
12
const bitDepth_ = require("bitdepth");
13
/** @private */
14
const riffChunks_ = require("riff-chunks");
15
/** @private */
16
const imaadpcm_ = require("imaadpcm");
17
/** @private */
18
const alawmulaw_ = require("alawmulaw");
19
/** @private */
20
const WaveFileReaderWriter = require("./src/wavefile-reader-writer");
21
22
/**
23
 * Class representing a wav file.
24
 * @extends WaveFileReaderWriter
25
 */
26
class WaveFile extends WaveFileReaderWriter {
27
28
    /**
29
     * @param {Uint8Array|Array<number>} bytes A wave file buffer.
30
     * @throws {Error} If no "RIFF" chunk is found.
31
     * @throws {Error} If no "fmt " chunk is found.
32
     * @throws {Error} If no "fact" chunk is found and "fact" is needed.
33
     * @throws {Error} If no "data" chunk is found.
34
     */
35
    constructor(bytes) {
36
        super();
37
        if(bytes) {
38
            this.fromBuffer(bytes);
39
        }
40
    }
41
42
    /**
43
     * Set up a WaveFile object based on the arguments passed.
44
     * @param {number} numChannels The number of channels
45
     *     (Integer numbers: 1 for mono, 2 stereo and so on).
46
     * @param {number} sampleRate The sample rate.
47
     *     Integer numbers like 8000, 44100, 48000, 96000, 192000.
48
     * @param {string} bitDepth The audio bit depth.
49
     *     One of "4", "8", "8a", "8m", "16", "24", "32", "32f", "64"
50
     *     or any value between "8" and "32".
51
     * @param {Array<number>} samples Array of samples to be written.
52
     *     The samples must be in the correct range according to the
53
     *     bit depth.
54
     * @throws {Error} If any argument does not meet the criteria.
55
     */
56
    fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) {
57
        if (!options.container) {
58
            options.container = "RIFF";
59
        }
60
        // closest nuber of bytes if not / 8
61
        let numBytes = (((parseInt(bitDepth, 10) - 1) | 7) + 1) / 8;
62
        // Clear the fact chunk
63
        this.clearFactChunk_();
64
        // Normal PCM file header
65
        this.chunkSize = 36 + samples.length * numBytes;
66
        this.fmtChunkSize = 16;
67
        this.byteRate = (numChannels * numBytes) * sampleRate;
68
        this.blockAlign = numChannels * numBytes;
69
        this.chunkId = options.container;
70
        this.format = "WAVE";
71
        this.fmtChunkId = "fmt ";
72
        this.audioFormat = this.headerFormats_[bitDepth] ? this.headerFormats_[bitDepth] : 65534;
73
        this.numChannels = numChannels;
74
        this.sampleRate = sampleRate;
75
        this.bitsPerSample = parseInt(bitDepth, 10);
76
        this.dataChunkId = "data";
77
        this.dataChunkSize = samples.length * numBytes;
78
        this.samples = samples;
79
        this.bitDepth = bitDepth;
80
        // IMA ADPCM header
81
        if (bitDepth == "4") {
82
            this.chunkSize = 44 + samples.length;
83
            this.fmtChunkSize = 20;
84
            this.byteRate = 4055;
85
            this.blockAlign = 256;
86
            this.bitsPerSample = 4;
87
            this.dataChunkSize = samples.length;
88
            this.cbSize = 2;
89
            this.validBitsPerSample = 505;
90
            this.factChunkId = "fact";
91
            this.factChunkSize = 4;
92
            this.dwSampleLength = samples.length * 2;
93
        }
94
        // A-Law and mu-Law header
95
        if (bitDepth == "8a" || bitDepth == "8m") {
96
            this.chunkSize = 44 + samples.length;
97
            this.fmtChunkSize = 20;
98
            this.cbSize = 2;
99
            this.validBitsPerSample = 8;
100
            this.factChunkId = "fact";
101
            this.factChunkSize = 4;
102
            this.dwSampleLength = samples.length;
103
        }
104
        // WAVE_FORMAT_EXTENSIBLE
105
        if (parseInt(bitDepth, 10) > 8 && (parseInt(bitDepth, 10) % 8)) {
106
            this.chunkSize = 36 + 24 + samples.length * numBytes;
107
            this.fmtChunkSize = 40;
108
            this.bitsPerSample = (((parseInt(bitDepth, 10) - 1) | 7) + 1);
109
            this.cbSize = 22;
110
            this.validBitsPerSample = parseInt(bitDepth, 10);
111
            this.dwChannelMask = 0;
112
            // subformat 128-bit GUID as 4 32-bit values
113
            // only supports uncompressed integer PCM samples
114
            this.subformat1 = 1;
115
            this.subformat2 = 1048576;
116
            this.subformat3 = 2852126848;
117
            this.subformat4 = 1905997824;
118
        }
119
    }
120
121
    /**
122
     * Init a WaveFile object from a byte buffer.
123
     * @param {Uint8Array|Array<number>} bytes The buffer.
124
     * @throws {Error} If no "RIFF" chunk is found.
125
     * @throws {Error} If no "fmt " chunk is found.
126
     * @throws {Error} If no "fact" chunk is found and "fact" is needed.
127
     * @throws {Error} If no "data" chunk is found.
128
     */
129
    fromBuffer(bytes) {
130
        this.readRIFFChunk_(bytes);
131
        let bigEndian = this.chunkId == "RIFX";
132
        let chunk = riffChunks_.read(bytes, bigEndian);
133
        this.readFmtChunk_(chunk.subChunks);
134
        this.readFactChunk_(chunk.subChunks);
135
        this.readBextChunk_(chunk.subChunks);
136
        this.readCueChunk_(chunk.subChunks);
137
        this.readDataChunk_(
138
                chunk.subChunks,
139
                {"be": bigEndian, "single": true}
140
            );
141
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
142
            this.bitDepth = "32f";
143
        }else {
144
            this.bitDepth = this.bitsPerSample.toString();
145
        }
146
    }
147
148
    /**
149
     * Return a byte buffer representig the WaveFile object as a wav file.
150
     * @return {Uint8Array}
151
     * @throws {Error} If any property of the object appears invalid.
152
     */
153
    toBuffer() {
154
        this.checkWriteInput_();
155
        return new Uint8Array(this.createWaveFile_());
156
    }
157
158
    /**
159
     * Turn the file to RIFF.
160
     */
161
    toRIFF() {
162
        this.chunkId = "RIFF";
163
        this.LEorBE_();
164
    }
165
166
    /**
167
     * Turn the file to RIFX.
168
     */
169
    toRIFX() {
170
        this.chunkId = "RIFX";
171
        this.LEorBE_();
172
    }
173
174
    /**
175
     * Change the bit depth of the samples.
176
     * @param {string} bitDepth The new bit depth of the samples.
177
     *      One of "8" ... "32" (integers), "32f" or "64" (floats)
178
     * @param {boolean} changeResolution A boolean indicating if the
179
     *      resolution of samples should be actually changed or not.
180
     * @throws {Error} If the bit depth is invalid.
181
     */
182
    toBitDepth(bitDepth, changeResolution=true) {
183
        if (!changeResolution) {
184
            let toBitDepth = this.realBitDepth_(bitDepth);
0 ignored issues
show
The variable toBitDepth seems to be never used. Consider removing it.
Loading history...
185
            let thisBitDepth = this.realBitDepth_(this.bitDepth);
0 ignored issues
show
The variable thisBitDepth seems to be never used. Consider removing it.
Loading history...
186
        }
187
        bitDepth_.toBitDepth(this.samples, this.bitDepth, bitDepth);
188
        this.fromScratch(
189
            this.numChannels,
190
            this.sampleRate,
191
            bitDepth,
192
            this.samples,
193
            {"container": this.chunkId}
194
        );
195
    }
196
197
    /**
198
     * Interleave multi-channel samples.
199
     */
200
    interleave() {
201
        let finalSamples = [];
202
        let numChannels = this.samples[0].length;
203
        for (let i = 0; i < numChannels; i++) {
204
            for (let j = 0; j < this.samples.length; j++) {
205
                finalSamples.push(this.samples[j][i]);
206
            }
207
        }
208
        this.samples = finalSamples;
209
    }
210
211
    /**
212
     * De-interleave samples into multiple channels.
213
     */
214
    deInterleave() {
215
        let finalSamples = [];
216
        let i;
217
        for (i = 0; i < this.numChannels; i++) {
218
            finalSamples[i] = [];
219
        }
220
        i = 0;
221
        let j;
222
        while (i < this.samples.length) {
223
            for (j = 0; j < this.numChannels; j++) {
224
                finalSamples[j].push(this.samples[i+j]);
225
            }
226
            i += j;
227
        }
228
        this.samples = finalSamples;
229
    }
230
231
    /**
232
     * Encode a 16-bit wave file as 4-bit IMA ADPCM.
233
     */
234
    toIMAADPCM() {
235
        this.fromScratch(
236
            this.numChannels,
237
            this.sampleRate,
238
            "4",
239
            imaadpcm_.encode(this.samples),
240
            {"container": this.chunkId}
241
        );
242
    }
243
244
    /**
245
     * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
246
     */
247
    fromIMAADPCM() {
248
        this.fromScratch(
249
            this.numChannels,
250
            this.sampleRate,
251
            "16",
252
            imaadpcm_.decode(this.samples, this.blockAlign),
253
            {"container": this.chunkId}
254
        );
255
    }
256
257
    /**
258
     * Encode 16-bit wave file as 8-bit A-Law.
259
     */
260
    toALaw() {
261
        this.fromScratch(
262
            this.numChannels,
263
            this.sampleRate,
264
            "8a",
265
            alawmulaw_.alaw.encode(this.samples),
266
            {"container": this.chunkId}
267
        );
268
    }
269
270
    /**
271
     * Decode a 8-bit A-Law wave file into a 16-bit wave file.
272
     */
273
    fromALaw() {
274
        this.fromScratch(
275
            this.numChannels,
276
            this.sampleRate,
277
            "16",
278
            alawmulaw_.alaw.decode(this.samples),
279
            {"container": this.chunkId}
280
        );
281
    }
282
283
    /**
284
     * Encode 16-bit wave file as 8-bit mu-Law.
285
     */
286
    toMuLaw() {
287
        this.fromScratch(
288
            this.numChannels,
289
            this.sampleRate,
290
            "8m",
291
            alawmulaw_.mulaw.encode(this.samples),
292
            {"container": this.chunkId}
293
        );
294
    }
295
296
    /**
297
     * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
298
     */
299
    fromMuLaw() {
300
        this.fromScratch(
301
            this.numChannels,
302
            this.sampleRate,
303
            "16",
304
            alawmulaw_.mulaw.decode(this.samples),
305
            {"container": this.chunkId}
306
        );
307
    }
308
309
    /**
310
     * Get the closest greater number of bits for a number of bits that
311
     * do not fill a full sequence of bytes.
312
     * @param {string} bitDepth The bit depth.
313
     * @return {string}
314
     */
315
    realBitDepth_(bitDepth) {
316
        if (bitDepth != "32f") {
317
            bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1)
318
                .toString();
319
        }
320
        return bitDepth;
321
    }
322
323
    /**
324
     * Validate the input for wav writing.
325
     * @throws {Error} If any property of the object appears invalid.
326
     * @private
327
     */
328
    checkWriteInput_() {
329
        this.validateBitDepth_();
330
        this.validateNumChannels_();
331
        this.validateSampleRate_();
332
    }
333
334
    /**
335
     * Validate the bit depth.
336
     * @throws {Error} If bit depth is invalid.
337
     * @private
338
     */
339
    validateBitDepth_() {
340
        if (!this.headerFormats_[this.bitDepth]) {
341
            if (parseInt(this.bitDepth, 10) > 8 &&
342
                    parseInt(this.bitDepth, 10) < 32) {
343
                return true;
344
            }
345
            throw new Error(WAVE_ERRORS.bitDepth);
346
        }
347
        return true;
348
    }
349
350
    /**
351
     * Validate the number of channels.
352
     * @throws {Error} If the number of channels is invalid.
353
     * @private
354
     */
355
    validateNumChannels_() {
356
        let blockAlign = this.numChannels * this.bitsPerSample / 8;
357
        if (this.numChannels < 1 || blockAlign > 65535) {
358
            throw new Error(WAVE_ERRORS.numChannels);
359
        }
360
        return true;
361
    }
362
363
    /**
364
     * Validate the sample rate value.
365
     * @throws {Error} If the sample rate is invalid.
366
     * @private
367
     */
368
    validateSampleRate_() {
369
        let byteRate = this.numChannels *
370
            (this.bitsPerSample / 8) * this.sampleRate;
371
        if (this.sampleRate < 1 || byteRate > 4294967295) {
372
            throw new Error(WAVE_ERRORS.sampleRate);
373
        }
374
        return true;
375
    }
376
377
    /**
378
     * Reset the attributes related to the "fact" chunk.
379
     * @private
380
     */
381
    clearFactChunk_() {
382
        this.cbSize = 0;
383
        this.validBitsPerSample = 0;
384
        this.factChunkId = "";
385
        this.factChunkSize = 0;
386
        this.dwSampleLength = 0;
387
    }
388
}
389
390
module.exports = WaveFile;
391