@jioffe/arraybufferstream
Version:
Utility for read and write operations on ArrayBuffer instances
639 lines (550 loc) • 19.2 kB
JavaScript
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;