Passed
Push — master ( d21d52...79cef3 )
by Rafael S.
01:53
created

wavefile-reader-writer.js ➔ ???   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 81
Bugs 3 Features 0
Metric Value
c 81
b 3
f 0
nc 1
dl 0
loc 28
rs 8.8571
cc 1
nop 0
1
/*
2
 * WaveFileReaderWriter
3
 * Class to read and write wav files to and from buffers.
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 WaveFileHeader = require("../src/wavefile-header");
13
/** @private */
14
const byteData_ = require("byte-data");
15
/** @private */
16
let uInt8_ = byteData_.uInt8;
17
/** @private */
18
let uInt16_ = byteData_.uInt16;
19
/** @private */
20
let uInt32_ = byteData_.uInt32;
21
/** @private */
22
const chr_ = byteData_.chr;
23
24
/**
25
 * Class to read and write wav files to and from buffers.
26
 * @extends WaveFileHeader
27
 */
28
class WaveFileReaderWriter extends WaveFileHeader {
29
30
    constructor() {
31
        super();
32
        /**
33
         * @type {Array<number>}
34
         */
35
        this.samples = [];
36
        /**
37
         * Header formats.
38
         * @enum {number}
39
         * @private
40
         */
41
        this.headerFormats_ = {
42
            "4": 17,
43
            "8": 1,
44
            "8a": 6,
45
            "8m": 7,
46
            "16": 1,
47
            "24": 1,
48
            "32": 1,
49
            "32f": 3,
50
            "64": 3
51
        };
52
        /**
53
         * @type {number} 
54
         * @private
55
         */
56
        this.head_ = 0;
57
    }
58
59
    /**
60
     * Read the RIFF chunk a wave file.
61
     * @param {Uint8Array} bytes A wav buffer.
62
     * @throws {Error} If no "RIFF" chunk is found.
63
     * @private
64
     */
65
    readRIFFChunk_(bytes) {
66
        this.chunkId = byteData_.unpackArray(bytes.slice(0, 4), chr_);
67
        if (this.chunkId != "RIFF" && this.chunkId != "RIFX") {
68
            throw Error(WAVE_ERRORS.format);
69
        }
70
        this.LEorBE_();
71
        this.chunkSize = byteData_.unpack(bytes.slice(4, 8), uInt32_);
72
        this.format = byteData_.unpackArray(bytes.slice(8, 12), chr_);
73
        if (this.format != "WAVE") {
74
            throw Error(WAVE_ERRORS.wave);
75
        }
76
    }
77
78
    /**
79
     * Set up to work wih big-endian or little-endian files.
80
     * The types used are changed from LE or BE. If the
81
     * the file is big-endian (RIFX), true is returned.
82
     * @private
83
     */
84
    LEorBE_() {
85
        let bigEndian = this.chunkId == "RIFX";
86
        uInt8_.be = bigEndian;
87
        uInt16_.be = bigEndian;
88
        uInt32_.be = bigEndian;
89
        return bigEndian;
90
    }
91
92
    /**
93
     * Read the "fmt " chunk of a wave file.
94
     * @param {Object} chunks The wav file chunks.
95
     * @throws {Error} If no "fmt " chunk is found.
96
     * @private
97
     */
98
    readFmtChunk_(chunks) {
99
        let chunk = this.findChunk_(chunks, "fmt ");
100
        if (chunk) {
101
            this.fmtChunkId = "fmt ";
102
            this.fmtChunkSize = chunk.chunkSize;
103
            this.audioFormat = byteData_.unpack(
104
                chunk.chunkData.slice(0, 2), uInt16_);
105
            this.numChannels = byteData_.unpack(
106
                chunk.chunkData.slice(2, 4), uInt16_);
107
            this.sampleRate = byteData_.unpack(
108
                chunk.chunkData.slice(4, 8), uInt32_);
109
            this.byteRate = byteData_.unpack(
110
                chunk.chunkData.slice(8, 12), uInt32_);
111
            this.blockAlign = byteData_.unpack(
112
                chunk.chunkData.slice(12, 14), uInt16_);
113
            this.bitsPerSample = byteData_.unpack(
114
                    chunk.chunkData.slice(14, 16), uInt16_);
115
            this.readFmtExtension_(chunk);
116
        } else {
117
            throw Error(WAVE_ERRORS["fmt "]);
118
        }
119
    }
120
121
    /**
122
     * Read the "fmt " chunk extension.
123
     * @param {Object} chunk The "fmt " chunk.
124
     * @private
125
     */
126
    readFmtExtension_(chunk) {
127
        if (this.fmtChunkSize > 16) {
128
            this.cbSize = byteData_.unpack(
129
                chunk.chunkData.slice(16, 18), uInt16_);
130
            if (this.fmtChunkSize > 18) {
131
                this.validBitsPerSample = byteData_.unpack(
132
                    chunk.chunkData.slice(18, 20), uInt16_);
133
            }
134
        }
135
    }
136
137
    /**
138
     * Read the "fact" chunk of a wav file.
139
     * @param {Object} chunks The wav file chunks.
140
     * @throws {Error} If no "fact" chunk is found.
141
     * @private
142
     */
143
    readFactChunk_(chunks) {
144
        let chunk = this.findChunk_(chunks, "fact");
145
        if (chunk) {
146
            this.factChunkId = "fact";
147
            this.factChunkSize = chunk.chunkSize;
148
            this.dwSampleLength = byteData_.unpack(
149
                chunk.chunkData.slice(0, 4), uInt32_);
150
        } else if (this.enforceFact) {
151
            throw Error(WAVE_ERRORS["fact"]);
152
        }
153
    }
154
155
    /**
156
     * Read the "bext" chunk of a wav file.
157
     * @param {Object} chunks The wav file chunks.
158
     * @throws {Error} If no "bext" chunk is found.
159
     * @private
160
     */
161
    readBextChunk_(chunks) {
162
        let chunk = this.findChunk_(chunks, "bext");
163
        if (chunk) {
164
            this.bextChunkId = "bext";
165
            this.bextChunkSize = chunk.chunkSize;
166
            this.bextChunkData = chunk.chunkData;
167
            this.readBextChunkFields_();
168
        }
169
    }
170
171
    /**
172
     * Read the fields of the "bext" chunk.
173
     * @private
174
     */
175
    readBextChunkFields_() {
176
        this.head_ = 0;
177
        this.bextChunkFields =  {
178
            "description": this.readVariableSizeString_(
179
                this.bextChunkData, 256),
180
            "originator": this.readVariableSizeString_(
181
                this.bextChunkData, 32),
182
            "originatorReference": this.readVariableSizeString_(
183
                this.bextChunkData, 32),
184
            "originationDate": this.readVariableSizeString_(
185
                this.bextChunkData, 10),
186
            "originationTime": this.readVariableSizeString_(
187
                this.bextChunkData, 8),
188
            // timeReference is a 64-bit value
189
            "timeReference": this.readBytes_(
190
                this.bextChunkData, 8), 
191
            "version": this.readFromChunk_(
192
                this.bextChunkData, uInt16_),
193
            "UMID": this.readVariableSizeString_(
194
                this.bextChunkData, 64), 
195
            "loudnessValue": this.readFromChunk_(
196
                this.bextChunkData, uInt16_),
197
            "loudnessRange": this.readFromChunk_(
198
                this.bextChunkData, uInt16_),
199
            "maxTruePeakLevel": this.readFromChunk_(
200
                this.bextChunkData, uInt16_),
201
            "maxMomentaryLoudness": this.readFromChunk_(
202
                this.bextChunkData, uInt16_),
203
            "maxShortTermLoudness": this.readFromChunk_(
204
                this.bextChunkData, uInt16_),
205
            "reserved": this.readVariableSizeString_(
206
                this.bextChunkData, 180),
207
            "codingHistory": this.readVariableSizeString_(
208
                this.bextChunkData, this.bextChunkData.length - 602),
209
        }
210
    }
211
212
    /**
213
     * Return a slice of the byte array while moving the reading head.
214
     * @param {Array<number>|Uint8Array} bytes The bytes.
215
     * @param {number} size the number of bytes to read.
216
     * @private
217
     */
218
    readBytes_(bytes, size) {
219
        let v = this.head_;
220
        this.head_ += size;
221
        return bytes.slice(v, this.head_);
222
    }
223
224
    /**
225
     * Read bytes as a string from a RIFF chunk.
226
     * @param {Array<number>|Uint8Array} bytes The bytes.
227
     * @param {number} maxSize the max size of the string.
228
     * @private
229
     */
230
    readVariableSizeString_(bytes, maxSize) {
231
        let str = "";
232
        for (let i=0; i<maxSize; i++) {
233
            str += byteData_.unpack([bytes[this.head_]], chr_);
234
            this.head_++;
235
        }
236
        return str;
237
    }
238
239
    /**
240
     * Read a number from a chunk.
241
     * @param {Array<number>|Uint8Array} bytes The chunk bytes.
242
     * @param {Object} bdType The byte-data corresponding type.
243
     * @private
244
     */
245
    readFromChunk_(bytes, bdType) {
246
        let size = bdType.bits / 8;
247
        let value = byteData_.unpack(
248
            bytes.slice(this.head_, this.head_ + size), bdType);
249
        this.head_ += size;
250
        return value;
251
    }
252
253
    /**
254
     * Write a variable size string as bytes.
255
     * If the string is smaller than the max size it 
256
     * is filled with 0s.
257
     * @param {string} str The string to be written as bytes.
258
     * @param {number} maxSize the max size of the string.
259
     * @private
260
     */
261
    writeVariableSizeString_(str, maxSize) {
262
        let bytes = byteData_.packArray(str, chr_);
263
        for (let i=bytes.length; i<maxSize; i++) {
264
            bytes.push(0);
265
        }
266
        return bytes;
267
    }
268
269
    /**
270
     * Read the "cue " chunk of a wave file.
271
     * @param {Object} chunks The RIFF file chunks.
272
     * @throws {Error} If no "cue" chunk is found.
273
     * @private
274
     */
275
    readCueChunk_(chunks) {
276
        let chunk = this.findChunk_(chunks, "cue ");
277
        if (chunk) {
278
            this.cueChunkId = "cue ";
279
            this.cueChunkSize = chunk.chunkSize;
280
            this.cueChunkData = chunk.chunkData;
281
        }
282
    }
283
284
    /**
285
     * Read the "data" chunk of a wave file.
286
     * @param {Object} chunks The RIFF file chunks.
287
     * @param {Object} options Type options.
288
     * @throws {Error} If no "data" chunk is found.
289
     * @private
290
     */
291
    readDataChunk_(chunks, options) {
292
        let chunk = this.findChunk_(chunks, "data");
293
        if (chunk) {
294
            this.dataChunkId = "data";
295
            this.dataChunkSize = chunk.chunkSize;
296
            this.samplesFromBytes_(chunk.chunkData, options);
297
        } else {
298
            throw Error(WAVE_ERRORS["data"]);
299
        }
300
    }
301
302
    /**
303
     * Find and return the start offset of the data chunk on a wave file.
304
     * @param {Array<number>|Uint8Array} bytes A wav file buffer.
305
     * @param {Object} options Type options.
306
     * @private
307
     */
308
    samplesFromBytes_(bytes, options) {
309
        options.bits = this.bitsPerSample == 4 ? 8 : this.bitsPerSample;
310
        options.signed = options.bits == 8 ? false : true;
311
        options.float = (this.audioFormat == 3 || 
312
            this.bitsPerSample == 64) ? true : false;
313
        options.single = false;
314
        this.samples = byteData_.unpackArray(
315
            bytes, new byteData_.Type(options));
316
    }
317
318
    /**
319
     * Find a chunk by its FourCC in a array of RIFF chunks.
320
     * @param {Object} chunks The wav file chunks.
321
     * @param {string} fourCC The chunk fourCC.
322
     * @return {Object|null}
323
     * @private
324
     */
325
    findChunk_(chunks, fourCC) {
326
        for (let i = 0; i<chunks.length; i++) {
327
            if (chunks[i].chunkId == fourCC) {
328
                return chunks[i];
329
            }
330
        }
331
        return null;
332
    }
333
334
    /**
335
     * Turn samples to bytes.
336
     * @param {Object} options Type options.
337
     * @private
338
     */
339
    samplesToBytes_(options) {
340
        options.bits = this.bitsPerSample == 4 ? 8 : this.bitsPerSample;
341
        options.signed = options.bits == 8 ? false : true;
342
        options.float = (this.audioFormat == 3  ||
343
                this.bitsPerSample == 64) ? true : false;
344
        let bytes = byteData_.packArray(
345
            this.samples, new byteData_.Type(options));
346
        if (bytes.length % 2) {
347
            bytes.push(0);
348
        }
349
        return bytes;
350
    }
351
352
    /**
353
     * Get the bytes of the "bext" chunk.
354
     * @return {Array<number>} The "bext" chunk bytes.
355
     * @private
356
     */
357
    getBextBytes_() {
358
        if (this.bextChunkId) {
359
            let bextBytes = [].concat(this.writeVariableSizeString_(
360
                this.bextChunkFields["description"], 256));
361
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
362
                this.bextChunkFields["originator"], 32));
363
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
364
                this.bextChunkFields["originatorReference"], 32));
