Passed
Branch master (ea9505)
by Rafael S.
01:23
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
     * Read a wave file from an array of bytes.
62
     * @param {Uint8Array} bytes The wave file as an array of bytes.
63
     */    
64
    fromBytes(bytes) {
65
        this.readRIFFChunk_(bytes);
66
        this.readWAVEChunk_(bytes);
67
        this.readFmtChunk_(bytes);
68
        try {
69
            this.readFactChunk_(bytes);
70
        }catch(err) {
71
            if (this.enforceFact) {
72
                throw err;
73
            }
74
        }
75
        try {
76
            this.readBextChunk_(bytes);
77
        }catch(err) {
78
            if (this.enforceBext) {
79
                throw err;
80
            }
81
        }
82
        this.readDataChunk_(bytes);
83
    }
84
85
    /**
86
     * Turn the wave file represented by an object of this class into
87
     * a array of bytes.
88
     * @return {Uint8Array}
89
     */
90
    toBytes() {
91
        this.checkWriteInput_(this.numChannels, this.sampleRate, this.bitDepth_);
92
        this.samplesToBytes_();
93
        return new Uint8Array(this.createWaveFile_());
94
    }
95
96
    /**
97
     * Read the RIFF chunk a wave file.
98
     * @param {Uint8Array} bytes an array representing the wave file.
99
     * @throws {Error} If no "RIFF" chunk is found.
100
     */
101
    readRIFFChunk_(bytes) {
102
        this.chunkId = byteData.stringFromBytes(bytes.slice(0, 4));
103
        if (this.chunkId != "RIFF") {
104
            throw Error(this.WaveErrors.format);
105
        }
106
        this.chunkSize = byteData.intFrom4Bytes(
107
            bytes.slice(4, 8))[0];
108
    }
109
110
    /**
111
     * Read the WAVE chunk of a wave file.
112
     * @param {Uint8Array} bytes an array representing the wave file.
113
     * @throws {Error} If no "WAVE" chunk is found.
114
     */
115
    readWAVEChunk_(bytes) {
116
        let start = byteData.findString(bytes, "WAVE");
117
        if (start === -1) {
118
            throw Error(this.WaveErrors.wave);
119
        }
120
        this.format = byteData.stringFromBytes(
121
                bytes.slice(start, start + 4));
122
    }
123
124
    /**
125
     * Read the "fmt " chunk of a wave file.
126
     * @param {Uint8Array} bytes an array representing the wave file.
127
     * @throws {Error} If no "fmt " chunk is found.
128
     */
129
    readFmtChunk_(bytes) {
130
        let start = byteData.findString(bytes, "fmt ");
131
        if (start === -1) {
132
            throw Error(this.WaveErrors['fmt ']);
133
        }
134
        this.subChunk1Id = byteData.stringFromBytes(
135
            bytes.slice(start, start + 4));
136
        this.subChunk1Size = byteData.uIntFrom4Bytes(
137
            bytes.slice(start + 4, start + 8))[0];
138
        this.audioFormat = byteData.uIntFrom2Bytes(
139
            bytes.slice(start + 8, start + 10))[0];
140
        this.numChannels = byteData.uIntFrom2Bytes(
141
            bytes.slice(start + 10, start + 12))[0];
142
        this.sampleRate = byteData.uIntFrom4Bytes(
143
            bytes.slice(start + 12, start + 16))[0];
144
        this.byteRate = byteData.uIntFrom4Bytes(
145
            bytes.slice(start + 16, start + 20))[0];
146
        this.blockAlign = byteData.uIntFrom2Bytes(
147
            bytes.slice(start + 20, start + 22))[0];
148
        this.bitsPerSample = byteData.uIntFrom2Bytes(
149
            bytes.slice(start + 22, start + 24))[0];
150
        // The bitDepth_ is used internally to determine
151
        // wich function use to read the samples
152
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
153
            this.bitDepth_ = "32f";
154
        }else {
155
            this.bitDepth_ = this.bitsPerSample.toString();
156
        }
157
    }
