Completed
Branch master (9c6b57)
by Rafael S.
01:32
created

index.js (1 issue)

Severity
1
/*!
2
 * Wavefile
3
 * Handle wave files with 4, 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.enforceFact = enforceFact;
28
        /** @type {boolean} */
29
        this.enforceBext = enforceBext;        
30
        /**
31
         * Header formats.
32
         * @enum {number}
33
         */
34
        this.headerFormats_ = {
35
            "4": 17,
36
            "8": 1,
37
            "16": 1,
38
            "24": 1,
39
            "32": 1,
40
            "32f": 3,
41
            "64": 3
42
        };
43
        /**
44
         * Error messages.
45
         * @enum {string}
46
         */
47
        this.WaveErrors = {
48
            'format': "Not a supported format.",
49
            'wave': "Could not find the 'WAVE' chunk",
50
            'fmt ': "Could not find the 'fmt ' chunk",
51
            'data': "Could not find the 'data' chunk",
52
            'fact': "Could not find the 'fact' chunk",
53
            'bext': "Could not find the 'bext' chunk"
54
        };
55
        if(bytes) {
56
            this.fromBytes(bytes);
57
        }
58
    }
59
60
    /**
61
     * Create a WaveFile object based on the arguments passed.
62
     * @param {number} numChannels The number of channels
63
     *     (Ints like 1 for mono, 2 stereo and so on).
64
     * @param {number} sampleRate The sample rate.
65
     *     Integer numbers like 8000, 44100, 48000, 96000, 192000.
66
     * @param {string} bitDepth The audio bit depth.
67
     *     One of "8", "16", "24", "32", "32f", "64".
68
     * @param {!Array<number>} samples Array of samples to be written.
69
     *     Samples must be in the correct range according to the bit depth.
70
     *     Samples of multi-channel data .
71
     */
72
    fromScratch(numChannels, sampleRate, bitDepth, samples) {
73
        let bytes = parseInt(bitDepth, 10) / 8;
74
        this.chunkId = "RIFF";
75
        this.chunkSize = 36 + samples.length * bytes;
76
        this.format = "WAVE";
77
        this.subChunk1Id = "fmt ";
78
        this.subChunk1Size = 16;
79
        this.audioFormat = this.headerFormats_[bitDepth];
80
        this.numChannels = numChannels;
81
        this.sampleRate = sampleRate;
82
        this.byteRate = (numChannels * bytes) * sampleRate;
83
        this.blockAlign = numChannels * bytes;
84
        this.bitsPerSample = parseInt(bitDepth, 10);
85
        this.subChunk2Id = "data";
86
        this.subChunk2Size = samples.length * bytes;
87
        this.samples_ = samples;
88
        this.bitDepth_ = bitDepth;
89
    }
90
91
    /**
92
     * Read a wave file from an array of bytes.
93
     * @param {Uint8Array} bytes The wave file as an array of bytes.
94
     */
95
    fromBytes(bytes) {
96
        this.readRIFFChunk_(bytes);
97
        this.readWAVEChunk_(bytes);
98
        this.readFmtChunk_(bytes);
99
        try {
100
            this.readFactChunk_(bytes);
101
        }catch(err) {
102
            if (this.enforceFact) {
103
                throw err;
104
            }
105
        }
106
        try {
107
            this.readBextChunk_(bytes);
108
        }catch(err) {
109
            if (this.enforceBext) {
110
                throw err;
111
            }
112
        }
113
        this.readDataChunk_(bytes);
114
    }
115
116
    /**
117
     * Turn the wave file represented by an object of this class into
118
     * a array of bytes.
119
     * @return {Uint8Array}
120
     */
121
    toBytes() {
122
        this.checkWriteInput_(this.numChannels, this.sampleRate, this.bitDepth_);
123
        this.samplesToBytes_();
124
        return new Uint8Array(this.createWaveFile_());
125
    }
126
127
    /**
128
     * Interleave multi-channel samples.
129
     */