365
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
366
                this.bextChunkFields["originationDate"], 10));
367
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
368
                this.bextChunkFields["originationTime"], 8));
369
            // 64-bit value rw as bytes
370
            bextBytes = bextBytes.concat(
371
                this.bextChunkFields["timeReference"]);
372
            bextBytes = bextBytes.concat(byteData_.pack(
373
                this.bextChunkFields["version"], uInt16_));
374
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
375
                this.bextChunkFields["UMID"], 64));
376
            bextBytes = bextBytes.concat(byteData_.pack(
377
                this.bextChunkFields["loudnessValue"], uInt16_));
378
            bextBytes = bextBytes.concat(byteData_.pack(
379
                this.bextChunkFields["loudnessRange"], uInt16_));
380
            bextBytes = bextBytes.concat(byteData_.pack(
381
                this.bextChunkFields["maxTruePeakLevel"], uInt16_));
382
            bextBytes = bextBytes.concat(byteData_.pack(
383
                this.bextChunkFields["maxMomentaryLoudness"], uInt16_));
384
            bextBytes = bextBytes.concat(byteData_.pack(
385
                this.bextChunkFields["maxShortTermLoudness"], uInt16_));
386
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
387
                this.bextChunkFields["reserved"], 180));
388
            bextBytes = bextBytes.concat(this.writeVariableSizeString_(
389
                this.bextChunkFields["codingHistory"],
390
                this.bextChunkData.length - 602));
