Passed
Branch master (4064de)
by Rafael S.
02:04 queued 40s
created
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
     *     Ints like 8000, 44100, 48000, 192000, etc.
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 must be interleaved.
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
     * Read the RIFF chunk a wave file.
129
     * @param {Uint8Array} bytes an array representing the wave file.
130
     * @throws {Error} If no "RIFF" chunk is found.
131
     */
132
    readRIFFChunk_(bytes) {
133
        this.chunkId = byteData.stringFromBytes(bytes.slice(0, 4));
134
        if (this.chunkId != "RIFF") {
135
            throw Error(this.WaveErrors.format);
136
        }
137
        this.chunkSize = byteData.intFrom4Bytes(
138
            bytes.slice(4, 8))[0];
139
    }
140
141
    /**
142
     * Read the WAVE chunk of a wave file.
143
     * @param {Uint8Array} bytes an array representing the wave file.
144
     * @throws {Error} If no "WAVE" chunk is found.
145
     */
146
    readWAVEChunk_(bytes) {
147
        let start = byteData.findString(bytes, "WAVE");
148
        if (start === -1) {
149
            throw Error(this.WaveErrors.wave);
150
        }
151
        this.format = byteData.stringFromBytes(
152
                bytes.slice(start, start + 4));
153
    }
154
155
    /**
156
     * Read the "fmt " chunk of a wave file.
157
     * @param {Uint8Array} bytes an array representing the wave file.
158
     * @throws {Error} If no "fmt " chunk is found.
159
     */
160
    readFmtChunk_(bytes) {
161
        let start = byteData.findString(bytes, "fmt ");
162
        if (start === -1) {
163
            throw Error(this.WaveErrors['fmt ']);
164
        }
165
        this.subChunk1Id = byteData.stringFromBytes(
166
            bytes.slice(start, start + 4));
167
        this.subChunk1Size = byteData.uIntFrom4Bytes(
168
            bytes.slice(start + 4, start + 8))[0];
169
        this.audioFormat = byteData.uIntFrom2Bytes(
170
            bytes.slice(start + 8, start + 10))[0];
171
        this.numChannels = byteData.uIntFrom2Bytes(
172
            bytes.slice(start + 10, start + 12))[0];
173
        this.sampleRate = byteData.uIntFrom4Bytes(
174
            bytes.slice(start + 12, start + 16))[0];
175
        this.byteRate = byteData.uIntFrom4Bytes(
176
            bytes.slice(start + 16, start + 20))[0];
177
        this.blockAlign = byteData.uIntFrom2Bytes(
178
            bytes.slice(start + 20, start + 22))[0];
179
        this.bitsPerSample = byteData.uIntFrom2Bytes(
180
            bytes.slice(start + 22, start + 24))[0];
181
        // The bitDepth_ is used internally to determine
182
        // wich function use to read the samples
183
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
184
            this.bitDepth_ = "32f";
185
        }else {
186
            this.bitDepth_ = this.bitsPerSample.toString();
187
        }
188
    }
189
190
    /**
191
     * Read the "fact" chunk of a wave file.
192
     * @param {Uint8Array} bytes an array representing the wave file.
193
     * @throws {Error} If no "fact" chunk is found.
194
     */
195
    readFactChunk_(bytes) {
196
        let start = byteData.findString(bytes, "fact");
197
        if (start === -1) {
198
            throw Error(this.WaveErrors.fact);
199
        }else {
200
            this.factChunkId = byteData.stringFromBytes(
201
                bytes.slice(start, start + 4));
202
            //this.factChunkSize = byteData.uIntFrom4Bytes(
203
            //    bytes.slice(start + 4, start + 8));
204
            //this.dwSampleLength = byteData.uIntFrom4Bytes(
205
            //    bytes.slice(start + 8, start + 12));
206
        }
207
    }
208
209
    /**
210
     * Read the "bext" chunk of a wave file.
211
     * @param {Uint8Array} bytes an array representing the wave file.
212
     * @throws {Error} If no "bext" chunk is found.
213
     */
214
    readBextChunk_(bytes) {
215
        let start = byteData.findString(bytes, "bext");
216
        if (start === -1) {
217
            throw Error(this.WaveErrors.bext);
218
        }else {
219
            this.bextChunkId = byteData.stringFromBytes(
220
                bytes.slice(start, start + 4));
221
        }
222
    }
223
224
    /**
225
     * Read the "data" chunk of a wave file.
226
     * @param {Uint8Array} bytes an array representing the wave file.
227
     * @throws {Error} If no "data" chunk is found.
228
     */
229
    readDataChunk_(bytes) {
230
        let start = byteData.findString(bytes, "data");
231
        if (start === -1) {
232
            throw Error(this.WaveErrors.data);
233
        }
234
        this.subChunk2Id = byteData.stringFromBytes(
235
            bytes.slice(start, start + 4));
236
        this.subChunk2Size = byteData.intFrom4Bytes(
237
            bytes.slice(start + 4, start + 8))[0];
238
        this.samplesFromBytes_(bytes, start);
239
    }
240
241
    /**
242
     * Find and return the start offset of the data chunk on a wave file.
243
     * @param {Uint8Array} bytes Array of bytes representing the wave file.
244
     * @param {number} start The offset to start reading.
245
     */