130
    interleave() {
131
        let finalSamples = [];
132
        let i;
133
        let j;
134
        let numChannels = this.samples_[0].length;
135
        for (i = 0; i < numChannels; i++) {
136
            for (j = 0; j < this.samples_.length; j++) {
137
                finalSamples.push(this.samples_[j][i]);
138
            }
139
        }
140
        this.samples_ = finalSamples;
141
    }
142
143
    /**
144
     * De-interleave samples into multiple channels.
145
     */
146
    deInterleave() {
147
        let finalSamples = [];
148
        let i;
149
        for (i = 0; i < this.numChannels; i++) {
150
            finalSamples[i] = [];
151
        }
152
        i = 0;
153
        let j;
154
        while (i < this.samples_.length) {
155
            for (j = 0; j < this.numChannels; j++) {
156
                finalSamples[j].push(this.samples_[i+j]);
157
            }
158
            i += j;
159
        }
160
        this.samples_ = finalSamples;
161
    }
162
163
    /**
164
     * Read the RIFF chunk a wave file.
165
     * @param {Uint8Array} bytes an array representing the wave file.
166
     * @throws {Error} If no "RIFF" chunk is found.
167
     */
168
    readRIFFChunk_(bytes) {
169
        this.chunkId = byteData.stringFromBytes(bytes.slice(0, 4));
170
        if (this.chunkId != "RIFF") {
171
            throw Error(this.WaveErrors.format);
172
        }
173
        this.chunkSize = byteData.intFrom4Bytes(
174
            bytes.slice(4, 8))[0];
175
    }
176
177
    /**
178
     * Read the WAVE chunk of a wave file.
179
     * @param {Uint8Array} bytes an array representing the wave file.
180
     * @throws {Error} If no "WAVE" chunk is found.
181
     */
182
    readWAVEChunk_(bytes) {
183
        let start = byteData.findString(bytes, "WAVE");
184
        if (start === -1) {
185
            throw Error(this.WaveErrors.wave);
186
        }
187
        this.format = byteData.stringFromBytes(
188
                bytes.slice(start, start + 4));
189
    }
190
191
    /**
192
     * Read the "fmt " chunk of a wave file.
193
     * @param {Uint8Array} bytes an array representing the wave file.
194
     * @throws {Error} If no "fmt " chunk is found.
195
     */
196
    readFmtChunk_(bytes) {
197
        let start = byteData.findString(bytes, "fmt ");
198
        if (start === -1) {
199
            throw Error(this.WaveErrors['fmt ']);
200
        }
201
        this.subChunk1Id = byteData.stringFromBytes(
202
            bytes.slice(start, start + 4));
203
        this.subChunk1Size = byteData.uIntFrom4Bytes(
204
            bytes.slice(start + 4, start + 8))[0];
205
        this.audioFormat = byteData.uIntFrom2Bytes(
206
            bytes.slice(start + 8, start + 10))[0];
207
        this.numChannels = byteData.uIntFrom2Bytes(
208
            bytes.slice(start + 10, start + 12))[0];
209
        this.sampleRate = byteData.uIntFrom4Bytes(
210
            bytes.slice(start + 12, start + 16))[0];
211
        this.byteRate = byteData.uIntFrom4Bytes(
212
            bytes.slice(start + 16, start + 20))[0];
213
        this.blockAlign = byteData.uIntFrom2Bytes(
214
            bytes.slice(start + 20, start + 22))[0];
215
        this.bitsPerSample = byteData.uIntFrom2Bytes(
216
            bytes.slice(start + 22, start + 24))[0];
217
        // The bitDepth_ is used internally to determine
218
        // wich function use to read the samples
219
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
220
            this.bitDepth_ = "32f";
221
        }else {
222
            this.bitDepth_ = this.bitsPerSample.toString();
223
        }
224
    }
225
226
    /**
227
     * Read the "fact" chunk of a wave file.
228
     * @param {Uint8Array} bytes an array representing the wave file.
229
     * @throws {Error} If no "fact" chunk is found.
230
     */