391
            return [].concat(
392
                    byteData_.packArray(this.bextChunkId, chr_),
393
                    byteData_.pack(bextBytes.length, uInt32_),
394
                    bextBytes
395
                );
396
        }
397
        return [];
398
    }
399
400
    /**
401
     * Get the bytes of the "cue " chunk.
402
     * @return {Array<number>} The "cue " chunk bytes.
403
     * @private
404
     */
405
    getCueBytes_() {
406
        if (this.cueChunkId) {
407
            return [].concat(
408
                    byteData_.packArray(this.cueChunkId, chr_),
409
                    byteData_.pack(this.cueChunkSize, uInt32_),
410
                    this.cueChunkData
411
                );
412
        }
413
        return [];
414
    }
415
416
    /**
417
     * Get the bytes of the "fact" chunk.
418
     * @return {Array<number>} The "fact" chunk bytes.
419
     * @private
420
     */
421
    getFactBytes_() {
422
        if (this.factChunkId) {
423
            return [].concat(
424
                    byteData_.packArray(this.factChunkId, chr_),
425
                    byteData_.pack(this.factChunkSize, uInt32_),
426
                    byteData_.pack(this.dwSampleLength, uInt32_)
427
                );
428
        }
429
        return [];
430
    }
431
432
    /**
433
     * Get the bytes of the cbSize field.
434
     * @return {Array<number>} The cbSize bytes.
435
     * @private
436
     */
