carta1 1.0.22 → 1.1.1

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.
@@ -71,7 +71,6 @@ class Carta1Worker {
71
71
  * @param {number} [options.transientThresholdLow] - Low band transient threshold
72
72
  * @param {number} [options.transientThresholdMid] - Mid band transient threshold
73
73
  * @param {number} [options.transientThresholdHigh] - High band transient threshold
74
- * @param {number} [options.normalizationDb] - Normalization level in dB
75
74
  * @returns {Promise<Object>} Encoding result
76
75
  * @returns {Blob} returns.aeaBlob - ATRAC1 encoded audio blob
77
76
  * @returns {Object} returns.shortBlockData - Short block frame indices
@@ -1,317 +1,226 @@
1
1
  /**
2
2
  * Carta1 Audio Codec - Bit Allocation Module
3
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.
4
+ * This module implements the bit allocation system for the Carta1 audio codec,
5
+ * responsible for distributing available bits across frequency bands to minimize
6
+ * perceptual distortion while maintaining the target bitrate.
7
+ *
8
+ * The module uses Rate-Distortion Optimization (RDO) with a greedy algorithm
9
+ * that allocates bits to Band Frequency Units (BFUs) based on the perceptual
10
+ * benefit per bit spent. It employs psychoacoustic modeling to determine
11
+ * scale factors and uses a priority queue to efficiently find the optimal
12
+ * bit distribution.
13
+ *
14
+ * Key components:
15
+ * - RDO-based bit allocation using exponential distortion modeling
16
+ * - Scale factor computation for each frequency band
17
+ * - Max-heap priority queue for greedy optimization
18
+ * - Support for variable word lengths per BFU
19
+ *
20
+ * The algorithm ensures that bits are allocated where they provide the
21
+ * greatest reduction in audible distortion, resulting in high-quality
22
+ * audio compression at the target bitrate.
7
23
  */
24
+
8
25
  import {
26
+ NUM_BFUS,
9
27
  FRAME_BITS,
10
28
  FRAME_OVERHEAD_BITS,
11
29
  BITS_PER_BFU_METADATA,
12
- BFU_AMOUNTS,
13
30
  MAX_WORD_LENGTH_INDEX,
14
31
  WORD_LENGTH_BITS,
15
- DISTORTION_DELTA_DB,
16
- PSYMODEL_MIN_POWER_DB,
17
- INTERPOLATION_COMPENSATION_FACTOR,
18
- BFU_MASK_INTERPOLATION_TABLE,
19
- BFU_FREQUENCIES,
20
- NUM_BFUS,
21
- SAMPLE_RATE,
22
- PSYMODEL_FFT_SIZE,
32
+ INV_POWER_OF_TWO,
33
+ SCALE_FACTORS,
23
34
  } from '../core/constants.js'
24
35
 
25
36
  /**
26
- * Allocate bits to BFUs based on psychoacoustic analysis
27
- * @param {Object} psychoResults - Psychoacoustic analysis results
28
- * @param {Array} bfuData - BFU coefficient data
29
- * @param {Array} bfuSizes - Size of each BFU
30
- * @param {number} maxBfuCount - Maximum number of BFUs to use
31
- * @returns {Object} Allocation results with BFU count and bit distribution
37
+ * Allocate bits to BFUs using optimized greedy Lp-RDO
38
+ * @param {Array<Float32Array>} bfuData
39
+ * @param {Int32Array} bfuSizes
40
+ * @param {number} maxBfuCount
41
+ * @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
32
42
  */
33
- export function allocateBits(psychoResults, bfuData, bfuSizes, maxBfuCount) {
34
- const energyDb = calculateBfuEnergy(bfuData, bfuSizes, maxBfuCount)
35
- const maskDb = interpolateMask(psychoResults.criticalBandThresholds)
36
- const smr = calculateSMR(energyDb, maskDb, maxBfuCount)
37
- const usedBFU = selectOptimalBfuCount(smr, energyDb, maxBfuCount)
43
+ export function allocateBits(bfuData, bfuSizes, maxBfuCount) {
44
+ const usedBFU = maxBfuCount
38
45
  const avail =
39
46
  FRAME_BITS - FRAME_OVERHEAD_BITS - usedBFU * BITS_PER_BFU_METADATA
40
- const allocation = distributeBitsGreedy(usedBFU, smr, bfuSizes, avail)
41
- return { bfuCount: usedBFU, allocation }
42
- }
43
47
 
44
- /**
45
- * Calculate energy in dB for each BFU
46
- * @param {Array} bfuData - BFU coefficient bfuData
47
- * @param {Array} bfuSizes - Size of each BFU
48
- * @param {number} bfuCount - Number of BFUs to process
49
- * @returns {Float32Array} Energy values in dB
50
- */
51
- function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
52
- const out = new Float32Array(bfuCount)
53
- const minDb = PSYMODEL_MIN_POWER_DB
54
- for (let i = 0; i < bfuCount; i++) {
55
- const sz = bfuSizes[i]
56
- if (!sz) {
57
- out[i] = minDb
58
- continue
59
- }
60
- let sum = 0,
61
- buf = bfuData[i]
62
- for (let j = 0; j < sz; j++) sum += buf[j] * buf[j]
63
- out[i] = sum > 0 ? 10 * Math.log10(sum / sz) : minDb
48
+ const rdoResult = distributeBitsRDO(usedBFU, bfuData, bfuSizes, avail)
49
+
50
+ return {
51
+ bfuCount: usedBFU,
52
+ allocation: rdoResult.wordLengths,
53
+ scaleFactorIndices: rdoResult.scaleFactorIndices,
64
54
  }
65
- return out
66
55
  }
67
56
 
68
57
  /**
69
- * Interpolate psychoacoustic masking thresholds from critical bands to BFU frequencies
70
- * @param {Float32Array} criticalBandThresholds - Masking thresholds for each critical band
71
- * @returns {Float32Array} Interpolated masking thresholds for each BFU
58
+ * Distributes available bits across Band Frequency Units (BFUs) using Rate-Distortion Optimization.
59
+ *
60
+ * This function implements an optimized greedy RDO allocator that minimizes perceptual distortion
61
+ * while staying within the bit budget. It uses a priority queue (max-heap) to greedily allocate
62
+ * bits to the BFUs that provide the greatest distortion reduction per bit spent.
63
+ *
64
+ * @param {number} activeBfuCount
65
+ * @param {Array<Float32Array>} bfuData
66
+ * @param {Int32Array} bfuSizes
67
+ * @param {number} remainingBits
68
+ * @returns {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
72
69
  */
73
- function interpolateMask(criticalBandThresholds) {
74
- const maskValues = new Float32Array(NUM_BFUS)
70
+ function distributeBitsRDO(activeBfuCount, bfuData, bfuSizes, remainingBits) {
71
+ const scaleFactorTable = new Int32Array(NUM_BFUS)
72
+ const wordLengths = new Int32Array(activeBfuCount)
75
73
 
76
- for (let bfuIndex = 0; bfuIndex < NUM_BFUS; bfuIndex++) {
77
- const interpolationEntry = BFU_MASK_INTERPOLATION_TABLE[bfuIndex]
78
- const currentBandThreshold = criticalBandThresholds[interpolationEntry.band]
79
- const nextBandThreshold = criticalBandThresholds[interpolationEntry.next]
74
+ const heapIndices = []
75
+ const heapPriorities = []
80
76
 
81
- // Calculate interpolation parameter t based on BFU frequency position
82
- const normalizedFreqPosition =
83
- (Math.round(
84
- BFU_FREQUENCIES[bfuIndex] / (SAMPLE_RATE / PSYMODEL_FFT_SIZE)
85
- ) -
86
- interpolationEntry.x0) *
87
- interpolationEntry.tInv
77
+ const deltaDistPerCoeff = (bfuIndex, currentWl, nextWl) => {
78
+ const sfi = scaleFactorTable[bfuIndex]
79
+ const scaleFactor = SCALE_FACTORS[sfi]
80
+ if (scaleFactor === 0) return 0
88
81
 
89
- const interpolatedValue =
90
- currentBandThreshold +
91
- (nextBandThreshold - currentBandThreshold) * normalizedFreqPosition
82
+ const bits1 = WORD_LENGTH_BITS[currentWl] | 0
83
+ const bits2 = WORD_LENGTH_BITS[nextWl] | 0
92
84
 
93
- // Add compensation factor based on threshold difference
94
- maskValues[bfuIndex] =
95
- interpolatedValue +
96
- Math.abs(nextBandThreshold - currentBandThreshold) *
97
- INTERPOLATION_COMPENSATION_FACTOR
98
- }
85
+ const f1 = bits1 === 0 ? 2.0 : INV_POWER_OF_TWO[bits1]
86
+ const f2 = INV_POWER_OF_TWO[bits2]
99
87
 
100
- return maskValues
101
- }
88
+ return scaleFactor * (f1 - f2)
89
+ }
102
90
 
103
- /**
104
- * Calculate Signal-to-Mask Ratios (SMR) for each BFU
105
- * SMR determines how much energy exceeds the masking threshold
106
- * @param {Float32Array} energyDb - Energy values in dB for each BFU
107
- * @param {Float32Array} maskDb - Masking threshold values in dB for each BFU
108
- * @param {number} bfuCount - Number of BFUs to process
109
- * @returns {Float32Array} SMR values for each BFU (-Infinity for inaudible BFUs)
110
- */
111
- function calculateSMR(energyDb, maskDb, bfuCount) {
112
- const smrValues = new Float32Array(bfuCount)
91
+ for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
92
+ const sz = bfuSizes[bfuIndex] | 0
93
+ if (sz === 0) continue
113
94
 
114
- for (let bfuIndex = 0; bfuIndex < bfuCount; bfuIndex++) {
115
- const energyValue = energyDb[bfuIndex]
116
- smrValues[bfuIndex] =
117
- energyValue <= PSYMODEL_MIN_POWER_DB
118
- ? -Infinity
119
- : energyValue - maskDb[bfuIndex]
120
- }
95
+ const sfi = findScaleFactor(bfuData[bfuIndex].subarray(0, sz))
96
+ scaleFactorTable[bfuIndex] = sfi
121
97
 
122
- return smrValues
123
- }
98
+ if (sfi === 0) continue
124
99
 
125
- /**
126
- * Determine optimal number of BFUs to use based on SMR analysis
127
- * Uses cumulative SMR analysis to find the point of diminishing returns
128
- * @param {Float32Array} smrValues - Signal-to-Mask ratios for each BFU
129
- * @param {Float32Array} energyValues - Energy values for each BFU
130
- * @param {number} maxBfuCount - Maximum BFUs available
131
- * @returns {number} Optimal number of BFUs to use for encoding
132
- */
133
- function selectOptimalBfuCount(smrValues, energyValues, maxBfuCount) {
134
- // Collect BFUs with positive SMR (above masking threshold)
135
- const positiveSMRBfus = []
136
- for (let bfuIndex = 0; bfuIndex < maxBfuCount; bfuIndex++) {
137
- if (smrValues[bfuIndex] > 0) {
138
- positiveSMRBfus.push({
139
- index: bfuIndex,
140
- smrValue: smrValues[bfuIndex],
141
- })
142
- }
143
- }
100
+ const currentWl = 0
101
+ const nextWl = 1
102
+ const deltaBits =
103
+ (WORD_LENGTH_BITS[nextWl] - WORD_LENGTH_BITS[currentWl]) | 0
104
+ if (deltaBits <= 0) continue
144
105
 
145
- // If no BFUs have positive SMR, fall back to energy-based selection
146
- if (!positiveSMRBfus.length) {
147
- for (let bfuIndex = maxBfuCount - 1; bfuIndex >= 0; bfuIndex--) {
148
- if (energyValues[bfuIndex] > PSYMODEL_MIN_POWER_DB) {
149
- return (
150
- BFU_AMOUNTS.find((count) => count > bfuIndex) ||
151
- BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
152
- )
153
- }
154
- }
155
- return BFU_AMOUNTS[0]
106
+ const ddPerCoeff = deltaDistPerCoeff(bfuIndex, currentWl, nextWl)
107
+ heapIndices.push(bfuIndex)
108
+ heapPriorities.push(ddPerCoeff / deltaBits)
156
109
  }
157
110
 
158
- // Sort BFUs by SMR value (highest first)
159
- positiveSMRBfus.sort((a, b) => b.smrValue - a.smrValue)
111
+ // Heapify
112
+ for (let i = (heapIndices.length >> 1) - 1; i >= 0; i--)
113
+ siftDown(heapIndices, heapPriorities, i)
160
114
 
161
- // Calculate cumulative SMR values
162
- const cumulativeSmr = new Float32Array(positiveSMRBfus.length)
163
- cumulativeSmr[0] = positiveSMRBfus[0].smrValue
164
- for (let k = 1; k < positiveSMRBfus.length; k++) {
165
- cumulativeSmr[k] = cumulativeSmr[k - 1] + positiveSMRBfus[k].smrValue
166
- }
115
+ // Greedy spending
116
+ while (remainingBits > 0 && heapIndices.length > 0) {
117
+ const bfu = heapIndices[0]
118
+ const cur = wordLengths[bfu] | 0
119
+ const nxt = cur + 1
167
120
 
168
- // Find optimal cutoff point using diminishing returns analysis
169
- for (const candidateCount of BFU_AMOUNTS) {
170
- if (candidateCount > maxBfuCount) continue
171
- if (positiveSMRBfus.length <= candidateCount) return candidateCount
121
+ const sz = bfuSizes[bfu] | 0
122
+ const deltaBits = (WORD_LENGTH_BITS[nxt] - WORD_LENGTH_BITS[cur]) | 0
123
+ const cost = deltaBits * sz
172
124
 
173
- const lastIncludedIndex = candidateCount - 1
174
- const averageIncludedSmr =
175
- cumulativeSmr[lastIncludedIndex] / (lastIncludedIndex + 1)
176
- const averageExcludedSmr =
177
- (cumulativeSmr[positiveSMRBfus.length - 1] -
178
- cumulativeSmr[lastIncludedIndex]) /
179
- (positiveSMRBfus.length - lastIncludedIndex - 1)
125
+ if (cost > remainingBits || cost <= 0) {
126
+ popRoot(heapIndices, heapPriorities)
127
+ continue
128
+ }
180
129
 
181
- // If excluded BFUs contribute less than 10% of included average, use this count
182
- if (averageExcludedSmr < averageIncludedSmr * 0.1) return candidateCount
130
+ remainingBits -= cost
131
+ wordLengths[bfu] = nxt
132
+
133
+ if (nxt < MAX_WORD_LENGTH_INDEX) {
134
+ const nxt2 = nxt + 1
135
+ const deltaBits2 = (WORD_LENGTH_BITS[nxt2] - WORD_LENGTH_BITS[nxt]) | 0
136
+ if (deltaBits2 > 0) {
137
+ heapPriorities[0] = deltaDistPerCoeff(bfu, nxt, nxt2) / deltaBits2
138
+ siftDown(heapIndices, heapPriorities, 0)
139
+ } else {
140
+ popRoot(heapIndices, heapPriorities)
141
+ }
142
+ } else {
143
+ popRoot(heapIndices, heapPriorities)
144
+ }
183
145
  }
184
146
 
185
- return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
147
+ return { wordLengths, scaleFactorIndices: scaleFactorTable }
186
148
  }
187
149
 
188
150
  /**
189
- * Distribute available bits among BFUs using a greedy heap-based algorithm
190
- * Iteratively upgrades quantization precision for the BFU with highest SMR
191
- * @param {number} activeBfuCount - Number of BFUs to distribute bits to
192
- * @param {Float32Array} smrValues - Signal-to-Mask ratios for prioritization
193
- * @param {Array} bfuSizes - Size (coefficient count) of each BFU
194
- * @param {number} remainingBits - Total bits available for distribution
195
- * @returns {Int32Array} Word length index allocation per BFU
151
+ * Find the optimal scale factor index for a set of coefficients.
152
+ * Same semantics as the original.
153
+ * @param {Float32Array} coefficients
154
+ * @returns {number}
196
155
  */
197
- function distributeBitsGreedy(
198
- activeBfuCount,
199
- smrValues,
200
- bfuSizes,
201
- remainingBits
202
- ) {
203
- const wordLengthAllocation = new Int32Array(activeBfuCount)
204
- const heapBfuIndices = []
205
- const heapPriorities = []
206
-
207
- // Initialize heap with BFUs that have finite SMR values
208
- for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
209
- if (smrValues[bfuIndex] > -Infinity) {
210
- heapBfuIndices.push(bfuIndex)
211
- heapPriorities.push(smrValues[bfuIndex])
212
- }
213
- }
214
-
215
- // Build max-heap structure
216
- for (
217
- let nodeIndex = (heapBfuIndices.length >> 1) - 1;
218
- nodeIndex >= 0;
219
- nodeIndex--
220
- ) {
221
- siftDown(heapBfuIndices, heapPriorities, nodeIndex)
156
+ export function findScaleFactor(coefficients) {
157
+ let maxAmplitude = 0.0
158
+ for (let i = 0; i < coefficients.length; i++) {
159
+ const a = Math.abs(coefficients[i])
160
+ if (a > maxAmplitude) maxAmplitude = a
222
161
  }
223
-
224
- // Greedily allocate bits to highest priority BFUs
225
- while (heapBfuIndices.length && remainingBits > 0) {
226
- const currentBfu = heapBfuIndices[0]
227
- const currentWordLength = wordLengthAllocation[currentBfu]
228
-
229
- // Skip BFUs that have reached maximum quantization precision
230
- if (currentWordLength >= MAX_WORD_LENGTH_INDEX) {
231
- popRoot(heapBfuIndices, heapPriorities)
232
- continue
233
- }
234
-
235
- // Calculate bit cost for upgrading this BFU's quantization
236
- const bfuSize = bfuSizes[currentBfu]
237
- const upgradeCost =
238
- WORD_LENGTH_BITS[currentWordLength + 1] * bfuSize -
239
- WORD_LENGTH_BITS[currentWordLength] * bfuSize
240
-
241
- if (upgradeCost > remainingBits) {
242
- popRoot(heapBfuIndices, heapPriorities)
243
- continue
244
- }
245
-
246
- // Perform the upgrade
247
- remainingBits -= upgradeCost
248
- wordLengthAllocation[currentBfu] = currentWordLength + 1
249
- heapPriorities[0] += DISTORTION_DELTA_DB[currentWordLength]
250
- siftDown(heapBfuIndices, heapPriorities, 0)
251
- }
252
-
253
- return wordLengthAllocation
162
+ if (maxAmplitude === 0) return 0
163
+ const index = Math.ceil(3 * (Math.log2(maxAmplitude) + 21))
164
+ return Math.max(0, Math.min(63, index))
254
165
  }
255
166
 
256
167
  /**
257
- * Remove the root element from a max-heap and restore heap property
258
- * @param {Array} heapIndices - Array of BFU indices forming the heap
259
- * @param {Array} heapPriorities - Array of priority values corresponding to indices
168
+ * Removes the root element from a max-heap and maintains heap property.
169
+ *
170
+ * This function efficiently removes the maximum priority element from the heap
171
+ * by replacing it with the last element and then restoring the heap property
172
+ * through sifting down.
173
+ *
174
+ * @param {Array<number>} heapIndices
175
+ * @param {Array<number>} heapPriorities
260
176
  */
261
177
  function popRoot(heapIndices, heapPriorities) {
262
- const lastIndex = heapIndices.length - 1
263
- heapIndices[0] = heapIndices[lastIndex]
264
- heapPriorities[0] = heapPriorities[lastIndex]
178
+ const last = heapIndices.length - 1
179
+ heapIndices[0] = heapIndices[last]
180
+ heapPriorities[0] = heapPriorities[last]
265
181
  heapIndices.pop()
266
182
  heapPriorities.pop()
267
183
  if (heapIndices.length) siftDown(heapIndices, heapPriorities, 0)
268
184
  }
269
185
 
270
186
  /**
271
- * Restore max-heap property by sifting an element down to its correct position
272
- * @param {Array} heapIndices - Array of BFU indices forming the heap
273
- * @param {Array} heapPriorities - Array of priority values corresponding to indices
274
- * @param {number} startIndex - Index of element to sift down
187
+ * Restores max-heap property by sifting an element down from a given position.
188
+ *
189
+ * This function maintains the heap invariant where parent nodes have higher
190
+ * priority values than their children. It compares the element at startIndex
191
+ * with its children and swaps with the larger child until the heap property
192
+ * is satisfied, or it reaches a leaf position.
193
+ *
194
+ * @param {Array<number>} heapIndices
195
+ * @param {Array<number>} heapPriorities
196
+ * @param {number} startIndex
275
197
  */
276
198
  function siftDown(heapIndices, heapPriorities, startIndex) {
277
199
  const heapSize = heapIndices.length
278
- let currentIndex = startIndex
279
- const originalIndexValue = heapIndices[currentIndex]
280
- const originalPriorityValue = heapPriorities[currentIndex]
200
+ let i = startIndex
201
+ const idxVal = heapIndices[i]
202
+ const prVal = heapPriorities[i]
281
203
 
282
204
  while (true) {
283
- const leftChildIndex = (currentIndex << 1) + 1
284
- const rightChildIndex = leftChildIndex + 1
285
- let maxIndex = currentIndex
286
- let maxPriority = originalPriorityValue
287
-
288
- // Check if left child has higher priority
289
- if (
290
- leftChildIndex < heapSize &&
291
- heapPriorities[leftChildIndex] > maxPriority
292
- ) {
293
- maxIndex = leftChildIndex
294
- maxPriority = heapPriorities[leftChildIndex]
205
+ const l = (i << 1) + 1
206
+ const r = l + 1
207
+ let maxI = i
208
+ let maxP = prVal
209
+
210
+ if (l < heapSize && heapPriorities[l] > maxP) {
211
+ maxI = l
212
+ maxP = heapPriorities[l]
295
213
  }
296
-
297
- // Check if right child has higher priority
298
- if (
299
- rightChildIndex < heapSize &&
300
- heapPriorities[rightChildIndex] > maxPriority
301
- ) {
302
- maxIndex = rightChildIndex
214
+ if (r < heapSize && heapPriorities[r] > maxP) {
215
+ maxI = r
303
216
  }
217
+ if (maxI === i) break
304
218
 
305
- // If current position is correct, we're done
306
- if (maxIndex === currentIndex) break
307
-
308
- // Move the higher priority child up
309
- heapIndices[currentIndex] = heapIndices[maxIndex]
310
- heapPriorities[currentIndex] = heapPriorities[maxIndex]
311
- currentIndex = maxIndex
219
+ heapIndices[i] = heapIndices[maxI]
220
+ heapPriorities[i] = heapPriorities[maxI]
221
+ i = maxI
312
222
  }
313
223
 
314
- // Place original element in its final position
315
- heapIndices[currentIndex] = originalIndexValue
316
- heapPriorities[currentIndex] = originalPriorityValue
224
+ heapIndices[i] = idxVal
225
+ heapPriorities[i] = prVal
317
226
  }