158
159
    /**
160
     * Read the "fact" chunk of a wave file.
161
     * @param {Uint8Array} bytes an array representing the wave file.
162
     * @throws {Error} If no "fact" chunk is found.
163
     */
164
    readFactChunk_(bytes) {
165
        let start = byteData.findString(bytes, "fact");
166
        if (start === -1) {
167
            throw Error(this.WaveErrors.fact);
168
        }else {
169
            this.factChunkId = byteData.stringFromBytes(
170
                bytes.slice(start, start + 4));
171
            //this.factChunkSize = byteData.uIntFrom4Bytes(
172
            //    bytes.slice(start + 4, start + 8));
173
            //this.dwSampleLength = byteData.uIntFrom4Bytes(
174
            //    bytes.slice(start + 8, start + 12));
175
        }
176
    }
177
178
    /**
179
     * Read the "bext" chunk of a wave file.
180
     * @param {Uint8Array} bytes an array representing the wave file.
181
     * @throws {Error} If no "bext" chunk is found.
182
     */
183
    readBextChunk_(bytes) {
184
        let start = byteData.findString(bytes, "bext");
185
        if (start === -1) {
186
            throw Error(this.WaveErrors.bext);
187
        }else {
188
            this.bextChunkId = byteData.stringFromBytes(
189
                bytes.slice(start, start + 4));
190
        }
191
    }
192
193
    /**
194
     * Read the "data" chunk of a wave file.
195
     * @param {Uint8Array} bytes an array representing the wave file.
196
     * @throws {Error} If no "data" chunk is found.
197
     */
198
    readDataChunk_(bytes) {
199
        let start = byteData.findString(bytes, "data");
200
        if (start === -1) {
201
            throw Error(this.WaveErrors.data);
202
        }
203
        this.subChunk2Id = byteData.stringFromBytes(
204
            bytes.slice(start, start + 4));
205
        this.subChunk2Size = byteData.intFrom4Bytes(
206
            bytes.slice(start + 4, start + 8))[0];
207
        this.samplesFromBytes_(bytes, start);
208
    }
209
210
    /**
211
     * Find and return the start offset of the data chunk on a wave file.
212
     * @param {Uint8Array} bytes Array of bytes representing the wave file.
213
     * @param {number} start The offset to start reading.
214
     */
215
    samplesFromBytes_(bytes, start) {
216
        let readingFunctions = {
217
            "4": byteData.intFrom1Byte,
218
            "8": byteData.uIntFrom1Byte,
219
            "16": byteData.intFrom2Bytes,
220
            "24": byteData.intFrom3Bytes,
221
            "32": byteData.intFrom4Bytes,
222
            "32f": byteData.floatFrom4Bytes,
223
            "64" : byteData.floatFrom8Bytes
224
        };
225
        let samples = bytes.slice(start + 8, start + 8 + this.subChunk2Size);
226
        this.samples_ = readingFunctions[this.bitDepth_](samples);
227
    }
228
229
    /**
230
     * Validate the input for wav writing.
231
     * @param {number} numChannels The number of channels
232
     *     Should be a int greater than zero smaller than the
233
     *     channel limit according to the bit depth.
234
     * @param {number} sampleRate The sample rate.
235
     *     Should be a int greater than zero smaller than the
236
     *     channel limit according to the bit depth and number of channels.
237
     * @param {string} bitDepth The audio bit depth.
238
     *     Should be one of "8", "16", "24", "32", "32f", "64".
239
     * @throws {Error} If any argument does not meet the criteria.
240
     */
241
    checkWriteInput_(numChannels, sampleRate, bitDepth) {
242
        if (typeof bitDepth !== "string" ||
243
            !(bitDepth in this.headerFormats_)) {
244
            throw new Error("Invalid bit depth.");
245
        }
246
        this.validateNumChannels_(numChannels, bitDepth);
247
        this.validateSampleRate_(numChannels, sampleRate, bitDepth);
248
    }
249
250
    /**
251
     * Validate the sample rate value.
252
     * @param {number} numChannels The number of channels
253
     * @param {string} bitDepth The audio bit depth.
254
     *     Should be one of "8", "16", "24", "32", "32f", "64".
255
     * @throws {Error} If any argument does not meet the criteria.
256
     */