231
    readFactChunk_(bytes) {
232
        let start = byteData.findString(bytes, "fact");
233
        if (start === -1) {
234
            throw Error(this.WaveErrors.fact);
235
        }else {
236
            this.factChunkId = byteData.stringFromBytes(
237
                bytes.slice(start, start + 4));
238
            //this.factChunkSize = byteData.uIntFrom4Bytes(
239
            //    bytes.slice(start + 4, start + 8));
240
            //this.dwSampleLength = byteData.uIntFrom4Bytes(
241
            //    bytes.slice(start + 8, start + 12));
242
        }
243
    }
244
245
    /**
246
     * Read the "bext" chunk of a wave file.
247
     * @param {Uint8Array} bytes an array representing the wave file.
248
     * @throws {Error} If no "bext" chunk is found.
249
     */
250
    readBextChunk_(bytes) {
251
        let start = byteData.findString(bytes, "bext");
252
        if (start === -1) {
253
            throw Error(this.WaveErrors.bext);
254
        }else {
255
            this.bextChunkId = byteData.stringFromBytes(
256
                bytes.slice(start, start + 4));
257
        }
258
    }
259
260
    /**
261
     * Read the "data" chunk of a wave file.
262
     * @param {Uint8Array} bytes an array representing the wave file.
263
     * @throws {Error} If no "data" chunk is found.
264
     */
265
    readDataChunk_(bytes) {
266
        let start = byteData.findString(bytes, "data");
267
        if (start === -1) {
268
            throw Error(this.WaveErrors.data);
269
        }
270
        this.subChunk2Id = byteData.stringFromBytes(
271
            bytes.slice(start, start + 4));
272
        this.subChunk2Size = byteData.intFrom4Bytes(
273
            bytes.slice(start + 4, start + 8))[0];
274
        this.samplesFromBytes_(bytes, start);
275
    }
276
277
    /**
278
     * Find and return the start offset of the data chunk on a wave file.
279
     * @param {Uint8Array} bytes Array of bytes representing the wave file.
280
     * @param {number} start The offset to start reading.
281
     */
282
    samplesFromBytes_(bytes, start) {
283
        let readingFunctions = {
284
            "4": byteData.intFrom1Byte,
285
            "8": byteData.uIntFrom1Byte,
286
            "16": byteData.intFrom2Bytes,
287
            "24": byteData.intFrom3Bytes,
288
            "32": byteData.intFrom4Bytes,
289
            "32f": byteData.floatFrom4Bytes,
290
            "64" : byteData.floatFrom8Bytes
291
        };
292
        let samples = bytes.slice(start + 8, start + 8 + this.subChunk2Size);
293
        this.samples_ = readingFunctions[this.bitDepth_](samples);
294
    }
295
296
    /**
297
     * Validate the input for wav writing.
298
     * @param {number} numChannels The number of channels
299
     *     Should be a int greater than zero smaller than the
300
     *     channel limit according to the bit depth.
301
     * @param {number} sampleRate The sample rate.
302
     *     Should be a int greater than zero smaller than the
303
     *     channel limit according to the bit depth and number of channels.
304
     * @param {string} bitDepth The audio bit depth.
305
     *     Should be one of "8", "16", "24", "32", "32f", "64".
306
     * @throws {Error} If any argument does not meet the criteria.
307
     */
308
    checkWriteInput_(numChannels, sampleRate, bitDepth) {
309
        if (typeof bitDepth !== "string" ||
310
            !(bitDepth in this.headerFormats_)) {
311
            throw new Error("Invalid bit depth.");
312
        }
313
        this.validateNumChannels_(numChannels, bitDepth);
314
        this.validateSampleRate_(numChannels, sampleRate, bitDepth);
315
    }
316
317
    /**
318
     * Validate the sample rate value.
319
     * @param {number} numChannels The number of channels
320
     * @param {string} bitDepth The audio bit depth.
321
     *     Should be one of "8", "16", "24", "32", "32f", "64".
322
     * @throws {Error} If any argument does not meet the criteria.
323
     */