246
    samplesFromBytes_(bytes, start) {
247
        let readingFunctions = {
248
            "4": byteData.intFrom1Byte,
249
            "8": byteData.uIntFrom1Byte,
250
            "16": byteData.intFrom2Bytes,
251
            "24": byteData.intFrom3Bytes,
252
            "32": byteData.intFrom4Bytes,
253
            "32f": byteData.floatFrom4Bytes,
254
            "64" : byteData.floatFrom8Bytes
255
        };
256
        let samples = bytes.slice(start + 8, start + 8 + this.subChunk2Size);
257
        this.samples_ = readingFunctions[this.bitDepth_](samples);
258
    }
259
260
    /**
261
     * Validate the input for wav writing.
262
     * @param {number} numChannels The number of channels
263
     *     Should be a int greater than zero smaller than the
264
     *     channel limit according to the bit depth.
265
     * @param {number} sampleRate The sample rate.
266
     *     Should be a int greater than zero smaller than the
267
     *     channel limit according to the bit depth and number of channels.
268
     * @param {string} bitDepth The audio bit depth.
269
     *     Should be one of "8", "16", "24", "32", "32f", "64".
270
     * @throws {Error} If any argument does not meet the criteria.
271
     */
272
    checkWriteInput_(numChannels, sampleRate, bitDepth) {
273
        if (typeof bitDepth !== "string" ||
274
            !(bitDepth in this.headerFormats_)) {
275
            throw new Error("Invalid bit depth.");
276
        }
277
        this.validateNumChannels_(numChannels, bitDepth);
278
        this.validateSampleRate_(numChannels, sampleRate, bitDepth);
279
    }
280
281
    /**
282
     * Validate the sample rate value.
283
     * @param {number} numChannels The number of channels
284
     * @param {string} bitDepth The audio bit depth.
285
     *     Should be one of "8", "16", "24", "32", "32f", "64".
286
     * @throws {Error} If any argument does not meet the criteria.
287
     */
288
    validateNumChannels_(numChannels, bitDepth) {
289
        let errorText = "Invalid number of channels.";
290
        let validChannnelNumber = false;
291
        let blockAlign = numChannels * (parseInt(bitDepth, 10) / 8);
292
        if (blockAlign <= 65535) {
293
            validChannnelNumber = true;
294
        }
295
        if (numChannels < 1 || !validChannnelNumber ||
296
            !(typeof numChannels==="number" && (numChannels%1)===0)) {
297
            throw new Error(errorText);
298
        }
299
        return true;
300
    }
301
302
    /**
303
     * Validate the sample rate value.
304
     * @param {number} numChannels The number of channels
305
     *     Should be a int greater than zero smaller than the
306
     *     channel limit according to the bit depth.
307
     * @param {number} sampleRate The sample rate.
308
     * @param {string} bitDepth The audio bit depth.
309
     *     Should be one of "8", "16", "24", "32", "32f", "64".
310
     * @throws {Error} If any argument does not meet the criteria.
311
     */
312
    validateSampleRate_(numChannels, sampleRate, bitDepth) {
313
        let errorText = "Invalid sample rate.";
314
        let validSampleRateValue = false;
315
        let byteRate = numChannels * (parseInt(bitDepth, 10) / 8) * sampleRate;
316
        if (byteRate <= 4294967295) {
317
            validSampleRateValue = true;
318
        }
319
        if (sampleRate < 1 || !validSampleRateValue ||
320
            !(typeof sampleRate==="number" && (sampleRate%1)===0)) {
321
            throw new Error(errorText);
322
        }
323
        return true;
324
    }
325
326
    /**
327
     * Split each sample into bytes.
328
     */
329
    samplesToBytes_() {
330
        let writingFunctions = {
331
            "4": byteData.intTo1Byte,
332
            "8": byteData.intTo1Byte,
333
            "16": byteData.intTo2Bytes,
334
            "24": byteData.intTo3Bytes,
335
            "32": byteData.intTo4Bytes,
336
            "32f": byteData.floatTo4Bytes,
337
            "64" : byteData.floatTo8Bytes
338
        };
339
        // FIXME byte-data should not modify the original array
340
        let s = [];
341
        for (let l=0; l<this.samples_.length; l++) {
342
            s[l] = this.samples_[l];
343
        }
344
        this.bytes_ = writingFunctions[this.bitDepth_](s);
345
        if (this.bytes_.length % 2) {
346
            this.bytes_.push(0);
347
        }
348
    }
349
350
    /**
351
     * Turn a WaveFile object into a file.
352
     * @return {Uint8Array} The wav file bytes.
353
     */
354
    createWaveFile_() {
355
        let factVal = [];
356
        let cbSizeVal = [];
0 ignored issues
show
The variable cbSizeVal seems to be never used. Consider removing it.
Loading history...
357
        if (this.factChunkId) {
358
            factVal= byteData.stringToBytes(this.factChunkId);
359
        }
360
        return byteData.stringToBytes(this.chunkId).concat(
361
            byteData.intTo4Bytes([this.chunkSize]),
362
            byteData.stringToBytes(this.format), 
363
            byteData.stringToBytes(this.subChunk1Id),
364
            byteData.intTo4Bytes([this.subChunk1Size]),
365
            byteData.intTo2Bytes([this.audioFormat]),
366
            byteData.intTo2Bytes([this.numChannels]),
367
            byteData.intTo4Bytes([this.sampleRate]),
368
            byteData.intTo4Bytes([this.byteRate]),
369
            byteData.intTo2Bytes([this.blockAlign]),
370
            byteData.intTo2Bytes([this.bitsPerSample]),
371
            factVal,
372
            byteData.stringToBytes(this.subChunk2Id),
373
            byteData.intTo4Bytes([this.subChunk2Size]),
374
            this.bytes_);
375
    }
376
}
377
378
module.exports.WaveFile = WaveFile;
379