UNPKG

@jioffe/arraybufferstream

Version:

Utility for read and write operations on ArrayBuffer instances

639 lines (550 loc) 19.2 kB
const BYTE_TO_NORM = 1 / 0xFF; const SHORT_TO_NORM = 1 / 0xFFFF; /** * Wraps an ArrayBuffer object with a stream-like interface * for read and write operations. */ class ArrayBufferStream{ /** * Constructs an ArrayBufferStream * @param {(number|ArrayBufferLike|object)} arg Size of ArrayBuffer to allocate or existing allocation * @param {boolean} [littleEndian=false] Whether to use "Little Endian" for byte order */ constructor(arg, littleEndian){ const numValue = +arg; /** * Underlying ArrayBuffer instance with data for read/write * @type {ArrayBuffer} */ this.buffer = null; if(!isNaN(numValue)){ // Numeric value for size this.buffer = new ArrayBuffer(Math.floor(numValue)); }else if(typeof arg === 'object'){ if(Object.prototype.toString(arg).indexOf('ArrayBuffer') >= 0){ // ArrayBuffer object passed directly this.buffer = arg; }else if(Object.prototype.toString(arg.data).indexOf('ArrayBuffer') >= 0){ // Node Buffer is passed const {buffer, byteOffset, byteLength} = arg; if(!isNaN(+byteOffset) && !isNaN(+byteLength)){ this.buffer = buffer.slice(byteOffset, byteOffset + byteLength); } } } if(!this.buffer) throw `Unsupported: [${typeof arg}] ${arg}`; /** * Size of underlying buffer in bytes * @type {number} */ this.size = this.buffer.byteLength; /** * Current read/write offset in bytes * @type {number} */ this.cursor = 0; /** * @type {DataView} */ this.dv = new DataView(this.buffer); /** * @type {boolean} */ this.littleEndian = !!littleEndian; } /** * Sets read/write cursor at specified location * @param {number} cursor */ setCursor(cursor){ const i = +cursor; if(isNaN(i)){ throw `Invalid value for cursor: ${cursor}`; } if(i > this.size || i < 0){ throw `Cursor out of range (cursor: ${cursor}, length: ${this.size})`; } this.cursor = Math.floor(i); } /** * Adds numeric value(s) as UINT8, advances cursor * @param {...number} val */ writeUint8(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint8(this.cursor++, val[i]); } } /** * Adds numeric value(s) as UINT8, advances cursor. * Value is clamped to UINT8 range * @param {...number} val */ writeUint8Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint8(this.cursor++, Math.max(0, Math.min(0xFF, val[i]))); } } /** * Reads single UINT8 as number, advances cursor */ getNextUint8(){ return this.dv.getUint8(this.cursor++); } /** * Reads a series of UINT8 values into a destination buffer. * If a buffer is not provided, then a UInt8Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextUint8Array(length, dest, offset){ dest = dest || new Uint8Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getUint8(this.cursor++); } return dest; } /** * Adds numeric value(s) as INT8, advances cursor * @param {...number} val */ writeInt8(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt8(this.cursor++, val[i]); } } /** * Adds numeric value(s) as INT8, advances cursor. * Value is clamped to INT8 range * @param {...number} val */ writeInt8Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt8(this.cursor++, Math.max(-128, Math.min(0x7F, val[i]))); } } /** * Reads INT8 as number, advances cursor */ getNextInt8(){ return this.dv.getInt8(this.cursor++); } /** * Reads a series of INT8 values into a destination buffer. * If a buffer is not provided, then new Int8Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextInt8Array(length, dest, offset){ dest = dest || new Int8Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getInt8(this.cursor++); } return dest; } /** * Adds numeric value(s) as UINT16, advances cursor * @param {...number} val */ writeUint16(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint16(this.cursor, val[i], this.littleEndian); this.cursor += 2; } } /** * Adds numeric value(s) as UINT16, advances cursor. * Value is clamped to UINT16 range * @param {...number} val */ writeUint16Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint16(this.cursor, Math.max(0, Math.min(0xFFFF, val[i])), this.littleEndian); this.cursor += 2; } } /** * Reads UINT16 as number, advances cursor */ getNextUint16(){ const val = this.dv.getUint16(this.cursor, this.littleEndian); this.cursor += 2; return val; } /** * Reads a series of UINT16 values into a destination buffer. * If a buffer is not provided, then new Uint16Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextUint16Array(length, dest, offset){ dest = dest || new Uint16Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getUint16(this.cursor, this.littleEndian); this.cursor += 2; } return dest; } /** * Adds numeric value(s) as INT16, advances cursor * @param {...number} val */ writeInt16(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt16(this.cursor, val[i], this.littleEndian); this.cursor += 2; } } /** * Adds numeric value(s) as INT16, advances cursor. * Value is clamped to INT16 range * @param {...number} val */ writeInt16Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt16(this.cursor, Math.max(-32768, Math.min(0x7FFF, val[i])), this.littleEndian); this.cursor += 2; } } /** * Reads INT16 as number, advances cursor */ getNextInt16(){ const val = this.dv.getInt16(this.cursor, this.littleEndian); this.cursor += 2; return val; } /** * Reads a series of INT16 values into a destination buffer. * If a buffer is not provided, then new Int16Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextInt16Array(length, dest, offset){ dest = dest || new Int16Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getInt16(this.cursor, this.littleEndian); this.cursor += 2; } return dest; } /** * Adds numeric value(s) as UINT32, advances cursor * @param {...number} val */ writeUint32(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint32(this.cursor, val[i], this.littleEndian); this.cursor += 4; } } /** * Adds numeric value(s) as INT32, advances cursor. * Value is clamped to INT32 range * @param {...number} val */ writeUint32Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint32(this.cursor, Math.max(0, Math.min(0xFFFFFFFF, val[i])), this.littleEndian); this.cursor += 4; } } /** * Reads UINT32 as number, advances cursor */ getNextUint32(){ const val = this.dv.getUint32(this.cursor, this.littleEndian); this.cursor += 4; return val; } /** * Reads a series of UINT32 values into a destination buffer. * If a buffer is not provided, then new Uint32Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextUint32Array(length, dest, offset){ dest = dest || new Uint32Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getUint32(this.cursor, this.littleEndian); this.cursor += 4; } return dest; } /** * Adds numeric value(s) as INT32, advances cursor * @param {...number} val */ writeInt32(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt32(this.cursor, val[i], this.littleEndian); this.cursor += 4; } } /** * Adds numeric value(s) as INT32, advances cursor. * Value is clamped to INT32 range * @param {...number} val */ writeInt32Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setInt32(this.cursor, Math.max(-2147483648, Math.min(0x7FFFFFFF, val[i])), this.littleEndian); this.cursor += 4; } } /** * Reads INT32 as number, advances cursor */ getNextInt32(){ const val = this.dv.getInt32(this.cursor, this.littleEndian); this.cursor += 4; return val; } /** * Reads a series of INT16 values into a destination buffer. * If a buffer is not provided, then new Int32Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextInt32Array(length, dest, offset){ dest = dest || new Int32Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getInt32(this.cursor, this.littleEndian); this.cursor += 4; } return dest; } /** * Encodes a float between 0-1 as UINT8, advances cursor * @param {...number} val */ writeUNorm8(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint8(this.cursor++, Math.floor(val[i] * 0xFF), this.littleEndian); } } /** * Encodes a float between 0-1 (clamped) as UINT8, advances cursor * @param {...number} val */ writeUNorm8Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint8(this.cursor++, Math.min(0xFF, Math.max(0, Math.floor(val[i] * 0xFF))), this.littleEndian); } } /** * Reads float between 0-1 encoded as a UINT8, advances cursor */ getNextUNorm8(){ return this.dv.getUint8(this.cursor++, this.littleEndian) * BYTE_TO_NORM; } /** * Reads a series of UNORM8 values into a destination buffer. * If a buffer is not provided, then new Float32Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextUNorm8Array(length, dest, offset){ dest = dest || new Float32Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getUint8(this.cursor++, this.littleEndian) * BYTE_TO_NORM; } return dest; } /** * Encodes a float between 0-1 as UINT16, advances cursor * @param {...number} val */ writeUNorm16(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint16(this.cursor, Math.floor(val[i] * 0xFFFF), this.littleEndian); this.cursor += 2; } } /** * Encodes a float between 0-1 (clamped) as UINT16, advances cursor * @param {...number} val */ writeUNorm16Clamped(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setUint16(this.cursor, Math.min(0xFFFF, Math.max(0, Math.floor(val[i] * 0xFFFF))), this.littleEndian); this.cursor += 2; } } /** * Reads float between 0-1 encoded as a UINT16, advances cursor */ getNextUNorm16(){ const val = this.dv.getUint16(this.cursor, this.littleEndian); this.cursor += 2; return val * SHORT_TO_NORM; } /** * Reads a series of UNORM16 values into a destination buffer. * If a buffer is not provided, then new Float32Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextUNorm16Array(length, dest, offset){ dest = dest || new Float32Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getUint16(this.cursor, this.littleEndian) * SHORT_TO_NORM; this.cursor += 2; } return dest; } /** * Adds numeric value(s) as FLOAT32, advances cursor * @param {...number} val */ writeFloat32(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setFloat32(this.cursor, val[i], this.littleEndian); this.cursor += 4; } } /** * Reads FLOAT32 as number, advances cursor */ getNextFloat32(){ const val = this.dv.getFloat32(this.cursor, this.littleEndian); this.cursor += 4; return val; } /** * Reads a series of FLOAT32 values into a destination buffer. * If a buffer is not provided, then new Float32Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextFloat32Array(length, dest, offset){ dest = dest || new Float32Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getFloat32(this.cursor, this.littleEndian); this.cursor += 4; } return dest; } /** * Adds numeric value(s) as FLOAT64, advances cursor * @param {...number} val */ writeFloat64(...val){ const n = val?.length; for(let i = 0; i < n; ++i){ this.dv.setFloat64(this.cursor, val[i], this.littleEndian); this.cursor += 8; } } /** * Reads FLOAT32 as number, advances cursor */ getNextFloat64(){ const val = this.dv.getFloat64(this.cursor, this.littleEndian); this.cursor += 8; return val; } /** * Reads a series of FLOAT64 values into a destination buffer. * If a buffer is not provided, then new Float64Array will be created. * @param {number} length number of values to read * @param {ArrayLike<number>} [dest=null] destination buffer * @param {number} [offset=0] write offset in destination buffer * @returns */ getNextFloat64Array(length, dest, offset){ dest = dest || new Float64Array(length); offset = Math.floor(offset || 0); for(let i = 0; i < length; ++i){ dest[i + offset] = this.dv.getFloat64(this.cursor, this.littleEndian); this.cursor += 8; } return dest; } /** * Encodes a string as a series of single-byte characters with a null-terminator. * These values are limited to the ASCII character range and results may be unpredictable * if the string contains multi-byte characters. * * The cursor is advanced by the length of the string + 1. * @param {string} str * @returns */ writeASCIIString(str){ const len = +str?.length; for(let i = 0; i < len; ++i){ this.dv.setUint8(this.cursor++, str.charCodeAt(i)); } this.dv.setUint8(this.cursor++, 0x00); } /** * Reads a null-terminated string of UINT8 characters, advances cursor * @returns {string} */ getNextASCIIString(){ const {size, dv} = this, lineBuffer = []; let i = this.cursor; while(i < size){ let byte = dv.getUint8(i++); if(!byte) break; lineBuffer.push(byte); } const val = String.fromCharCode(...lineBuffer); this.cursor = i; return val; } /** * Returns a copy of the underlying data buffer up to the current cursor. * @returns {ArrayBuffer} */ trimToCursor(){ return this.buffer.slice(0, this.cursor); } } export default ArrayBufferStream;