257
    validateNumChannels_(numChannels, bitDepth) {
258
        let errorText = "Invalid number of channels.";
259
        let validChannnelNumber = false;
260
        let blockAlign = numChannels * (parseInt(bitDepth, 10) / 8);
261
        if (blockAlign <= 65535) {
262
            validChannnelNumber = true;
263
        }
264
        if (numChannels < 1 || !validChannnelNumber ||
265
            !(typeof numChannels==="number" && (numChannels%1)===0)) {
266
            throw new Error(errorText);
267
        }
268
        return true;
269
    }
270
271
    /**
272
     * Validate the sample rate value.
273
     * @param {number} numChannels The number of channels
274
     *     Should be a int greater than zero smaller than the
275
     *     channel limit according to the bit depth.
276
     * @param {number} sampleRate The sample rate.
277
     * @param {string} bitDepth The audio bit depth.
278
     *     Should be one of "8", "16", "24", "32", "32f", "64".
279
     * @throws {Error} If any argument does not meet the criteria.
280
     */
281
    validateSampleRate_(numChannels, sampleRate, bitDepth) {
282
        let errorText = "Invalid sample rate.";
283
        let validSampleRateValue = false;
284
        let byteRate = numChannels * (parseInt(bitDepth, 10) / 8) * sampleRate;
285
        if (byteRate <= 4294967295) {
286
            validSampleRateValue = true;
287
        }
288
        if (sampleRate < 1 || !validSampleRateValue ||
289
            !(typeof sampleRate==="number" && (sampleRate%1)===0)) {
290
            throw new Error(errorText);
291
        }
292
        return true;
293
    }
294
295
    /**
296
     * Split each sample into bytes.
297
     */
298
    samplesToBytes_() {
299
        let writingFunctions = {
300
            "4": byteData.intTo1Byte,
301
            "8": byteData.intTo1Byte,
302
            "16": byteData.intTo2Bytes,
303
            "24": byteData.intTo3Bytes,
304
            "32": byteData.intTo4Bytes,
305
            "32f": byteData.floatTo4Bytes,
306
            "64" : byteData.floatTo8Bytes
307
        };
308
        // FIXME byte-data should not modify the original array
309
        let s = [];
310
        for (let l=0; l<this.samples_.length; l++) {
311
            s[l] = this.samples_[l];
312
        }
313
        this.bytes_ = writingFunctions[this.bitDepth_](s);
314
        if (this.bytes_.length % 2) {
315
            this.bytes_.push(0);
316
        }
317
    }
318
319
    /**
320
     * Turn a WaveFile object into a file.
321
     * @return {Uint8Array} The wav file bytes.
322
     */
323
    createWaveFile_() {
324
        let factVal = [];
325
        let cbSizeVal = [];
0 ignored issues
show
The variable cbSizeVal seems to be never used. Consider removing it.
Loading history...
326
        if (this.factChunkId) {
327
            factVal= byteData.stringToBytes(this.factChunkId);
328
        }
329
        return byteData.stringToBytes(this.chunkId).concat(
330
            byteData.intTo4Bytes([this.chunkSize]),
331
            byteData.stringToBytes(this.format), 
332
            byteData.stringToBytes(this.subChunk1Id),
333
            byteData.intTo4Bytes([this.subChunk1Size]),
334
            byteData.intTo2Bytes([this.audioFormat]),
335
            byteData.intTo2Bytes([this.numChannels]),
336
            byteData.intTo4Bytes([this.sampleRate]),
337
            byteData.intTo4Bytes([this.byteRate]),
338
            byteData.intTo2Bytes([this.blockAlign]),
339
            byteData.intTo2Bytes([this.bitsPerSample]),
340
            factVal,
341
            byteData.stringToBytes(this.subChunk2Id),
342
            byteData.intTo4Bytes([this.subChunk2Size]),
343
            this.bytes_);
344
    }
345
}
346
347
module.exports.WaveFile = WaveFile;
348