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.
- package/LICENSE +15 -0
- package/README.md +129 -0
- package/bin/cli.js +679 -0
- package/codec/analysis/psychoacoustics.js +589 -0
- package/codec/analysis/transient.js +226 -0
- package/codec/browser/interface.js +188 -0
- package/codec/browser/worker.js +140 -0
- package/codec/coding/bitallocation.js +407 -0
- package/codec/coding/quantization.js +150 -0
- package/codec/core/bitstream.js +82 -0
- package/codec/core/buffers.js +99 -0
- package/codec/core/constants.js +321 -0
- package/codec/core/options.js +151 -0
- package/codec/index.js +27 -0
- package/codec/io/processor.js +566 -0
- package/codec/io/readers.js +56 -0
- package/codec/io/serialization.js +254 -0
- package/codec/pipeline/decoder.js +278 -0
- package/codec/pipeline/encoder.js +369 -0
- package/codec/transforms/fft.js +69 -0
- package/codec/transforms/mdct.js +225 -0
- package/codec/transforms/qmf.js +127 -0
- package/codec/utils.js +151 -0
- package/dist/carta1-worker-interface.min.js +2 -0
- package/dist/carta1-worker-interface.min.js.map +1 -0
- package/dist/carta1-worker.min.js +2 -0
- package/dist/carta1-worker.min.js.map +1 -0
- package/dist/carta1.min.js +2 -0
- package/dist/carta1.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -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
|
+
}
|