324
    validateNumChannels_(numChannels, bitDepth) {
325
        let errorText = "Invalid number of channels.";
326
        let validChannnelNumber = false;
327
        let blockAlign = numChannels * (parseInt(bitDepth, 10) / 8);
328
        if (blockAlign <= 65535) {
329
            validChannnelNumber = true;
330
        }
331
        if (numChannels < 1 || !validChannnelNumber ||
332
            !(typeof numChannels==="number" && (numChannels%1)===0)) {
333
            throw new Error(errorText);
334
        }
335
        return true;
336
    }
337
338
    /**
339
     * Validate the sample rate value.
340
     * @param {number} numChannels The number of channels
341
     *     Should be a int greater than zero smaller than the
342
     *     channel limit according to the bit depth.
343
     * @param {number} sampleRate The sample rate.
344
     * @param {string} bitDepth The audio bit depth.
345
     *     Should be one of "8", "16", "24", "32", "32f", "64".
346
     * @throws {Error} If any argument does not meet the criteria.
347
     */
348
    validateSampleRate_(numChannels, sampleRate, bitDepth) {
349
        let errorText = "Invalid sample rate.";
350
        let validSampleRateValue = false;
351
        let byteRate = numChannels * (parseInt(bitDepth, 10) / 8) * sampleRate;
352
        if (byteRate <= 4294967295) {
353
            validSampleRateValue = true;
354
        }
355
        if (sampleRate < 1 || !validSampleRateValue ||
356
            !(typeof sampleRate==="number" && (sampleRate%1)===0)) {
357
            throw new Error(errorText);
358
        }
359
        return true;
360
    }
361
362
    /**
363
     * Split each sample into bytes.
364
     */
365
    samplesToBytes_() {
366
        let writingFunctions = {
367
            "4": byteData.intTo1Byte,
368
            "8": byteData.intTo1Byte,
369
            "16": byteData.intTo2Bytes,
370
            "24": byteData.intTo3Bytes,
371
            "32": byteData.intTo4Bytes,
372
            "32f": byteData.floatTo4Bytes,
373
            "64" : byteData.floatTo8Bytes
374
        };
375
        // FIXME byte-data should not modify the original array
376
        let s = [];
377
        for (let l=0; l<this.samples_.length; l++) {
378
            s[l] = this.samples_[l];
379
        }
380
        this.bytes_ = writingFunctions[this.bitDepth_](s);
381
        if (this.bytes_.length % 2) {
382
            this.bytes_.push(0);
383
        }
384
    }
385
386
    /**
387
     * Turn a WaveFile object into a file.
388
     * @return {Uint8Array} The wav file bytes.
389
     */
390
    createWaveFile_() {
391
        let factVal = [];
392
        let cbSizeVal = [];
0 ignored issues
show
The variable cbSizeVal seems to be never used. Consider removing it.
Loading history...
393
        if (this.factChunkId) {
394
            factVal= byteData.stringToBytes(this.factChunkId);
395
        }
396
        return byteData.stringToBytes(this.chunkId).concat(
397
            byteData.intTo4Bytes([this.chunkSize]),
398
            byteData.stringToBytes(this.format), 
399
            byteData.stringToBytes(this.subChunk1Id),
400
            byteData.intTo4Bytes([this.subChunk1Size]),
401
            byteData.intTo2Bytes([this.audioFormat]),
402
            byteData.intTo2Bytes([this.numChannels]),
403
            byteData.intTo4Bytes([this.sampleRate]),
404
            byteData.intTo4Bytes([this.byteRate]),
405
            byteData.intTo2Bytes([this.blockAlign]),
406
            byteData.intTo2Bytes([this.bitsPerSample]),
407
            factVal,
408
            byteData.stringToBytes(this.subChunk2Id),
409
            byteData.intTo4Bytes([this.subChunk2Size]),
410
            this.bytes_);
411
    }
412
}
413
414
module.exports.WaveFile = WaveFile;
415