carta1 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Carta1 Audio Codec - Bit Allocation Module
3
+ *
4
+ * This module implements the bit allocation algorithm for ATRAC1 compression,
5
+ * distributing available bits among Block Floating Units (BFUs) based on
6
+ * psychoacoustic masking and signal-to-mask ratios.
7
+ */
8
+
9
+ import {
10
+ FRAME_BITS,
11
+ FRAME_OVERHEAD_BITS,
12
+ BITS_PER_BFU_METADATA,
13
+ BFU_AMOUNTS,
14
+ MAX_WORD_LENGTH_INDEX,
15
+ WORD_LENGTH_BITS,
16
+ DISTORTION_DELTA_DB,
17
+ NUM_BFUS,
18
+ SAMPLE_RATE,
19
+ BFU_FREQUENCIES,
20
+ PSYMODEL_MIN_POWER_DB,
21
+ } from '../core/constants.js'
22
+
23
+ /**
24
+ * Allocate bits to BFUs based on psychoacoustic analysis
25
+ * @param {Object} psychoResults - Psychoacoustic analysis results
26
+ * @param {Array} bfuData - BFU coefficient data
27
+ * @param {Array} bfuSizes - Size of each BFU
28
+ * @param {number} maxBfuCount - Maximum number of BFUs to use
29
+ * @returns {Object} Allocation results with BFU count and bit distribution
30
+ */
31
+ export function allocateBits(psychoResults, bfuData, bfuSizes, maxBfuCount) {
32
+ // Calculate signal energy for each BFU
33
+ const signalEnergyDb = calculateBfuEnergy(bfuData, bfuSizes, maxBfuCount)
34
+
35
+ // Map psychoacoustic masking threshold to BFUs
36
+ const maskingThreshold =
37
+ psychoResults.maskingThreshold || psychoResults.globalThreshold
38
+ const maskThresholdDb = mapThresholdToBfus(maskingThreshold)
39
+
40
+ // Calculate Signal-to-Mask Ratios
41
+ const smrValues = calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount)
42
+
43
+ // Determine optimal number of BFUs to use
44
+ const actualBfuCount = selectOptimalBfuCount(
45
+ smrValues,
46
+ signalEnergyDb,
47
+ maxBfuCount
48
+ )
49
+
50
+ // Calculate available bits for allocation
51
+ const availableBits =
52
+ FRAME_BITS - FRAME_OVERHEAD_BITS - actualBfuCount * BITS_PER_BFU_METADATA
53
+
54
+ // Perform actual bit allocation
55
+ const allocation = performBitAllocation(
56
+ actualBfuCount,
57
+ smrValues,
58
+ bfuSizes,
59
+ availableBits
60
+ )
61
+
62
+ return {
63
+ bfuCount: actualBfuCount,
64
+ allocation,
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Calculate energy in dB for each BFU
70
+ * @param {Array} bfuData - BFU coefficient data
71
+ * @param {Array} bfuSizes - Size of each BFU
72
+ * @param {number} bfuCount - Number of BFUs to process
73
+ * @returns {Float32Array} Energy values in dB
74
+ */
75
+ function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
76
+ const energyPerBfu = new Float32Array(bfuCount)
77
+
78
+ for (let i = 0; i < bfuCount; i++) {
79
+ const data = bfuData[i]
80
+ const size = bfuSizes[i]
81
+
82
+ if (size === 0) {
83
+ energyPerBfu[i] = PSYMODEL_MIN_POWER_DB
84
+ continue
85
+ }
86
+
87
+ // Calculate mean square energy
88
+ let energy = 0
89
+ for (let j = 0; j < size; j++) {
90
+ energy += data[j] * data[j]
91
+ }
92
+
93
+ // Convert to dB (10 * log10(energy/size))
94
+ energyPerBfu[i] =
95
+ energy > 0 ? 10 * Math.log10(energy / size) : PSYMODEL_MIN_POWER_DB
96
+ }
97
+
98
+ return energyPerBfu
99
+ }
100
+
101
+ /**
102
+ * Map frequency-domain masking threshold to BFU indices
103
+ * @param {Float32Array} globalThreshold - Frequency-domain masking threshold
104
+ * @returns {Float32Array} Masking threshold per BFU
105
+ */
106
+ function mapThresholdToBfus(globalThreshold) {
107
+ const maskThresholdPerBfu = new Float32Array(NUM_BFUS)
108
+ maskThresholdPerBfu.fill(PSYMODEL_MIN_POWER_DB)
109
+
110
+ // Calculate frequency resolution
111
+ const fftSize = (globalThreshold.length - 1) * 2
112
+ const freqPerBin = SAMPLE_RATE / fftSize
113
+
114
+ // Map each BFU's center frequency to nearest FFT bin
115
+ for (let i = 0; i < NUM_BFUS; i++) {
116
+ const bfuFreq = BFU_FREQUENCIES[i]
117
+ const fftBinIndex = Math.round(bfuFreq / freqPerBin)
118
+
119
+ if (fftBinIndex >= 0 && fftBinIndex < globalThreshold.length) {
120
+ maskThresholdPerBfu[i] = globalThreshold[fftBinIndex]
121
+ }
122
+ }
123
+
124
+ return maskThresholdPerBfu
125
+ }
126
+
127
+ /**
128
+ * Calculate Signal-to-Mask Ratio for each BFU
129
+ * @param {Float32Array} signalEnergyDb - Signal energy per BFU
130
+ * @param {Float32Array} maskThresholdDb - Masking threshold per BFU
131
+ * @param {number} maxBfuCount - Maximum number of BFUs
132
+ * @returns {Float32Array} SMR values in dB
133
+ */
134
+ function calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount) {
135
+ const smrValues = new Float32Array(maxBfuCount)
136
+
137
+ for (let bfu = 0; bfu < maxBfuCount; bfu++) {
138
+ // BFUs with no signal have undefined SMR
139
+ smrValues[bfu] =
140
+ signalEnergyDb[bfu] <= PSYMODEL_MIN_POWER_DB
141
+ ? -Infinity
142
+ : signalEnergyDb[bfu] - maskThresholdDb[bfu]
143
+ }
144
+
145
+ return smrValues
146
+ }
147
+
148
+ /**
149
+ * Select optimal number of BFUs to use based on SMR distribution
150
+ * @param {Float32Array} smrValues - Signal-to-Mask ratios
151
+ * @param {Float32Array} signalEnergyDb - Signal energy per BFU
152
+ * @param {number} maxBfuCount - Maximum available BFUs
153
+ * @returns {number} Optimal BFU count
154
+ */
155
+ function selectOptimalBfuCount(smrValues, signalEnergyDb, maxBfuCount) {
156
+ // Check if we have any audible content (positive SMR)
157
+ const hasAudibleContent = smrValues.some((smr) => smr > 0)
158
+
159
+ if (hasAudibleContent) {
160
+ return selectBfuCountForAudibleContent(smrValues, maxBfuCount)
161
+ } else {
162
+ return selectBfuCountForInaudibleContent(
163
+ smrValues,
164
+ signalEnergyDb,
165
+ maxBfuCount
166
+ )
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Select BFU count when content is above masking threshold
172
+ * Uses diminishing returns approach - find where adding more BFUs
173
+ * provides minimal benefit
174
+ */
175
+ function selectBfuCountForAudibleContent(smrValues, maxBfuCount) {
176
+ // Create sorted list of BFUs by their contribution (positive SMR only)
177
+ const contributions = []
178
+ for (let i = 0; i < maxBfuCount; i++) {
179
+ if (smrValues[i] > 0) {
180
+ contributions.push({ index: i, smr: smrValues[i] })
181
+ }
182
+ }
183
+ contributions.sort((a, b) => b.smr - a.smr)
184
+
185
+ if (contributions.length === 0) {
186
+ return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
187
+ }
188
+
189
+ // Find the natural cutoff using gradient of cumulative SMR
190
+ const cumulativeSMR = []
191
+ let sum = 0
192
+ for (let i = 0; i < contributions.length; i++) {
193
+ sum += contributions[i].smr
194
+ cumulativeSMR.push(sum)
195
+ }
196
+
197
+ // Find where the gradient becomes shallow (diminishing returns)
198
+ for (const candidateCount of BFU_AMOUNTS) {
199
+ if (candidateCount > maxBfuCount) continue
200
+
201
+ // Check if all significant contributions fit within this count
202
+ const requiredBfus = contributions
203
+ .slice(0, Math.min(contributions.length, candidateCount))
204
+ .map((c) => c.index)
205
+
206
+ const maxRequiredIndex = Math.max(...requiredBfus, 0)
207
+
208
+ if (maxRequiredIndex < candidateCount) {
209
+ // Check if we've captured the bulk of the contribution
210
+ const capturedIndex = Math.min(
211
+ candidateCount - 1,
212
+ contributions.length - 1
213
+ )
214
+ if (capturedIndex >= contributions.length - 1) {
215
+ return candidateCount
216
+ }
217
+
218
+ // Use gradient to detect diminishing returns
219
+ const currentValue = cumulativeSMR[capturedIndex]
220
+ const totalValue = cumulativeSMR[cumulativeSMR.length - 1]
221
+ const remainingIndices = contributions.length - capturedIndex - 1
222
+
223
+ if (remainingIndices > 0) {
224
+ const averageRemaining = (totalValue - currentValue) / remainingIndices
225
+ const averageCaptured = currentValue / (capturedIndex + 1)
226
+
227
+ // If remaining BFUs contribute much less on average, we've found our cutoff
228
+ if (averageRemaining < averageCaptured * 0.1) {
229
+ return candidateCount
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
236
+ }
237
+
238
+ /**
239
+ * Select BFU count when all content is below masking threshold
240
+ * Simply finds the smallest count that includes all BFUs with signal
241
+ */
242
+ function selectBfuCountForInaudibleContent(
243
+ smrValues,
244
+ signalEnergyDb,
245
+ maxBfuCount
246
+ ) {
247
+ // Find the highest BFU index that contains actual signal
248
+ let highestSignalBfu = -1
249
+
250
+ for (let i = maxBfuCount - 1; i >= 0; i--) {
251
+ if (signalEnergyDb[i] > PSYMODEL_MIN_POWER_DB && smrValues[i] > -Infinity) {
252
+ highestSignalBfu = i
253
+ break
254
+ }
255
+ }
256
+
257
+ // Find smallest BFU count that includes all signal
258
+ for (const candidateCount of BFU_AMOUNTS) {
259
+ if (candidateCount > highestSignalBfu) {
260
+ return candidateCount
261
+ }
262
+ }
263
+
264
+ return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
265
+ }
266
+
267
+ /**
268
+ * Allocate available bits among BFUs using a greedy algorithm
269
+ * @param {number} actualBfuCount - Number of BFUs to allocate
270
+ * @param {Float32Array} smrValues - Signal-to-Mask ratios
271
+ * @param {Array} bfuSizes - Size of each BFU
272
+ * @param {number} availableBits - Total bits available for allocation
273
+ * @returns {Int32Array} Word length index allocation per BFU
274
+ */
275
+ function performBitAllocation(
276
+ actualBfuCount,
277
+ smrValues,
278
+ bfuSizes,
279
+ availableBits
280
+ ) {
281
+ const allocation = new Int32Array(actualBfuCount).fill(0)
282
+
283
+ // Initialize priority queue with BFUs that have signal
284
+ const priorityQueue = createPriorityQueue(smrValues, actualBfuCount)
285
+
286
+ // Greedy allocation: always upgrade BFU with highest priority
287
+ while (!priorityQueue.isEmpty()) {
288
+ const bestBfu = priorityQueue.peek()
289
+ const currentWl = allocation[bestBfu.bfu]
290
+
291
+ // Check if BFU is already at maximum word length
292
+ if (currentWl >= MAX_WORD_LENGTH_INDEX) {
293
+ priorityQueue.pop()
294
+ continue
295
+ }
296
+
297
+ // Calculate cost to upgrade this BFU
298
+ const upgradeCost = calculateUpgradeCost(bestBfu.bfu, currentWl, bfuSizes)
299
+
300
+ if (upgradeCost <= availableBits) {
301
+ // Perform upgrade
302
+ availableBits -= upgradeCost
303
+ allocation[bestBfu.bfu]++
304
+
305
+ // Update priority based on distortion reduction
306
+ bestBfu.priority += DISTORTION_DELTA_DB[currentWl]
307
+ priorityQueue.updateTop()
308
+ } else {
309
+ // Not enough bits for this BFU
310
+ priorityQueue.pop()
311
+ }
312
+ }
313
+
314
+ return allocation
315
+ }
316
+
317
+ /**
318
+ * Calculate bit cost to upgrade a BFU's word length
319
+ */
320
+ function calculateUpgradeCost(bfuIndex, currentWl, bfuSizes) {
321
+ const currentBits = WORD_LENGTH_BITS[currentWl] * bfuSizes[bfuIndex]
322
+ const nextBits = WORD_LENGTH_BITS[currentWl + 1] * bfuSizes[bfuIndex]
323
+ return nextBits - currentBits
324
+ }
325
+
326
+ /**
327
+ * Create a priority queue for BFU allocation
328
+ * @param {Float32Array} smrValues - Initial priorities (SMR values)
329
+ * @param {number} bfuCount - Number of BFUs
330
+ * @returns {Object} Priority queue interface
331
+ */
332
+ function createPriorityQueue(smrValues, bfuCount) {
333
+ const heap = []
334
+
335
+ // Initialize with BFUs that have signal
336
+ for (let i = 0; i < bfuCount; i++) {
337
+ if (smrValues[i] > -Infinity) {
338
+ heap.push({ bfu: i, priority: smrValues[i] })
339
+ }
340
+ }
341
+
342
+ // Build max heap
343
+ for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
344
+ heapifyDown(heap, i)
345
+ }
346
+
347
+ return {
348
+ isEmpty: () => heap.length === 0,
349
+ peek: () => heap[0],
350
+ pop: () => popMax(heap),
351
+ updateTop: () => heapifyDown(heap, 0),
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Maintain max heap property by moving element down
357
+ * @param {Array} heap - Heap array
358
+ * @param {number} startIdx - Starting index
359
+ */
360
+ function heapifyDown(heap, startIdx) {
361
+ let idx = startIdx
362
+ const element = heap[idx]
363
+ const length = heap.length
364
+
365
+ while (true) {
366
+ const leftChild = 2 * idx + 1
367
+ const rightChild = 2 * idx + 2
368
+ let largest = idx
369
+ let largestPriority = element.priority
370
+
371
+ // Find largest among parent and children
372
+ if (leftChild < length && heap[leftChild].priority > largestPriority) {
373
+ largest = leftChild
374
+ largestPriority = heap[leftChild].priority
375
+ }
376
+
377
+ if (rightChild < length && heap[rightChild].priority > largestPriority) {
378
+ largest = rightChild
379
+ }
380
+
381
+ // If parent is largest, we're done
382
+ if (largest === idx) break
383
+
384
+ // Swap and continue
385
+ heap[idx] = heap[largest]
386
+ idx = largest
387
+ }
388
+
389
+ heap[idx] = element
390
+ }
391
+
392
+ /**
393
+ * Remove and return the maximum element from heap
394
+ * @param {Array} heap - Heap array
395
+ * @returns {Object} Maximum element
396
+ */
397
+ function popMax(heap) {
398
+ const max = heap[0]
399
+ const last = heap.pop()
400
+
401
+ if (heap.length > 0) {
402
+ heap[0] = last
403
+ heapifyDown(heap, 0)
404
+ }
405
+
406
+ return max
407
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Carta1 Audio Codec - Quantization
3
+ *
4
+ * This module implements quantization and dequantization of MDCT coefficients
5
+ * for the ATRAC1 audio codec, organizing coefficients into Block Floating Units (BFUs)
6
+ * and applying appropriate scale factors for optimal bit allocation.
7
+ */
8
+
9
+ import {
10
+ SPECS_PER_BFU,
11
+ BFU_START_LONG,
12
+ BFU_START_SHORT,
13
+ BFU_BAND_BOUNDARIES,
14
+ SCALE_FACTORS,
15
+ QUANTIZATION_SIGN_BIT_SHIFT,
16
+ } from '../core/constants.js'
17
+
18
+ /**
19
+ * Find the optimal scale factor index for a set of coefficients
20
+ * @param {Float32Array} coefficients - MDCT coefficients to analyze
21
+ * @returns {number} Scale factor index (0-63)
22
+ */
23
+ export function findScaleFactor(coefficients) {
24
+ let maxAmplitude = 0
25
+
26
+ for (let i = 0; i < coefficients.length; i++) {
27
+ maxAmplitude = Math.max(maxAmplitude, Math.abs(coefficients[i]))
28
+ }
29
+
30
+ if (maxAmplitude === 0) return 0
31
+
32
+ // Binary search for optimal scale factor
33
+ let low = 0
34
+ let high = SCALE_FACTORS.length - 1
35
+
36
+ while (low < high) {
37
+ const mid = (low + high) >> 1
38
+ if (maxAmplitude <= SCALE_FACTORS[mid]) {
39
+ high = mid
40
+ } else {
41
+ low = mid + 1
42
+ }
43
+ }
44
+
45
+ return low
46
+ }
47
+
48
+ /**
49
+ * Quantize floating-point coefficients to integer values
50
+ * @param {Float32Array} coefficients - Input coefficients to quantize
51
+ * @param {number} scaleFactorIndex - Index into SCALE_FACTORS table
52
+ * @param {number} bitsPerSample - Number of bits per quantized sample
53
+ * @returns {Int32Array} Quantized integer coefficients
54
+ */
55
+ export function quantize(coefficients, scaleFactorIndex, bitsPerSample) {
56
+ if (bitsPerSample === 0 || scaleFactorIndex === 0) {
57
+ return new Int32Array(coefficients.length)
58
+ }
59
+
60
+ const scaleFactor = SCALE_FACTORS[scaleFactorIndex]
61
+ const quantRange = (1 << (bitsPerSample - QUANTIZATION_SIGN_BIT_SHIFT)) - 1
62
+ const normFactor = quantRange / scaleFactor
63
+
64
+ const quantized = new Int32Array(coefficients.length)
65
+
66
+ for (let i = 0; i < coefficients.length; i++) {
67
+ const normalized = coefficients[i] * normFactor
68
+ quantized[i] = Math.max(
69
+ -quantRange - 1,
70
+ Math.min(quantRange, Math.round(normalized))
71
+ )
72
+ }
73
+
74
+ return quantized
75
+ }
76
+
77
+ /**
78
+ * Dequantize integer coefficients back to floating-point values
79
+ * @param {Int32Array} quantized - Quantized integer coefficients
80
+ * @param {number} scaleFactorIndex - Index into SCALE_FACTORS table
81
+ * @param {number} bitsPerSample - Number of bits per quantized sample
82
+ * @returns {Float32Array} Dequantized floating-point coefficients
83
+ */
84
+ export function dequantize(quantized, scaleFactorIndex, bitsPerSample) {
85
+ if (bitsPerSample === 0 || scaleFactorIndex === 0) {
86
+ return new Float32Array(quantized.length)
87
+ }
88
+
89
+ const scaleFactor = SCALE_FACTORS[scaleFactorIndex]
90
+ const quantRange = (1 << (bitsPerSample - QUANTIZATION_SIGN_BIT_SHIFT)) - 1
91
+ const denormFactor = scaleFactor / quantRange
92
+
93
+ const dequantized = new Float32Array(quantized.length)
94
+
95
+ for (let i = 0; i < quantized.length; i++) {
96
+ dequantized[i] = quantized[i] * denormFactor
97
+ }
98
+
99
+ return dequantized
100
+ }
101
+
102
+ /**
103
+ * Group MDCT coefficients into Block Floating Units (BFUs)
104
+ * @param {Float32Array} coefficients - Input MDCT coefficients
105
+ * @param {number[]} blockModes - Block mode for each band (0=long, 1=short)
106
+ * @param {Array<Float32Array>} bfuBuffers - Optional pre-allocated BFU buffers
107
+ * @returns {Object} Object containing bfuData, bfuSizes, and bfuCount
108
+ */
109
+ export function groupIntoBFUs(coefficients, blockModes, bfuBuffers = null) {
110
+ const bfuData = bfuBuffers ?? []
111
+ const bfuSizes = []
112
+
113
+ let coeffIndex = 0
114
+ let bfuIndex = 0
115
+
116
+ for (let band = 0; band < 3; band++) {
117
+ const bandStart = coeffIndex
118
+ const bandSize = band === 2 ? 256 : 128
119
+ const bandEnd = band < 2 ? BFU_BAND_BOUNDARIES[band] : SPECS_PER_BFU.length
120
+ const isLongBlock = blockModes[band] === 0
121
+ const startPositions = isLongBlock ? BFU_START_LONG : BFU_START_SHORT
122
+
123
+ while (bfuIndex < bandEnd) {
124
+ const size = SPECS_PER_BFU[bfuIndex]
125
+ const startPos = startPositions[bfuIndex] - bandStart
126
+
127
+ const bfu = bfuBuffers ? bfuBuffers[bfuIndex] : new Float32Array(size)
128
+ bfu.fill(0) // Clear the buffer
129
+
130
+ if (startPos >= 0 && startPos < bandSize) {
131
+ const endPos = Math.min(startPos + size, bandSize)
132
+ const actualSize = endPos - startPos
133
+
134
+ for (let i = 0; i < actualSize; i++) {
135
+ bfu[i] = coefficients[bandStart + startPos + i]
136
+ }
137
+ }
138
+
139
+ if (!bfuBuffers) {
140
+ bfuData.push(bfu)
141
+ }
142
+ bfuSizes.push(size)
143
+ bfuIndex++
144
+ }
145
+
146
+ coeffIndex += bandSize
147
+ }
148
+
149
+ return { bfuData, bfuSizes, bfuCount: bfuIndex }
150
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Carta1 Audio Codec - Bitstream Operations
3
+ *
4
+ * This module provides low-level bitstream packing and unpacking operations
5
+ * for reading and writing ATRAC1 frame data with bit-level precision.
6
+ */
7
+
8
+ /**
9
+ * Pack bits into a buffer at a specific bit position
10
+ * @param {Uint8Array} buffer - Destination buffer
11
+ * @param {number} bitPosition - Bit position to start writing at
12
+ * @param {number} value - Value to pack
13
+ * @param {number} bitCount - Number of bits to pack
14
+ */
15
+ export function packBits(buffer, bitPosition, value, bitCount) {
16
+ if (bitCount === 0) return
17
+
18
+ let byteIndex = Math.floor(bitPosition / 8)
19
+ let bitOffset = bitPosition % 8
20
+
21
+ value &= (1 << bitCount) - 1
22
+
23
+ let bitsWritten = 0
24
+ while (bitsWritten < bitCount && byteIndex < buffer.length) {
25
+ const bitsAvailable = 8 - bitOffset
26
+ const bitsToWrite = Math.min(bitCount - bitsWritten, bitsAvailable)
27
+
28
+ const shift = bitCount - bitsWritten - bitsToWrite
29
+ const valueBits = (value >> shift) & ((1 << bitsToWrite) - 1)
30
+
31
+ const mask = ((1 << bitsToWrite) - 1) << (bitsAvailable - bitsToWrite)
32
+ buffer[byteIndex] =
33
+ (buffer[byteIndex] & ~mask) | (valueBits << (bitsAvailable - bitsToWrite))
34
+
35
+ bitsWritten += bitsToWrite
36
+ byteIndex++
37
+ bitOffset = 0
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Unpack bits from a buffer at a specific bit position
43
+ * @param {Uint8Array} buffer - Source buffer
44
+ * @param {number} bitPosition - Bit position to start reading from
45
+ * @param {number} bitCount - Number of bits to unpack
46
+ * @returns {number} Unpacked unsigned value
47
+ */
48
+ export function unpackBits(buffer, bitPosition, bitCount) {
49
+ if (bitCount === 0) return 0
50
+
51
+ let byteIndex = Math.floor(bitPosition / 8)
52
+ let bitOffset = bitPosition % 8
53
+ let value = 0
54
+
55
+ for (let bitsRead = 0; bitsRead < bitCount && byteIndex < buffer.length; ) {
56
+ const bitsAvailable = 8 - bitOffset
57
+ const bitsToRead = Math.min(bitCount - bitsRead, bitsAvailable)
58
+
59
+ const mask = (1 << bitsToRead) - 1
60
+ const bits = (buffer[byteIndex] >> (bitsAvailable - bitsToRead)) & mask
61
+
62
+ value = (value << bitsToRead) | bits
63
+ bitsRead += bitsToRead
64
+ byteIndex++
65
+ bitOffset = 0
66
+ }
67
+
68
+ return value
69
+ }
70
+
71
+ /**
72
+ * Unpack signed bits from a buffer at a specific bit position
73
+ * @param {Uint8Array} buffer - Source buffer
74
+ * @param {number} bitPosition - Bit position to start reading from
75
+ * @param {number} bitCount - Number of bits to unpack
76
+ * @returns {number} Unpacked signed value (two's complement)
77
+ */
78
+ export function unpackSignedBits(buffer, bitPosition, bitCount) {
79
+ const value = unpackBits(buffer, bitPosition, bitCount)
80
+ const signBit = 1 << (bitCount - 1)
81
+ return value >= signBit ? value - (1 << bitCount) : value
82
+ }