UNPKG

riff-chunks

Version:

Parse the chunks of RIFF and RIFX files.

784 lines (737 loc) 23.8 kB
/* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /** * @fileoverview A function to swap endianness in byte buffers. * @see https://github.com/rochars/endianness */ /** * Swap the byte ordering in a buffer. The buffer is modified in place. * @param {!Array<number|string>|!Uint8Array} bytes The bytes. * @param {number} offset The byte offset. * @param {number=} index The start index. Assumes 0. * @param {number=} end The end index. Assumes the buffer length. * @throws {Error} If the buffer length is not valid. */ function endianness(bytes, offset, index=0, end=bytes.length) { if (end % offset) { throw new Error("Bad buffer length."); } for (; index < end; index += offset) { swap(bytes, offset, index); } } /** * Swap the byte order of a value in a buffer. The buffer is modified in place. * @param {!Array<number|string>|!Uint8Array} bytes The bytes. * @param {number} offset The byte offset. * @param {number} index The start index. * @private */ function swap(bytes, offset, index) { offset--; for(let x = 0; x < offset; x++) { /** @type {number|string} */ let theByte = bytes[index + x]; bytes[index + x] = bytes[index + offset]; bytes[index + offset] = theByte; offset--; } } /* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /** * @fileoverview Pack and unpack two's complement ints and unsigned ints. * @see https://github.com/rochars/byte-data */ /** * A class to pack and unpack two's complement ints and unsigned ints. */ class Integer { /** * @param {number} bits Number of bits used by the data. * @param {boolean} signed True for signed types. * @throws {Error} if the number of bits is smaller than 1 or greater than 64. */ constructor(bits, signed) { /** * The max number of bits used by the data. * @type {number} * @private */ this.bits = bits; /** * If this type it is signed or not. * @type {boolean} * @private */ this.signed = signed; /** * The number of bytes used by the data. * @type {number} * @private */ this.offset = 0; /** * Min value for numbers of this type. * @type {number} * @private */ this.min = -Infinity; /** * Max value for numbers of this type. * @type {number} * @private */ this.max = Infinity; /** * The practical number of bits used by the data. * @type {number} * @private */ this.realBits_ = this.bits; /** * The mask to be used in the last byte. * @type {number} * @private */ this.lastByteMask_ = 255; this.build_(); } /** * Read one integer number from a byte buffer. * @param {!Uint8Array} bytes An array of bytes. * @param {number=} i The index to read. * @return {number} */ read(bytes, i=0) { let num = 0; let x = this.offset - 1; while (x > 0) { num = (bytes[x + i] << x * 8) | num; x--; } num = (bytes[i] | num) >>> 0; return this.overflow_(this.sign_(num)); } /** * Write one integer number to a byte buffer. * @param {!Array<number>} bytes An array of bytes. * @param {number} number The number. * @param {number=} j The index being written in the byte buffer. * @return {number} The next index to write on the byte buffer. */ write(bytes, number, j=0) { number = this.overflow_(number); bytes[j++] = number & 255; for (let i = 2; i <= this.offset; i++) { bytes[j++] = Math.floor(number / Math.pow(2, ((i - 1) * 8))) & 255; } return j; } /** * Write one integer number to a byte buffer. * @param {!Array<number>} bytes An array of bytes. * @param {number} number The number. * @param {number=} j The index being written in the byte buffer. * @return {number} The next index to write on the byte buffer. * @private */ writeEsoteric_(bytes, number, j=0) { number = this.overflow_(number); j = this.writeFirstByte_(bytes, number, j); for (let i = 2; i < this.offset; i++) { bytes[j++] = Math.floor(number / Math.pow(2, ((i - 1) * 8))) & 255; } if (this.bits > 8) { bytes[j++] = Math.floor( number / Math.pow(2, ((this.offset - 1) * 8))) & this.lastByteMask_; } return j; } /** * Read a integer number from a byte buffer by turning int bytes * to a string of bits. Used for data with more than 32 bits. * @param {!Uint8Array} bytes An array of bytes. * @param {number=} i The index to read. * @return {number} * @private */ readBits_(bytes, i=0) { let binary = ''; let j = 0; while(j < this.offset) { let bits = bytes[i + j].toString(2); binary = new Array(9 - bits.length).join('0') + bits + binary; j++; } return this.overflow_(this.sign_(parseInt(binary, 2))); } /** * Build the type. * @throws {Error} if the number of bits is smaller than 1 or greater than 64. * @private */ build_() { this.setRealBits_(); this.setLastByteMask_(); this.setMinMax_(); this.offset = this.bits < 8 ? 1 : Math.ceil(this.realBits_ / 8); if ((this.realBits_ != this.bits) || this.bits < 8 || this.bits > 32) { this.write = this.writeEsoteric_; this.read = this.readBits_; } } /** * Sign a number. * @param {number} num The number. * @return {number} * @private */ sign_(num) { if (num > this.max) { num -= (this.max * 2) + 2; } return num; } /** * Limit the value according to the bit depth in case of * overflow or underflow. * @param {number} value The data. * @return {number} * @private */ overflow_(value) { if (value > this.max) { throw new Error('Overflow.'); } else if (value < this.min) { throw new Error('Underflow.'); } return value; } /** * Set the minimum and maximum values for the type. * @private */ setMinMax_() { let max = Math.pow(2, this.bits); if (this.signed) { this.max = max / 2 -1; this.min = -max / 2; } else { this.max = max - 1; this.min = 0; } } /** * Set the practical bit number for data with bit count different * from the standard types (8, 16, 32, 40, 48, 64) and more than 8 bits. * @private */ setRealBits_() { if (this.bits > 8) { this.realBits_ = ((this.bits - 1) | 7) + 1; } } /** * Set the mask that should be used when writing the last byte. * @private */ setLastByteMask_() { let r = 8 - (this.realBits_ - this.bits); this.lastByteMask_ = Math.pow(2, r > 0 ? r : 8) -1; } /** * Write the first byte of a integer number. * @param {!Array<number>} bytes An array of bytes. * @param {number} number The number. * @param {number} j The index being written in the byte buffer. * @return {number} The next index to write on the byte buffer. * @private */ writeFirstByte_(bytes, number, j) { if (this.bits < 8) { bytes[j++] = number < 0 ? number + Math.pow(2, this.bits) : number; } else { bytes[j++] = number & 255; } return j; } } /* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /** * @fileoverview Functions to validate input. * @see https://github.com/rochars/byte-data */ /** * Validate that the code is a valid ASCII code. * @param {number} code The code. * @throws {Error} If the code is not a valid ASCII code. */ function validateASCIICode(code) { if (code > 127) { throw new Error ('Bad ASCII code.'); } } /** * Validate the type definition. * @param {!Object} theType The type definition. * @throws {Error} If the type definition is not valid. */ function validateType(theType) { if (!theType) { throw new Error('Undefined type.'); } if (theType.float) { validateFloatType_(theType); } else { validateIntType_(theType); } } /** * Validate the type definition of floating point numbers. * @param {!Object} theType The type definition. * @throws {Error} If the type definition is not valid. * @private */ function validateFloatType_(theType) { if ([16,32,64].indexOf(theType.bits) == -1) { throw new Error('Bad float type.'); } } /** * Validate the type definition of integers. * @param {!Object} theType The type definition. * @throws {Error} If the type definition is not valid. * @private */ function validateIntType_(theType) { if (theType.bits < 1 || theType.bits > 53) { throw new Error('Bad type definition.'); } } /* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /** * Use a Typed Array to check if the host is BE or LE. This will impact * on how 64-bit floating point numbers are handled. * @type {boolean} * @private */ const BE_ENV = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x12; const HIGH = BE_ENV ? 1 : 0; const LOW = BE_ENV ? 0 : 1; /** * @type {!Int8Array} * @private */ let int8_ = new Int8Array(8); /** * @type {!Uint32Array} * @private */ let ui32_ = new Uint32Array(int8_.buffer); /** * @type {!Float32Array} * @private */ let f32_ = new Float32Array(int8_.buffer); /** * @type {!Float64Array} * @private */ let f64_ = new Float64Array(int8_.buffer); /** * @type {Function} * @private */ let reader_; /** * @type {Object} * @private */ let gInt_ = {}; /** * Validate the type and set up the packing/unpacking functions. * @param {!Object} theType The type definition. * @throws {Error} If the type definition is not valid. * @private */ function setUp_(theType) { validateType(theType); theType.offset = theType.bits < 8 ? 1 : Math.ceil(theType.bits / 8); theType.be = theType.be || false; setReader(theType); setWriter(theType); gInt_ = new Integer( theType.bits == 64 ? 32 : theType.bits, theType.float ? false : theType.signed); } /** * Read int values from bytes. * @param {!Uint8Array} bytes An array of bytes. * @param {number} i The index to read. * @return {number} * @private */ function readInt_(bytes, i) { return gInt_.read(bytes, i); } /** * Read 1 16-bit float from bytes. * Thanks https://stackoverflow.com/a/8796597 * @param {!Uint8Array} bytes An array of bytes. * @param {number} i The index to read. * @return {number} * @private */ function read16F_(bytes, i) { /** @type {number} */ let int = gInt_.read(bytes, i); /** @type {number} */ let exponent = (int & 0x7C00) >> 10; /** @type {number} */ let fraction = int & 0x03FF; /** @type {number} */ let floatValue; if (exponent) { floatValue = Math.pow(2, exponent - 15) * (1 + fraction / 0x400); } else { floatValue = 6.103515625e-5 * (fraction / 0x400); } return floatValue * (int >> 15 ? -1 : 1); } /** * Read 1 32-bit float from bytes. * @param {!Uint8Array} bytes An array of bytes. * @param {number} i The index to read. * @return {number} * @private */ function read32F_(bytes, i) { ui32_[0] = gInt_.read(bytes, i); return f32_[0]; } /** * Read 1 64-bit float from bytes. * Thanks https://gist.github.com/kg/2192799 * @param {!Uint8Array} bytes An array of bytes. * @param {number} i The index to read. * @return {number} * @private */ function read64F_(bytes, i) { ui32_[HIGH] = gInt_.read(bytes, i); ui32_[LOW] = gInt_.read(bytes, i + 4); return f64_[0]; } /** * Set the function to unpack the data. * @param {!Object} theType The type definition. * @private */ function setReader(theType) { if (theType.float) { if (theType.bits == 16) { reader_ = read16F_; } else if(theType.bits == 32) { reader_ = read32F_; } else if(theType.bits == 64) { reader_ = read64F_; } } else { reader_ = readInt_; } } /** * Set the function to pack the data. * @param {!Object} theType The type definition. * @private */ function setWriter(theType) { if (theType.float) { if (theType.bits == 16) ; else if(theType.bits == 32) ; else if(theType.bits == 64) ; } } /* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // ASCII characters /** * Read a string of ASCII characters from a byte buffer. * @param {!Uint8Array} bytes A byte buffer. * @param {number=} index The index to read. * @param {?number=} len The number of bytes to read. * @return {string} * @throws {Error} If a character in the string is not valid ASCII. */ function unpackString(bytes, index=0, len=null) { let chrs = ''; len = len ? index + len : bytes.length; while (index < len) { validateASCIICode(bytes[index]); chrs += String.fromCharCode(bytes[index]); index++; } return chrs; } /** * Unpack a number from a byte buffer by index. * @param {!Uint8Array} buffer The byte buffer. * @param {!Object} theType The type definition. * @param {number=} index The buffer index to read. * @return {number} * @throws {Error} If the type definition is not valid */ function unpackFrom(buffer, theType, index=0) { setUp_(theType); if (theType.be) { endianness(buffer, theType.offset, index, index + theType.offset); } let value = reader_(buffer, index); if (theType.be) { endianness(buffer, theType.offset, index, index + theType.offset); } return value; } /* * Copyright (c) 2017-2018 Rafael da Silva Rocha. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /** @private */ const uInt32_ = {bits: 32}; /** @type {number} */ let head_ = 0; /** * Return the chunks in a RIFF/RIFX file. * @param {!Uint8Array} buffer The file bytes. * @return {!Object} The RIFF chunks. */ function riffChunks(buffer) { head_ = 0; let chunkId = getChunkId_(buffer, 0); uInt32_.be = chunkId == 'RIFX'; let format = unpackString(buffer, 8, 4); head_ += 4; return { chunkId: chunkId, chunkSize: getChunkSize_(buffer, 0), format: format, subChunks: getSubChunksIndex_(buffer) }; } /** * Find a chunk by its fourCC_ in a array of RIFF chunks. * @param {!Object} chunks The wav file chunks. * @param {string} chunkId The chunk fourCC_. * @param {boolean} multiple True if there may be multiple chunks * with the same chunkId. * @return {?Array<!Object>} */ function findChunk(chunks, chunkId, multiple=false) { /** @type {!Array<!Object>} */ let chunk = []; for (let i=0; i<chunks.length; i++) { if (chunks[i].chunkId == chunkId) { if (multiple) { chunk.push(chunks[i]); } else { return chunks[i]; } } } if (chunkId == 'LIST') { return chunk.length ? chunk : null; } return null; } /** * Return the sub chunks of a RIFF file. * @param {!Uint8Array} buffer the RIFF file bytes. * @return {!Array<Object>} The subchunks of a RIFF/RIFX or LIST chunk. * @private */ function getSubChunksIndex_(buffer) { let chunks = []; let i = head_; while(i <= buffer.length - 8) { chunks.push(getSubChunkIndex_(buffer, i)); i += 8 + chunks[chunks.length - 1].chunkSize; i = i % 2 ? i + 1 : i; } return chunks; } /** * Return a sub chunk from a RIFF file. * @param {!Uint8Array} buffer the RIFF file bytes. * @param {number} index The start index of the chunk. * @return {!Object} A subchunk of a RIFF/RIFX or LIST chunk. * @private */ function getSubChunkIndex_(buffer, index) { let chunk = { chunkId: getChunkId_(buffer, index), chunkSize: getChunkSize_(buffer, index), }; if (chunk.chunkId == 'LIST') { chunk.format = unpackString(buffer, index + 8, 4); head_ += 4; chunk.subChunks = getSubChunksIndex_(buffer); } else { let realChunkSize = chunk.chunkSize % 2 ? chunk.chunkSize + 1 : chunk.chunkSize; head_ = index + 8 + realChunkSize; chunk.chunkData = { start: index + 8, end: head_ }; } return chunk; } /** * Return the fourCC_ of a chunk. * @param {!Uint8Array} buffer the RIFF file bytes. * @param {number} index The start index of the chunk. * @return {string} The id of the chunk. * @private */ function getChunkId_(buffer, index) { head_ += 4; return unpackString(buffer, index, 4); } /** * Return the size of a chunk. * @param {!Uint8Array} buffer the RIFF file bytes. * @param {number} index The start index of the chunk. * @return {number} The size of the chunk without the id and size fields. * @private */ function getChunkSize_(buffer, index) { head_ += 4; return unpackFrom(buffer, uInt32_, index + 4); } export { riffChunks, findChunk };