From 24b1603968e3bf47bb2e381c76465fa361b2d2bc Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 20 Dec 2021 20:35:59 -0800 Subject: [PATCH] Add a BitBuffer to the io package and a few tests --- build/io/Makefile | 19 ++++ build/io/bitbuffer-def.js | 200 +++++++++++++++++++++++++++++++++++ build/io/bitstream-def.js | 15 +-- build/io/bytebuffer-def.js | 4 +- io/bitbuffer-worker.js | 204 ++++++++++++++++++++++++++++++++++++ io/bitbuffer.js | 202 +++++++++++++++++++++++++++++++++++ io/bitstream-worker.js | 15 +-- io/bitstream.js | 15 +-- io/bytebuffer-worker.js | 4 +- io/bytebuffer.js | 4 +- tests/io-bitbuffer.spec.js | 88 ++++++++++++++++ tests/io-bytebuffer.spec.js | 2 +- 12 files changed, 747 insertions(+), 25 deletions(-) create mode 100644 build/io/bitbuffer-def.js create mode 100644 io/bitbuffer-worker.js create mode 100644 io/bitbuffer.js create mode 100644 tests/io-bitbuffer.spec.js diff --git a/build/io/Makefile b/build/io/Makefile index 43dcd9f..dbe4c30 100644 --- a/build/io/Makefile +++ b/build/io/Makefile @@ -6,17 +6,22 @@ BITSTREAM_WORKER=${OUT_PATH}/bitstream-worker.js BYTESTREAM_MODULE=${OUT_PATH}/bytestream.js BYTESTREAM_WORKER=${OUT_PATH}/bytestream-worker.js +BITBUFFER_MODULE=${OUT_PATH}/bitbuffer.js +BITBUFFER_WORKER=${OUT_PATH}/bitbuffer-worker.js + BYTEBUFFER_MODULE=${OUT_PATH}/bytebuffer.js BYTEBUFFER_WORKER=${OUT_PATH}/bytebuffer-worker.js BITSTREAM_DEF=bitstream-def.js BYTESTREAM_DEF=bytestream-def.js +BITBUFFER_DEF=bitbuffer-def.js BYTEBUFFER_DEF=bytebuffer-def.js DISCLAIMER="// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io." all: ${BITSTREAM_MODULE} ${BITSTREAM_WORKER} \ ${BYTESTREAM_MODULE} ${BYTESTREAM_WORKER} \ + ${BITBUFFER_MODULE} ${BITBUFFER_WORKER} \ ${BYTEBUFFER_MODULE} ${BYTEBUFFER_WORKER} clean: @@ -24,6 +29,8 @@ clean: rm -rf ${BITSTREAM_WORKER} rm -rf ${BYTESTREAM_MODULE} rm -rf ${BYTESTREAM_WORKER} + rm -rf ${BITBUFFER_MODULE} + rm -rf ${BITBUFFER_WORKER} rm -rf ${BYTEBUFFER_MODULE} rm -rf ${BYTEBUFFER_WORKER} @@ -51,6 +58,18 @@ ${BYTESTREAM_WORKER}: Makefile ${BYTESTREAM_DEF} echo "bitjs.io.ByteStream =" >> ${BYTESTREAM_WORKER} cat ${BYTESTREAM_DEF} >> ${BYTESTREAM_WORKER} +${BITBUFFER_MODULE}: Makefile ${BITBUFFER_DEF} + echo ${DISCLAIMER} > ${BITBUFFER_MODULE} + echo "export const BitBuffer =" >> ${BITBUFFER_MODULE} + cat ${BITBUFFER_DEF} >> ${BITBUFFER_MODULE} + +${BITBUFFER_WORKER}: Makefile ${BITBUFFER_DEF} + echo ${DISCLAIMER} > ${BITBUFFER_WORKER} + echo "var bitjs = bitjs || {};" >> ${BITBUFFER_WORKER} + echo "bitjs.io = bitjs.io || {};" >> ${BITBUFFER_WORKER} + echo "bitjs.io.BitBuffer =" >> ${BITBUFFER_WORKER} + cat ${BITBUFFER_DEF} >> ${BITBUFFER_WORKER} + ${BYTEBUFFER_MODULE}: Makefile ${BYTEBUFFER_DEF} echo ${DISCLAIMER} > ${BYTEBUFFER_MODULE} echo "export const ByteBuffer =" >> ${BYTEBUFFER_MODULE} diff --git a/build/io/bitbuffer-def.js b/build/io/bitbuffer-def.js new file mode 100644 index 0000000..55c45b3 --- /dev/null +++ b/build/io/bitbuffer-def.js @@ -0,0 +1,200 @@ +/* + * bytebuffer-def.js + * + * Provides a writer for bits. + * + * Licensed under the MIT License + * + * Copyright(c) 2021 Google Inc. + */ + +(function () { + const BITMASK = [ + 0, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, + ] + + /** + * A write-only Bit buffer which uses a Uint8Array as a backing store. + */ + class BitBuffer { + /** + * @param {number} numBytes The number of bytes to allocate. + * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to + * least-significant (0). Defaults false: least-significant (0) to most-significant (8). + */ + constructor(numBytes, mtl = false) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; + } + + /** + * @type {Uint8Array} + * @public + */ + this.data = new Uint8Array(numBytes); + + /** + * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most + * significant bit packing). + * @type {boolean} + * @private + */ + this.mtl = mtl; + + /** + * The current byte we are filling with bits. + * @type {number} + * @public + */ + this.bytePtr = 0; + + /** + * Points at the bit within the current byte where the next bit will go. This number ranges + * from 0 to 7 and the direction of packing is indicated by the mtl property. + * @type {number} + * @public + */ + this.bitPtr = this.mtl ? 7 : 0; + } + + /** @returns {boolean} */ + getPackingDirection() { + return this.mtl; + } + + /** + * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to + * most-significant (7). Changing the bit-packing direction when the bit pointer is in the + * middle of a byte will fill the rest of that byte with 0s using the current bit-packing + * direction and then set the bit pointer to the appropriate bit of the next byte. If there + * are no more bytes left in this buffer, it will throw an error. + */ + setPackingDirection(mtl = false) { + if (this.mtl !== mtl) { + if (this.mtl && this.bitPtr !== 7) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 7; + } else if (!this.mtl && this.bitPtr !== 0) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 0; + } + } + + this.mtl = mtl; + } + + /** + * writeBits(3, 6) is the same as writeBits(0b000011, 6). + * Will throw an error (without writing) if this would over-flow the buffer. + * @param {number} val The bits to pack into the buffer. Negative values are not allowed. + * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since + * JavaScript can only support 53-bit integers. + */ + writeBits(val, numBits) { + if (val < 0 || typeof val !== typeof 1) { + throw `Trying to write an invalid value into the BitBuffer: ${val}`; + } + if (numBits < 0 || numBits > 53) { + throw `Trying to write ${numBits} bits into the BitBuffer`; + } + + const totalBitsInBuffer = this.data.byteLength * 8; + const writtenBits = this.bytePtr * 8 + this.bitPtr; + const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; + if (numBits > bitsLeftInBuffer) { + throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; + } + + // Least-to-most-significant bit packing method (LTM). + if (!this.mtl) { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = 8 - this.bitPtr; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in LTM bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at LSB. */ + let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + actualBitsToWrite <<= this.bitPtr; + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write. + this.bitPtr += numBitsToWriteIntoThisByte; + if (this.bitPtr > 7) { + if (this.bitPtr !== 8) { + throw `Error in LTM bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 0; + } + // Remove bits that have been written from LSB end. + val >>= numBitsToWriteIntoThisByte; + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + // Most-to-least-significant bit packing method (MTL). + else { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = this.bitPtr + 1; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in MTL bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at MSB. */ + let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + // If the number of bits left to write do not fill up this byte, we need to shift these + // bits to the left so they are written into the proper place in the buffer. + if (numBitsLeftToWrite < bitCapacityInThisByte) { + actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); + } + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write + this.bitPtr -= numBitsToWriteIntoThisByte; + if (this.bitPtr < 0) { + if (this.bitPtr !== -1) { + throw `Error in MTL bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 7; + } + // Remove bits that have been written from MSB end. + val -= (actualBitsToWrite << numExcessBits); + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + } + } + + return BitBuffer; +})(); diff --git a/build/io/bitstream-def.js b/build/io/bitstream-def.js index 606a279..19ddc5c 100644 --- a/build/io/bitstream-def.js +++ b/build/io/bitstream-def.js @@ -22,12 +22,13 @@ class BitStream { /** * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} rtl Whether the stream reads bits from the byte starting - * from bit 7 to 0 (true) or bit 0 to 7 (false). + * @param {boolean} mtl Whether the stream reads bits from the byte starting with the + * most-significant-bit (bit 7) to least-significant (bit 0). False means the direciton is + * from least-significant-bit (bit 0) to most-significant (bit 7). * @param {Number} opt_offset The offset into the ArrayBuffer * @param {Number} opt_length The length of this BitStream */ - constructor(ab, rtl, opt_offset, opt_length) { + constructor(ab, mtl, opt_offset, opt_length) { if (!(ab instanceof ArrayBuffer)) { throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; } @@ -63,7 +64,7 @@ */ this.bitsRead_ = 0; - this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; + this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; } /** @@ -85,13 +86,13 @@ * byte0 byte1 byte2 byte3 * 7......0 | 7......0 | 7......0 | 7......0 * - * The bit pointer starts at bit0 of byte0 and moves left until it reaches + * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches * bit7 of byte0, then jumps to bit0 of byte1, etc. * @param {number} n The number of bits to peek, must be a positive integer. * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_ltr(n, opt_movePointers) { + peekBits_ltm(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { @@ -149,7 +150,7 @@ * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_rtl(n, opt_movePointers) { + peekBits_mtl(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { diff --git a/build/io/bytebuffer-def.js b/build/io/bytebuffer-def.js index 276e5d7..4f010f8 100644 --- a/build/io/bytebuffer-def.js +++ b/build/io/bytebuffer-def.js @@ -21,11 +21,13 @@ if (typeof numBytes != typeof 1 || numBytes <= 0) { throw "Error! ByteBuffer initialized with '" + numBytes + "'"; } + /** * @type {Uint8Array} * @public */ this.data = new Uint8Array(numBytes); + /** * @type {number} * @public @@ -118,7 +120,7 @@ } this.insertByte(curByte); } - }; + } } return ByteBuffer; diff --git a/io/bitbuffer-worker.js b/io/bitbuffer-worker.js new file mode 100644 index 0000000..191439d --- /dev/null +++ b/io/bitbuffer-worker.js @@ -0,0 +1,204 @@ +// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. +var bitjs = bitjs || {}; +bitjs.io = bitjs.io || {}; +bitjs.io.BitBuffer = +/* + * bytebuffer-def.js + * + * Provides a writer for bits. + * + * Licensed under the MIT License + * + * Copyright(c) 2021 Google Inc. + */ + +(function () { + const BITMASK = [ + 0, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, + ] + + /** + * A write-only Bit buffer which uses a Uint8Array as a backing store. + */ + class BitBuffer { + /** + * @param {number} numBytes The number of bytes to allocate. + * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to + * least-significant (0). Defaults false: least-significant (0) to most-significant (8). + */ + constructor(numBytes, mtl = false) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; + } + + /** + * @type {Uint8Array} + * @public + */ + this.data = new Uint8Array(numBytes); + + /** + * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most + * significant bit packing). + * @type {boolean} + * @private + */ + this.mtl = mtl; + + /** + * The current byte we are filling with bits. + * @type {number} + * @public + */ + this.bytePtr = 0; + + /** + * Points at the bit within the current byte where the next bit will go. This number ranges + * from 0 to 7 and the direction of packing is indicated by the mtl property. + * @type {number} + * @public + */ + this.bitPtr = this.mtl ? 7 : 0; + } + + /** @returns {boolean} */ + getPackingDirection() { + return this.mtl; + } + + /** + * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to + * most-significant (7). Changing the bit-packing direction when the bit pointer is in the + * middle of a byte will fill the rest of that byte with 0s using the current bit-packing + * direction and then set the bit pointer to the appropriate bit of the next byte. If there + * are no more bytes left in this buffer, it will throw an error. + */ + setPackingDirection(mtl = false) { + if (this.mtl !== mtl) { + if (this.mtl && this.bitPtr !== 7) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 7; + } else if (!this.mtl && this.bitPtr !== 0) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 0; + } + } + + this.mtl = mtl; + } + + /** + * writeBits(3, 6) is the same as writeBits(0b000011, 6). + * Will throw an error (without writing) if this would over-flow the buffer. + * @param {number} val The bits to pack into the buffer. Negative values are not allowed. + * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since + * JavaScript can only support 53-bit integers. + */ + writeBits(val, numBits) { + if (val < 0 || typeof val !== typeof 1) { + throw `Trying to write an invalid value into the BitBuffer: ${val}`; + } + if (numBits < 0 || numBits > 53) { + throw `Trying to write ${numBits} bits into the BitBuffer`; + } + + const totalBitsInBuffer = this.data.byteLength * 8; + const writtenBits = this.bytePtr * 8 + this.bitPtr; + const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; + if (numBits > bitsLeftInBuffer) { + throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; + } + + // Least-to-most-significant bit packing method (LTM). + if (!this.mtl) { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = 8 - this.bitPtr; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in LTM bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at LSB. */ + let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + actualBitsToWrite <<= this.bitPtr; + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write. + this.bitPtr += numBitsToWriteIntoThisByte; + if (this.bitPtr > 7) { + if (this.bitPtr !== 8) { + throw `Error in LTM bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 0; + } + // Remove bits that have been written from LSB end. + val >>= numBitsToWriteIntoThisByte; + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + // Most-to-least-significant bit packing method (MTL). + else { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = this.bitPtr + 1; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in MTL bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at MSB. */ + let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + // If the number of bits left to write do not fill up this byte, we need to shift these + // bits to the left so they are written into the proper place in the buffer. + if (numBitsLeftToWrite < bitCapacityInThisByte) { + actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); + } + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write + this.bitPtr -= numBitsToWriteIntoThisByte; + if (this.bitPtr < 0) { + if (this.bitPtr !== -1) { + throw `Error in MTL bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 7; + } + // Remove bits that have been written from MSB end. + val -= (actualBitsToWrite << numExcessBits); + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + } + } + + return BitBuffer; +})(); diff --git a/io/bitbuffer.js b/io/bitbuffer.js new file mode 100644 index 0000000..836b4d1 --- /dev/null +++ b/io/bitbuffer.js @@ -0,0 +1,202 @@ +// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. +export const BitBuffer = +/* + * bytebuffer-def.js + * + * Provides a writer for bits. + * + * Licensed under the MIT License + * + * Copyright(c) 2021 Google Inc. + */ + +(function () { + const BITMASK = [ + 0, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, + ] + + /** + * A write-only Bit buffer which uses a Uint8Array as a backing store. + */ + class BitBuffer { + /** + * @param {number} numBytes The number of bytes to allocate. + * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to + * least-significant (0). Defaults false: least-significant (0) to most-significant (8). + */ + constructor(numBytes, mtl = false) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; + } + + /** + * @type {Uint8Array} + * @public + */ + this.data = new Uint8Array(numBytes); + + /** + * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most + * significant bit packing). + * @type {boolean} + * @private + */ + this.mtl = mtl; + + /** + * The current byte we are filling with bits. + * @type {number} + * @public + */ + this.bytePtr = 0; + + /** + * Points at the bit within the current byte where the next bit will go. This number ranges + * from 0 to 7 and the direction of packing is indicated by the mtl property. + * @type {number} + * @public + */ + this.bitPtr = this.mtl ? 7 : 0; + } + + /** @returns {boolean} */ + getPackingDirection() { + return this.mtl; + } + + /** + * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to + * most-significant (7). Changing the bit-packing direction when the bit pointer is in the + * middle of a byte will fill the rest of that byte with 0s using the current bit-packing + * direction and then set the bit pointer to the appropriate bit of the next byte. If there + * are no more bytes left in this buffer, it will throw an error. + */ + setPackingDirection(mtl = false) { + if (this.mtl !== mtl) { + if (this.mtl && this.bitPtr !== 7) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 7; + } else if (!this.mtl && this.bitPtr !== 0) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 0; + } + } + + this.mtl = mtl; + } + + /** + * writeBits(3, 6) is the same as writeBits(0b000011, 6). + * Will throw an error (without writing) if this would over-flow the buffer. + * @param {number} val The bits to pack into the buffer. Negative values are not allowed. + * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since + * JavaScript can only support 53-bit integers. + */ + writeBits(val, numBits) { + if (val < 0 || typeof val !== typeof 1) { + throw `Trying to write an invalid value into the BitBuffer: ${val}`; + } + if (numBits < 0 || numBits > 53) { + throw `Trying to write ${numBits} bits into the BitBuffer`; + } + + const totalBitsInBuffer = this.data.byteLength * 8; + const writtenBits = this.bytePtr * 8 + this.bitPtr; + const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; + if (numBits > bitsLeftInBuffer) { + throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; + } + + // Least-to-most-significant bit packing method (LTM). + if (!this.mtl) { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = 8 - this.bitPtr; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in LTM bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at LSB. */ + let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + actualBitsToWrite <<= this.bitPtr; + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write. + this.bitPtr += numBitsToWriteIntoThisByte; + if (this.bitPtr > 7) { + if (this.bitPtr !== 8) { + throw `Error in LTM bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 0; + } + // Remove bits that have been written from LSB end. + val >>= numBitsToWriteIntoThisByte; + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + // Most-to-least-significant bit packing method (MTL). + else { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = this.bitPtr + 1; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in MTL bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at MSB. */ + let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + // If the number of bits left to write do not fill up this byte, we need to shift these + // bits to the left so they are written into the proper place in the buffer. + if (numBitsLeftToWrite < bitCapacityInThisByte) { + actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); + } + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write + this.bitPtr -= numBitsToWriteIntoThisByte; + if (this.bitPtr < 0) { + if (this.bitPtr !== -1) { + throw `Error in MTL bit packing. Tried to write more bits than it should have.`; + } + this.bytePtr++; + this.bitPtr = 7; + } + // Remove bits that have been written from MSB end. + val -= (actualBitsToWrite << numExcessBits); + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + } + } + } + } + + return BitBuffer; +})(); diff --git a/io/bitstream-worker.js b/io/bitstream-worker.js index 90500a9..32a6772 100644 --- a/io/bitstream-worker.js +++ b/io/bitstream-worker.js @@ -26,12 +26,13 @@ bitjs.io.BitStream = class BitStream { /** * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} rtl Whether the stream reads bits from the byte starting - * from bit 7 to 0 (true) or bit 0 to 7 (false). + * @param {boolean} mtl Whether the stream reads bits from the byte starting with the + * most-significant-bit (bit 7) to least-significant (bit 0). False means the direciton is + * from least-significant-bit (bit 0) to most-significant (bit 7). * @param {Number} opt_offset The offset into the ArrayBuffer * @param {Number} opt_length The length of this BitStream */ - constructor(ab, rtl, opt_offset, opt_length) { + constructor(ab, mtl, opt_offset, opt_length) { if (!(ab instanceof ArrayBuffer)) { throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; } @@ -67,7 +68,7 @@ bitjs.io.BitStream = */ this.bitsRead_ = 0; - this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; + this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; } /** @@ -89,13 +90,13 @@ bitjs.io.BitStream = * byte0 byte1 byte2 byte3 * 7......0 | 7......0 | 7......0 | 7......0 * - * The bit pointer starts at bit0 of byte0 and moves left until it reaches + * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches * bit7 of byte0, then jumps to bit0 of byte1, etc. * @param {number} n The number of bits to peek, must be a positive integer. * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_ltr(n, opt_movePointers) { + peekBits_ltm(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { @@ -153,7 +154,7 @@ bitjs.io.BitStream = * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_rtl(n, opt_movePointers) { + peekBits_mtl(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { diff --git a/io/bitstream.js b/io/bitstream.js index 525cf7e..afa60f0 100644 --- a/io/bitstream.js +++ b/io/bitstream.js @@ -24,12 +24,13 @@ export const BitStream = class BitStream { /** * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} rtl Whether the stream reads bits from the byte starting - * from bit 7 to 0 (true) or bit 0 to 7 (false). + * @param {boolean} mtl Whether the stream reads bits from the byte starting with the + * most-significant-bit (bit 7) to least-significant (bit 0). False means the direciton is + * from least-significant-bit (bit 0) to most-significant (bit 7). * @param {Number} opt_offset The offset into the ArrayBuffer * @param {Number} opt_length The length of this BitStream */ - constructor(ab, rtl, opt_offset, opt_length) { + constructor(ab, mtl, opt_offset, opt_length) { if (!(ab instanceof ArrayBuffer)) { throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; } @@ -65,7 +66,7 @@ export const BitStream = */ this.bitsRead_ = 0; - this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; + this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; } /** @@ -87,13 +88,13 @@ export const BitStream = * byte0 byte1 byte2 byte3 * 7......0 | 7......0 | 7......0 | 7......0 * - * The bit pointer starts at bit0 of byte0 and moves left until it reaches + * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches * bit7 of byte0, then jumps to bit0 of byte1, etc. * @param {number} n The number of bits to peek, must be a positive integer. * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_ltr(n, opt_movePointers) { + peekBits_ltm(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { @@ -151,7 +152,7 @@ export const BitStream = * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - peekBits_rtl(n, opt_movePointers) { + peekBits_mtl(n, opt_movePointers) { const NUM = parseInt(n, 10); let num = NUM; if (n !== num || num <= 0) { diff --git a/io/bytebuffer-worker.js b/io/bytebuffer-worker.js index 83f33c6..0f6212b 100644 --- a/io/bytebuffer-worker.js +++ b/io/bytebuffer-worker.js @@ -25,11 +25,13 @@ bitjs.io.ByteBuffer = if (typeof numBytes != typeof 1 || numBytes <= 0) { throw "Error! ByteBuffer initialized with '" + numBytes + "'"; } + /** * @type {Uint8Array} * @public */ this.data = new Uint8Array(numBytes); + /** * @type {number} * @public @@ -122,7 +124,7 @@ bitjs.io.ByteBuffer = } this.insertByte(curByte); } - }; + } } return ByteBuffer; diff --git a/io/bytebuffer.js b/io/bytebuffer.js index 4ab547c..68a7d35 100644 --- a/io/bytebuffer.js +++ b/io/bytebuffer.js @@ -23,11 +23,13 @@ export const ByteBuffer = if (typeof numBytes != typeof 1 || numBytes <= 0) { throw "Error! ByteBuffer initialized with '" + numBytes + "'"; } + /** * @type {Uint8Array} * @public */ this.data = new Uint8Array(numBytes); + /** * @type {number} * @public @@ -120,7 +122,7 @@ export const ByteBuffer = } this.insertByte(curByte); } - }; + } } return ByteBuffer; diff --git a/tests/io-bitbuffer.spec.js b/tests/io-bitbuffer.spec.js new file mode 100644 index 0000000..ba5c8fb --- /dev/null +++ b/tests/io-bitbuffer.spec.js @@ -0,0 +1,88 @@ +/* + * io-bitbuffer.spec.js + * + * Licensed under the MIT License + * + * Copyright(c) 2021 Google Inc. + */ + +import { BitBuffer } from '../io/bitbuffer.js'; +import 'mocha'; +import { expect } from 'chai'; + +describe('bitjs.io.BitBuffer', () => { + let buffer; + + describe('least-to-most-significant bit-packing', () => { + beforeEach(() => { + buffer = new BitBuffer(4); + }); + + it('bit/byte pointers initialized properly', () => { + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(0); + }) + + it('write multiple bits', () => { + buffer.writeBits(0b01011, 5); // Should result in: 0b00001011. + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(0 + 5); + expect(Array.from(buffer.data)).to.have.ordered.members([1 + 2 + 8, 0, 0, 0]); + }); + + it('write multiple bits across byte boundary', () => { + buffer.writeBits(0b01011, 5); + buffer.writeBits(0b11101, 5); // Should result in: 0b10101011 and 0b00000011. + expect(buffer.bytePtr).equals(Math.floor((5 + 5) / 8)); + expect(buffer.bitPtr).equals((5 + 5) % 8); + expect(Array.from(buffer.data)).to.have.ordered.members( + [1 + 2 + 8 + 32 + 128, 1 + 2, 0, 0]); + }); + + it('write multiple bytes to buffer', () => { + buffer.writeBits(0, 1); + buffer.writeBits(0x1ffff, 17); // Should result in 0b111111110, 0b11111111, 0b00000011. + expect(buffer.bytePtr).equals(2); + expect(buffer.bitPtr).equals(2); + expect(Array.from(buffer.data)).to.have.ordered.members( + [0xfe, 0xff, 0x03, 0x00]); + }); + }); + + describe('most-to-least-significant bit-packing', () => { + beforeEach(() => { + buffer = new BitBuffer(4, true); + }); + + it('bit/byte pointers initialized properly', () => { + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(7); + }) + + it('write multiple bits', () => { + buffer.writeBits(0b01011, 5); // Should result in: 0b01011000 + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(7 - 5); + expect(Array.from(buffer.data)).to.have.ordered.members( + [64+16+8, 0, 0, 0]); + }); + + it('write multiple bits across byte boundary', () => { + buffer.writeBits(0b01011, 5); + buffer.writeBits(0b11101, 5); // Should result in: 0b01011111 and 0b01000000. + expect(buffer.bytePtr).equals(Math.floor((5 + 5) / 8)); + expect(buffer.bitPtr).equals(7 - ((5 + 5) % 8)); + expect(Array.from(buffer.data)).to.have.ordered.members( + [64+16+8+4+2+1, 64, 0, 0]); + }); + + it('write multiple bytes to buffer', () => { + buffer.writeBits(0, 1); + buffer.writeBits(0x1ffff, 17); // Should result in 0b011111111, 0b11111111, 0b11000000. + expect(buffer.bytePtr).equals(2); + expect(buffer.bitPtr).equals(5); + expect(Array.from(buffer.data)).to.have.ordered.members( + [0x7f, 0xff, 0xc0, 0x00]); + }); + }); +}); diff --git a/tests/io-bytebuffer.spec.js b/tests/io-bytebuffer.spec.js index aaa892c..54355d9 100644 --- a/tests/io-bytebuffer.spec.js +++ b/tests/io-bytebuffer.spec.js @@ -12,7 +12,7 @@ import 'mocha'; import { expect } from 'chai'; // TODO: Only test ByteBuffer here. -describe('bitjs.io.ByteBufferBitStream', () => { +describe('bitjs.io.ByteBuffer', () => { let buffer; beforeEach(() => {