rochars /
wavefile
| 1 | /*! |
||
| 2 | * wavefile |
||
| 3 | * Read & write wave files with 4, 8, 11, 12, 16, 20, 24, 32 & 64-bit data. |
||
| 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 bitDepth_ = require("bitdepth"); |
||
| 13 | /** @private */ |
||
| 14 | const riffChunks_ = require("riff-chunks"); |
||
| 15 | /** @private */ |
||
| 16 | const imaadpcm_ = require("imaadpcm"); |
||
| 17 | /** @private */ |
||
| 18 | const alawmulaw_ = require("alawmulaw"); |
||
| 19 | /** @private */ |
||
| 20 | const WaveFileReaderWriter = require("./src/wavefile-reader-writer"); |
||
| 21 | |||
| 22 | /** |
||
| 23 | * Class representing a wav file. |
||
| 24 | * @extends WaveFileReaderWriter |
||
| 25 | */ |
||
| 26 | class WaveFile extends WaveFileReaderWriter { |
||
| 27 | |||
| 28 | /** |
||
| 29 | * @param {Uint8Array|Array<number>} bytes A wave file buffer. |
||
| 30 | * @throws {Error} If no "RIFF" chunk is found. |
||
| 31 | * @throws {Error} If no "fmt " chunk is found. |
||
| 32 | * @throws {Error} If no "fact" chunk is found and "fact" is needed. |
||
| 33 | * @throws {Error} If no "data" chunk is found. |
||
| 34 | */ |
||
| 35 | constructor(bytes) { |
||
| 36 | super(); |
||
| 37 | if(bytes) { |
||
| 38 | this.fromBuffer(bytes); |
||
| 39 | } |
||
| 40 | } |
||
| 41 | |||
| 42 | /** |
||
| 43 | * Set up a WaveFile object based on the arguments passed. |
||
| 44 | * @param {number} numChannels The number of channels |
||
| 45 | * (Integer numbers: 1 for mono, 2 stereo and so on). |
||
| 46 | * @param {number} sampleRate The sample rate. |
||
| 47 | * Integer numbers like 8000, 44100, 48000, 96000, 192000. |
||
| 48 | * @param {string} bitDepth The audio bit depth. |
||
| 49 | * One of "4", "8", "8a", "8m", "16", "24", "32", "32f", "64" |
||
| 50 | * or any value between "8" and "32". |
||
| 51 | * @param {Array<number>} samples Array of samples to be written. |
||
| 52 | * The samples must be in the correct range according to the |
||
| 53 | * bit depth. |
||
| 54 | * @throws {Error} If any argument does not meet the criteria. |
||
| 55 | */ |
||
| 56 | fromScratch(numChannels, sampleRate, bitDepth, samples, options={}) { |
||
| 57 | if (!options.container) { |
||
| 58 | options.container = "RIFF"; |
||
| 59 | } |
||
| 60 | // closest nuber of bytes if not / 8 |
||
| 61 | let numBytes = (((parseInt(bitDepth, 10) - 1) | 7) + 1) / 8; |
||
| 62 | // Clear the fact chunk |
||
| 63 | this.clearFactChunk_(); |
||
| 64 | // Normal PCM file header |
||
| 65 | this.chunkSize = 36 + samples.length * numBytes; |
||
| 66 | this.fmtChunkSize = 16; |
||
| 67 | this.byteRate = (numChannels * numBytes) * sampleRate; |
||
| 68 | this.blockAlign = numChannels * numBytes; |
||
| 69 | this.chunkId = options.container; |
||
| 70 | this.format = "WAVE"; |
||
| 71 | this.fmtChunkId = "fmt "; |
||
| 72 | this.audioFormat = this.headerFormats_[bitDepth] ? this.headerFormats_[bitDepth] : 65534; |
||
| 73 | this.numChannels = numChannels; |
||
| 74 | this.sampleRate = sampleRate; |
||
| 75 | this.bitsPerSample = parseInt(bitDepth, 10); |
||
| 76 | this.dataChunkId = "data"; |
||
| 77 | this.dataChunkSize = samples.length * numBytes; |
||
| 78 | this.samples = samples; |
||
| 79 | this.bitDepth = bitDepth; |
||
| 80 | // IMA ADPCM header |
||
| 81 | if (bitDepth == "4") { |
||
| 82 | this.chunkSize = 44 + samples.length; |
||
| 83 | this.fmtChunkSize = 20; |
||
| 84 | this.byteRate = 4055; |
||
| 85 | this.blockAlign = 256; |
||
| 86 | this.bitsPerSample = 4; |
||
| 87 | this.dataChunkSize = samples.length; |
||
| 88 | this.cbSize = 2; |
||
| 89 | this.validBitsPerSample = 505; |
||
| 90 | this.factChunkId = "fact"; |
||
| 91 | this.factChunkSize = 4; |
||
| 92 | this.dwSampleLength = samples.length * 2; |
||
| 93 | } |
||
| 94 | // A-Law and mu-Law header |
||
| 95 | if (bitDepth == "8a" || bitDepth == "8m") { |
||
| 96 | this.chunkSize = 44 + samples.length; |
||
| 97 | this.fmtChunkSize = 20; |
||
| 98 | this.cbSize = 2; |
||
| 99 | this.validBitsPerSample = 8; |
||
| 100 | this.factChunkId = "fact"; |
||
| 101 | this.factChunkSize = 4; |
||
| 102 | this.dwSampleLength = samples.length; |
||
| 103 | } |
||
| 104 | // WAVE_FORMAT_EXTENSIBLE |
||
| 105 | if (parseInt(bitDepth, 10) > 8 && (parseInt(bitDepth, 10) % 8)) { |
||
| 106 | this.chunkSize = 36 + 24 + samples.length * numBytes; |
||
| 107 | this.fmtChunkSize = 40; |
||
| 108 | this.bitsPerSample = (((parseInt(bitDepth, 10) - 1) | 7) + 1); |
||
| 109 | this.cbSize = 22; |
||
| 110 | this.validBitsPerSample = parseInt(bitDepth, 10); |
||
| 111 | this.dwChannelMask = 0; |
||
| 112 | // subformat 128-bit GUID as 4 32-bit values |
||
| 113 | // only supports uncompressed integer PCM samples |
||
| 114 | this.subformat1 = 1; |
||
| 115 | this.subformat2 = 1048576; |
||
| 116 | this.subformat3 = 2852126848; |
||
| 117 | this.subformat4 = 1905997824; |
||
| 118 | } |
||
| 119 | } |
||
| 120 | |||
| 121 | /** |
||
| 122 | * Init a WaveFile object from a byte buffer. |
||
| 123 | * @param {Uint8Array|Array<number>} bytes The buffer. |
||
| 124 | * @throws {Error} If no "RIFF" chunk is found. |
||
| 125 | * @throws {Error} If no "fmt " chunk is found. |
||
| 126 | * @throws {Error} If no "fact" chunk is found and "fact" is needed. |
||
| 127 | * @throws {Error} If no "data" chunk is found. |
||
| 128 | */ |
||
| 129 | fromBuffer(bytes) { |
||
| 130 | this.readRIFFChunk_(bytes); |
||
| 131 | let bigEndian = this.chunkId == "RIFX"; |
||
| 132 | let chunk = riffChunks_.read(bytes, bigEndian); |
||
| 133 | this.readFmtChunk_(chunk.subChunks); |
||
| 134 | this.readFactChunk_(chunk.subChunks); |
||
| 135 | this.readBextChunk_(chunk.subChunks); |
||
| 136 | this.readCueChunk_(chunk.subChunks); |
||
| 137 | this.readDataChunk_( |
||
| 138 | chunk.subChunks, |
||
| 139 | {"be": bigEndian, "single": true} |
||
| 140 | ); |
||
| 141 | if (this.audioFormat == 3 && this.bitsPerSample == 32) { |
||
| 142 | this.bitDepth = "32f"; |
||
| 143 | }else { |
||
| 144 | this.bitDepth = this.bitsPerSample.toString(); |
||
| 145 | } |
||
| 146 | } |
||
| 147 | |||
| 148 | /** |
||
| 149 | * Return a byte buffer representig the WaveFile object as a wav file. |
||
| 150 | * @return {Uint8Array} |
||
| 151 | * @throws {Error} If any property of the object appears invalid. |
||
| 152 | */ |
||
| 153 | toBuffer() { |
||
| 154 | this.checkWriteInput_(); |
||
| 155 | return new Uint8Array(this.createWaveFile_()); |
||
| 156 | } |
||
| 157 | |||
| 158 | /** |
||
| 159 | * Turn the file to RIFF. |
||
| 160 | */ |
||
| 161 | toRIFF() { |
||
| 162 | this.chunkId = "RIFF"; |
||
| 163 | this.LEorBE_(); |
||
| 164 | } |
||
| 165 | |||
| 166 | /** |
||
| 167 | * Turn the file to RIFX. |
||
| 168 | */ |
||
| 169 | toRIFX() { |
||
| 170 | this.chunkId = "RIFX"; |
||
| 171 | this.LEorBE_(); |
||
| 172 | } |
||
| 173 | |||
| 174 | /** |
||
| 175 | * Change the bit depth of the samples. |
||
| 176 | * @param {string} bitDepth The new bit depth of the samples. |
||
| 177 | * One of "8" ... "32" (integers), "32f" or "64" (floats) |
||
| 178 | * @param {boolean} changeResolution A boolean indicating if the |
||
| 179 | * resolution of samples should be actually changed or not. |
||
| 180 | * @throws {Error} If the bit depth is invalid. |
||
| 181 | */ |
||
| 182 | toBitDepth(bitDepth, changeResolution=true) { |
||
| 183 | if (!changeResolution) { |
||
| 184 | let toBitDepth = this.realBitDepth_(bitDepth); |
||
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
| 185 | let thisBitDepth = this.realBitDepth_(this.bitDepth); |
||
|
0 ignored issues
–
show
|
|||
| 186 | } |
||
| 187 | bitDepth_.toBitDepth(this.samples, this.bitDepth, bitDepth); |
||
| 188 | this.fromScratch( |
||
| 189 | this.numChannels, |
||
| 190 | this.sampleRate, |
||
| 191 | bitDepth, |
||
| 192 | this.samples, |
||
| 193 | {"container": this.chunkId} |
||
| 194 | ); |
||
| 195 | } |
||
| 196 | |||
| 197 | /** |
||
| 198 | * Interleave multi-channel samples. |
||
| 199 | */ |
||
| 200 | interleave() { |
||
| 201 | let finalSamples = []; |
||
| 202 | let numChannels = this.samples[0].length; |
||
| 203 | for (let i = 0; i < numChannels; i++) { |
||
| 204 | for (let j = 0; j < this.samples.length; j++) { |
||
| 205 | finalSamples.push(this.samples[j][i]); |
||
| 206 | } |
||
| 207 | } |
||
| 208 | this.samples = finalSamples; |
||
| 209 | } |
||
| 210 | |||
| 211 | /** |
||
| 212 | * De-interleave samples into multiple channels. |
||
| 213 | */ |
||
| 214 | deInterleave() { |
||
| 215 | let finalSamples = []; |
||
| 216 | let i; |
||
| 217 | for (i = 0; i < this.numChannels; i++) { |
||
| 218 | finalSamples[i] = []; |
||
| 219 | } |
||
| 220 | i = 0; |
||
| 221 | let j; |
||
| 222 | while (i < this.samples.length) { |
||
| 223 | for (j = 0; j < this.numChannels; j++) { |
||
| 224 | finalSamples[j].push(this.samples[i+j]); |
||
| 225 | } |
||
| 226 | i += j; |
||
| 227 | } |
||
| 228 | this.samples = finalSamples; |
||
| 229 | } |
||
| 230 | |||
| 231 | /** |
||
| 232 | * Encode a 16-bit wave file as 4-bit IMA ADPCM. |
||
| 233 | */ |
||
| 234 | toIMAADPCM() { |
||
| 235 | this.fromScratch( |
||
| 236 | this.numChannels, |
||
| 237 | this.sampleRate, |
||
| 238 | "4", |
||
| 239 | imaadpcm_.encode(this.samples), |
||
| 240 | {"container": this.chunkId} |
||
| 241 | ); |
||
| 242 | } |
||
| 243 | |||
| 244 | /** |
||
| 245 | * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file. |
||
| 246 | */ |
||
| 247 | fromIMAADPCM() { |
||
| 248 | this.fromScratch( |
||
| 249 | this.numChannels, |
||
| 250 | this.sampleRate, |
||
| 251 | "16", |
||
| 252 | imaadpcm_.decode(this.samples, this.blockAlign), |
||
| 253 | {"container": this.chunkId} |
||
| 254 | ); |
||
| 255 | } |
||
| 256 | |||
| 257 | /** |
||
| 258 | * Encode 16-bit wave file as 8-bit A-Law. |
||
| 259 | */ |
||
| 260 | toALaw() { |
||
| 261 | this.fromScratch( |
||
| 262 | this.numChannels, |
||
| 263 | this.sampleRate, |
||
| 264 | "8a", |
||
| 265 | alawmulaw_.alaw.encode(this.samples), |
||
| 266 | {"container": this.chunkId} |
||
| 267 | ); |
||
| 268 | } |
||
| 269 | |||
| 270 | /** |
||
| 271 | * Decode a 8-bit A-Law wave file into a 16-bit wave file. |
||
| 272 | */ |
||
| 273 | fromALaw() { |
||
| 274 | this.fromScratch( |
||
| 275 | this.numChannels, |
||
| 276 | this.sampleRate, |
||
| 277 | "16", |
||
| 278 | alawmulaw_.alaw.decode(this.samples), |
||
| 279 | {"container": this.chunkId} |
||
| 280 | ); |
||
| 281 | } |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Encode 16-bit wave file as 8-bit mu-Law. |
||
| 285 | */ |
||
| 286 | toMuLaw() { |
||
| 287 | this.fromScratch( |
||
| 288 | this.numChannels, |
||
| 289 | this.sampleRate, |
||
| 290 | "8m", |
||
| 291 | alawmulaw_.mulaw.encode(this.samples), |
||
| 292 | {"container": this.chunkId} |
||
| 293 | ); |
||
| 294 | } |
||
| 295 | |||
| 296 | /** |
||
| 297 | * Decode a 8-bit mu-Law wave file into a 16-bit wave file. |
||
| 298 | */ |
||
| 299 | fromMuLaw() { |
||
| 300 | this.fromScratch( |
||
| 301 | this.numChannels, |
||
| 302 | this.sampleRate, |
||
| 303 | "16", |
||
| 304 | alawmulaw_.mulaw.decode(this.samples), |
||
| 305 | {"container": this.chunkId} |
||
| 306 | ); |
||
| 307 | } |
||
| 308 | |||
| 309 | /** |
||
| 310 | * Get the closest greater number of bits for a number of bits that |
||
| 311 | * do not fill a full sequence of bytes. |
||
| 312 | * @param {string} bitDepth The bit depth. |
||
| 313 | * @return {string} |
||
| 314 | */ |
||
| 315 | realBitDepth_(bitDepth) { |
||
| 316 | if (bitDepth != "32f") { |
||
| 317 | bitDepth = (((parseInt(bitDepth, 10) - 1) | 7) + 1) |
||
| 318 | .toString(); |
||
| 319 | } |
||
| 320 | return bitDepth; |
||
| 321 | } |
||
| 322 | |||
| 323 | /** |
||
| 324 | * Validate the input for wav writing. |
||
| 325 | * @throws {Error} If any property of the object appears invalid. |
||
| 326 | * @private |
||
| 327 | */ |
||
| 328 | checkWriteInput_() { |
||
| 329 | this.validateBitDepth_(); |
||
| 330 | this.validateNumChannels_(); |
||
| 331 | this.validateSampleRate_(); |
||
| 332 | } |
||
| 333 | |||
| 334 | /** |
||
| 335 | * Validate the bit depth. |
||
| 336 | * @throws {Error} If bit depth is invalid. |
||
| 337 | * @private |
||
| 338 | */ |
||
| 339 | validateBitDepth_() { |
||
| 340 | if (!this.headerFormats_[this.bitDepth]) { |
||
| 341 | if (parseInt(this.bitDepth, 10) > 8 && |
||
| 342 | parseInt(this.bitDepth, 10) < 32) { |
||
| 343 | return true; |
||
| 344 | } |
||
| 345 | throw new Error(WAVE_ERRORS.bitDepth); |
||
| 346 | } |
||
| 347 | return true; |
||
| 348 | } |
||
| 349 | |||
| 350 | /** |
||
| 351 | * Validate the number of channels. |
||
| 352 | * @throws {Error} If the number of channels is invalid. |
||
| 353 | * @private |
||
| 354 | */ |
||
| 355 | validateNumChannels_() { |
||
| 356 | let blockAlign = this.numChannels * this.bitsPerSample / 8; |
||
| 357 | if (this.numChannels < 1 || blockAlign > 65535) { |
||
| 358 | throw new Error(WAVE_ERRORS.numChannels); |
||
| 359 | } |
||
| 360 | return true; |
||
| 361 | } |
||
| 362 | |||
| 363 | /** |
||
| 364 | * Validate the sample rate value. |
||
| 365 | * @throws {Error} If the sample rate is invalid. |
||
| 366 | * @private |
||
| 367 | */ |
||
| 368 | validateSampleRate_() { |
||
| 369 | let byteRate = this.numChannels * |
||
| 370 | (this.bitsPerSample / 8) * this.sampleRate; |
||
| 371 | if (this.sampleRate < 1 || byteRate > 4294967295) { |
||
| 372 | throw new Error(WAVE_ERRORS.sampleRate); |
||
| 373 | } |
||
| 374 | return true; |
||
| 375 | } |
||
| 376 | |||
| 377 | /** |
||
| 378 | * Reset the attributes related to the "fact" chunk. |
||
| 379 | * @private |
||
| 380 | */ |
||
| 381 | clearFactChunk_() { |
||
| 382 | this.cbSize = 0; |
||
| 383 | this.validBitsPerSample = 0; |
||
| 384 | this.factChunkId = ""; |
||
| 385 | this.factChunkSize = 0; |
||
| 386 | this.dwSampleLength = 0; |
||
| 387 | } |
||
| 388 | } |
||
| 389 | |||
| 390 | module.exports = WaveFile; |
||
| 391 |