437
    getCbSizeBytes_() {
438
        if (this.fmtChunkSize > 16) {
439
            return byteData_.pack(this.cbSize, uInt16_);
440
        }
441
        return [];
442
    }
443
444
    /**
445
     * Get the bytes of the validBitsPerSample field.
446
     * @return {Array<number>} The validBitsPerSample bytes.
447
     * @private
448
     */
449
    getValidBitsPerSampleBytes_() {
450
        if (this.fmtChunkSize > 18) {
451
            return byteData_.pack(this.validBitsPerSample, uInt16_);
452
        }
453
        return [];
454
    }
455
456
    /**
457
     * Turn a WaveFile object into a file.
458
     * @return {Array<number>} The wav file bytes.
459
     * @private
460
     */
461
    createWaveFile_() {
462
        let options = {"be": this.LEorBE_()};
463
        return byteData_.packArray(this.chunkId, chr_).concat(
464
                byteData_.pack(this.chunkSize, uInt32_),
465
                byteData_.packArray(this.format, chr_),
466
                this.getBextBytes_(),
467
                byteData_.packArray(this.fmtChunkId, chr_),
468
                byteData_.pack(this.fmtChunkSize, uInt32_),
469
                byteData_.pack(this.audioFormat, uInt16_),
470
                byteData_.pack(this.numChannels, uInt16_),
471
                byteData_.pack(this.sampleRate, uInt32_),
472
                byteData_.pack(this.byteRate, uInt32_),
473
                byteData_.pack(this.blockAlign, uInt16_),
474
                byteData_.pack(this.bitsPerSample, uInt16_),
475
                this.getCbSizeBytes_(),
476
                this.getValidBitsPerSampleBytes_(),
477
                this.getFactBytes_(),
478
                byteData_.packArray(this.dataChunkId, chr_),
479
                byteData_.pack(this.dataChunkSize, uInt32_),
480
                this.samplesToBytes_(options),
481
                this.getCueBytes_()
482
            );
483
    }
484
}
485
486
module.exports = WaveFileReaderWriter;
487