Passed
Push — master ( 66fc5e...2c9c12 )
by Rafael S.
01:28
created

wavefile.js ➔ ???   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 14
Bugs 0 Features 0
Metric Value
c 14
b 0
f 0
nc 1
dl 0
loc 26
rs 8.8571
cc 1
nop 3
1
/*!
2
 * wavefile
3
 * Read & write wave files with 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 bitDepthLib = require("bitdepth");
0 ignored issues
show
Unused Code introduced by
The constant bitDepthLib seems to be never used. Consider removing it.
Loading history...
11
const wavefileheader = require("../src/wavefileheader");
12
13
/**
14
 * A wave file.
15
 */
16
class WaveFile extends wavefileheader.WaveFileHeader {
17
18
    /**
19
     * @param {Uint8Array} bytes The file bytes.
20
     * @param {boolean} enforceFact True if it should throw a error
21
     *      if no "fact" chunk is found.
22
     * @param {boolean} enforceBext True if it should throw a error
23
     *      if no "bext" chunk is found.
24
     */
25
    constructor(bytes, enforceFact=false, enforceBext=false) {
26
        super();
27
        /** @type {boolean} */
28
        this.isFromScratch_ = false;
29
        /** @type {boolean} */
30
        this.enforceFact = enforceFact;
31
        /** @type {boolean} */
32
        this.enforceBext = enforceBext;
33
        /**
34
         * Error messages.
35
         * @enum {string}
36
         */
37
        this.WaveErrors = {
38
            "format": "Not a supported format.",
39
            "wave": "Could not find the 'WAVE' chunk",
40
            "fmt ": "Could not find the 'fmt ' chunk",
41
            "data": "Could not find the 'data' chunk",
42
            "fact": "Could not find the 'fact' chunk",
43
            "bext": "Could not find the 'bext' chunk",
44
            "bitDepth": "Invalid bit depth.",
45
            "numChannels": "Invalid number of channels.",
46
            "sampleRate": "Invalid sample rate."
47
        };
48
        this.samples_ = [];
49
        this.bytes_ = [];
50
    }
51
    
52
    /**
53
     * Read the RIFF chunk a wave file.
54
     * @param {Uint8Array} bytes an array representing the wave file.
55
     * @throws {Error} If no "RIFF" chunk is found.
56
     */
57
    readRIFFChunk_(bytes) {
58
        this.chunkId = byteData.fromBytes(bytes.slice(0, 4),
59
            8, {"char": true});
60
        if (this.chunkId != "RIFF" && this.chunkId != "RIFX") {
61
            throw Error(this.WaveErrors.format);
62
        }
63
        this.chunkSize = byteData.fromBytes(
64
            bytes.slice(4, 8), 32, {"be": this.chunkId == "RIFX"})[0];
65
    }
66
67
    /**
68
     * Read the WAVE chunk of a wave file.
69
     * @param {Uint8Array} bytes an array representing the wave file.
70
     * @throws {Error} If no "WAVE" chunk is found.
71
     */
72
    readWAVEChunk_(bytes) {
73
        let start = byteData.findString(bytes, "WAVE");
74
        if (start === -1) {
75
            throw Error(this.WaveErrors.wave);
76
        }
77
        this.format = "WAVE";
78
    }
79
80
    /**
81
     * Read the "fmt " chunk of a wave file.
82
     * @param {Uint8Array} bytes an array representing the wave file.
83
     * @throws {Error} If no "fmt " chunk is found.
84
     */
85
    readFmtChunk_(bytes) {
86
        let start = byteData.findString(bytes, "fmt ");
87
        if (start === -1) {
88
            throw Error(this.WaveErrors["fmt "]);
89
        }
90
        let options = {"be": this.chunkId == "RIFX"};
91
        this.subChunk1Id = "fmt ";
92
        this.subChunk1Size = byteData.fromBytes(
93
            bytes.slice(start + 4, start + 8), 32, options)[0];
94
        this.audioFormat = byteData.fromBytes(
95
            bytes.slice(start + 8, start + 10), 16, options)[0];
96
        this.numChannels = byteData.fromBytes(
97
            bytes.slice(start + 10, start + 12), 16, options)[0];
98
        this.sampleRate = byteData.fromBytes(
99
            bytes.slice(start + 12, start + 16), 32, options)[0];
100
        this.byteRate = byteData.fromBytes(
101
            bytes.slice(start + 16, start + 20), 32, options)[0];
102
        this.blockAlign = byteData.fromBytes(
103
            bytes.slice(start + 20, start + 22), 16, options)[0];
104
        this.bitsPerSample = byteData.fromBytes(
105
            bytes.slice(start + 22, start + 24), 16, options)[0];
106
        if (this.audioFormat == 3 && this.bitsPerSample == 32) {
107
            this.bitDepth_ = "32f";
108
        }else {
109
            this.bitDepth_ = this.bitsPerSample.toString();
110
        }
111
    }
112
113
    /**
114
     * Read the "fact" chunk of a wave file.
115
     * @param {Uint8Array} bytes an array representing the wave file.
116
     * @throws {Error} If no "fact" chunk is found.
117
     */
118
    readFactChunk_(bytes) {
119
        let start = byteData.findString(bytes, "fact");
120
        if (start === -1 && this.enforceFact) {
121
            throw Error(this.WaveErrors.fact);
122
        }else if (start > -1) {
123
            this.factChunkId = "fact";
124
            //this.factChunkSize = byteData.uIntFrom4Bytes(
125
            //    bytes.slice(start + 4, start + 8));
126
            //this.dwSampleLength = byteData.uIntFrom4Bytes(
127
            //    bytes.slice(start + 8, start + 12));
128
        }
129
    }
130
131
    /**
132
     * Read the "bext" chunk of a wave file.
133
     * @param {Uint8Array} bytes an array representing the wave file.
134
     * @throws {Error} If no "bext" chunk is found.
135
     */
136
    readBextChunk_(bytes) {
137
        let start = byteData.findString(bytes, "bext");
138
        if (start === -1 && this.enforceBext) {
139
            throw Error(this.WaveErrors.bext);
140
        }else if (start > -1){
141
            this.bextChunkId = "bext";
142
        }
143
    }
144
145
    /**
146
     * Read the "data" chunk of a wave file.
147
     * @param {Uint8Array} bytes an array representing the wave file.
148
     * @throws {Error} If no "data" chunk is found.
149
     */
150
    readDataChunk_(bytes) {
151
        let start = byteData.findString(bytes, "data");
152
        if (start === -1) {
153
            throw Error(this.WaveErrors.data);
154
        }
155
        this.subChunk2Id = "data";
156
        this.subChunk2Size = byteData.fromBytes(
157
            bytes.slice(start + 4, start + 8),
158
            32,
159
            {"be": this.chunkId == "RIFX"})[0];
160
        this.samplesFromBytes_(bytes, start);
161
    }
162
163
    /**
164
     * Find and return the start offset of the data chunk on a wave file.
165
     * @param {Uint8Array} bytes Array of bytes representing the wave file.
166
     * @param {number} start The offset to start reading.
167
     */
168
    samplesFromBytes_(bytes, start) {
169
        let params = {
170
            "signed": this.bitsPerSample == 8 ? false : true,
171
            "be": this.chunkId == "RIFX"
172
        };
173
        if (this.bitsPerSample == 32 && this.audioFormat == 3) {
174
            params.float = true;
175
        }
176
        let samples = bytes.slice(start + 8, start + 8 + this.subChunk2Size);
177
        if (this.bitsPerSample == 4) {
178
            this.samples_ = byteData.fromBytes(samples, 8, params);
179
        } else {
180
            this.samples_ = byteData.fromBytes(samples, this.bitsPerSample, params);
181
        }
182
    }
183
184
    /**
185
     * Validate the input for wav writing.
186
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
187
     *     Should be a int greater than zero smaller than the
188
     *     channel limit according to the bit depth.
189
     * @param {number} sampleRate The sample rate.
0 ignored issues
show
Documentation introduced by
The parameter sampleRate does not exist. Did you maybe forget to remove this comment?
Loading history...
190
     *     Should be a int greater than zero smaller than the
191
     *     channel limit according to the bit depth and number of channels.
192
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
193
     *     Should be one of "8", "16", "24", "32", "32f", "64".
194
     * @throws {Error} If any argument does not meet the criteria.
195
     */
196
    checkWriteInput_() {
197
        this.validateBitDepth_();
198
        this.validateNumChannels_();
199
        this.validateSampleRate_();
200
    }
201
202
    /**
203
     * Validate the bit depth.
204
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
205
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
206
     *     Should be one of "8", "16", "24", "32", "32f", "64".
207
     * @throws {Error} If any argument does not meet the criteria.
208
     */
209
    validateBitDepth_() {
210
        if (!this.headerFormats_[this.bitDepth_]) {
211
            throw new Error(this.WaveErrors.bitDepth);
212
        }
213
        return true;
214
    }
215
216
    /**
217
     * Validate the sample rate value.
218
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
219
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
220
     *     Should be one of "8", "16", "24", "32", "32f", "64".
221
     * @throws {Error} If any argument does not meet the criteria.
222
     */
223
    validateNumChannels_() {
224
        let blockAlign = this.numChannels * this.bitsPerSample / 8;
225
        if (this.numChannels < 1 || blockAlign > 65535) {
226
            throw new Error(this.WaveErrors.numChannels);
227
        }
228
        return true;
229
    }
230
231
    /**
232
     * Validate the sample rate value.
233
     * @param {number} numChannels The number of channels
0 ignored issues
show
Documentation introduced by
The parameter numChannels does not exist. Did you maybe forget to remove this comment?
Loading history...
234
     *     Should be a int greater than zero smaller than the
235
     *     channel limit according to the bit depth.
236
     * @param {number} sampleRate The sample rate.
0 ignored issues
show
Documentation introduced by
The parameter sampleRate does not exist. Did you maybe forget to remove this comment?
Loading history...
237
     * @param {string} bitDepth The audio bit depth.
0 ignored issues
show
Documentation introduced by
The parameter bitDepth does not exist. Did you maybe forget to remove this comment?
Loading history...
238
     *     Should be one of "8", "16", "24", "32", "32f", "64".
239
     * @throws {Error} If any argument does not meet the criteria.
240
     */
241
    validateSampleRate_() {
242
        let byteRate = this.numChannels *
243
            (this.bitsPerSample / 8) * this.sampleRate;
244
        if (this.sampleRate < 1 || byteRate > 4294967295) {
245
            throw new Error(this.WaveErrors.sampleRate);
246
        }
247
        return true;
248
    }
249
250
    /**
251
     * Split each sample into bytes.
252
     */
253
    samplesToBytes_() {
254
        let params = {"be": this.chunkId == "RIFX"};
255
        if (this.bitsPerSample == 32 && this.audioFormat == 3) {
256
            params.float = true;
257
        }
258
        let bitDepth = this.bitsPerSample == 4 ? 8 : this.bitsPerSample;
259
        this.bytes_ = byteData.toBytes(this.samples_, bitDepth, params);
260
        if (this.bytes_.length % 2) {
261
            this.bytes_.push(0);
262
        }
263
    }
264
265
    /**
266
     * Turn a WaveFile object into a file.
267
     * @return {Uint8Array} The wav file bytes.
268
     */
269
    createWaveFile_() {
270
        let factVal = [];
271
        if (this.factChunkId) {
272
            factVal = byteData.toBytes(this.factChunkId, 8, {"char": true});
273
        }
274
        let options = {"be": this.chunkId == "RIFX"};
275
        return byteData.toBytes(this.chunkId, 8, {"char": true}).concat(
276
            byteData.toBytes([this.chunkSize], 32, options),
277
            byteData.toBytes(this.format, 8, {"char": true}), 
278
            byteData.toBytes(this.subChunk1Id, 8, {"char": true}),
279
            byteData.toBytes([this.subChunk1Size], 32, options),
280
            byteData.toBytes([this.audioFormat], 16, options),
281
            byteData.toBytes([this.numChannels], 16, options),
282
            byteData.toBytes([this.sampleRate], 32, options),
283
            byteData.toBytes([this.byteRate], 32, options),
284
            byteData.toBytes([this.blockAlign], 16, options),
285
            byteData.toBytes([this.bitsPerSample], 16, options),
286
            factVal,
287
            byteData.toBytes(this.subChunk2Id, 8, {"char": true}),
288
            byteData.toBytes([this.subChunk2Size], 32, options),
289
            this.bytes_);
290
    }
291
}
292
293
module.exports.WaveFile = WaveFile;
294