carta1 1.1.7 → 1.1.9

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.
@@ -5,21 +5,20 @@
5
5
  * responsible for distributing available bits across frequency bands to minimize
6
6
  * perceptual distortion while maintaining the target bitrate.
7
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.
8
+ * The module uses Rate-Distortion Optimization (RDO) by searching over a
9
+ * predefined set of valid Band Frequency Unit (BFU) counts. For each count,
10
+ * it performs a greedy bit distribution to find the best allocation. This
11
+ * approach optimally trades off the cost of encoding more frequency bands
12
+ * (metadata overhead) against the benefit of reducing truncation distortion.
13
13
  *
14
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
15
+ * - RDO search over valid BFU counts to select the optimal number of bands.
16
+ * - Greedy bit allocation for a fixed BFU count using a max-heap.
17
+ * - Exponential distortion modeling based on scale factors.
18
+ * - Support for variable word lengths per BFU.
19
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.
20
+ * The algorithm ensures that the codec dynamically adapts the encoded bandwidth
21
+ * to best match the signal characteristics for the given bitrate.
23
22
  */
24
23
 
25
24
  import {
@@ -31,10 +30,41 @@ import {
31
30
  WORD_LENGTH_BITS,
32
31
  INV_POWER_OF_TWO,
33
32
  SCALE_FACTORS,
33
+ BFU_AMOUNTS,
34
+ WORD_LENGTH_DELTA_BITS,
35
+ DISTORTION_DELTA_FACTORS,
34
36
  } from '../core/constants.js'
35
37
 
38
+ const biasedScaleFactorCache = new Map()
39
+
40
+ /**
41
+ * Builds and returns a cached table of biased scale factors.
42
+ *
43
+ * @param {number} bias
44
+ * @returns {Float64Array}
45
+ */
46
+ function buildBiasedScaleFactorTable(bias) {
47
+ if (biasedScaleFactorCache.has(bias)) {
48
+ return biasedScaleFactorCache.get(bias)
49
+ }
50
+
51
+ const out = new Float64Array(64)
52
+ if (bias === 1) {
53
+ out.set(SCALE_FACTORS)
54
+ } else {
55
+ for (let i = 0; i < 64; i++) {
56
+ out[i] = Math.pow(SCALE_FACTORS[i], bias)
57
+ }
58
+ }
59
+ biasedScaleFactorCache.set(bias, out)
60
+ return out
61
+ }
62
+
36
63
  /**
37
- * Allocate bits to BFUs using optimized greedy Lp-RDO
64
+ * Allocate bits by finding the optimal BFU count and distribution using RDO.
65
+ * This function searches through the valid BFU counts to find the one that
66
+ * minimizes the total perceptual distortion.
67
+ *
38
68
  * @param {Array<Float32Array>} bfuData
39
69
  * @param {Int32Array} bfuSizes
40
70
  * @param {number} maxBfuCount
@@ -42,136 +72,224 @@ import {
42
72
  * @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
43
73
  */
44
74
  export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
45
- const usedBFU = maxBfuCount
46
- const avail =
47
- FRAME_BITS - FRAME_OVERHEAD_BITS - usedBFU * BITS_PER_BFU_METADATA
48
-
49
- const rdoResult = distributeBitsRDO(
50
- usedBFU,
51
- bfuData,
52
- bfuSizes,
53
- avail,
54
- allocationBias
55
- )
56
-
57
- return {
58
- bfuCount: usedBFU,
59
- allocation: rdoResult.wordLengths,
60
- scaleFactorIndices: rdoResult.scaleFactorIndices,
75
+ const allScaleFactorIndices = new Int32Array(maxBfuCount)
76
+ const zeroBitDistortions = new Float32Array(maxBfuCount)
77
+ const biasedScaleFactors = buildBiasedScaleFactorTable(allocationBias)
78
+
79
+ for (let i = 0; i < maxBfuCount; i++) {
80
+ const sz = bfuSizes[i] | 0
81
+ if (sz === 0) continue
82
+
83
+ const sfi = findScaleFactor(bfuData[i], sz)
84
+ allScaleFactorIndices[i] = sfi
85
+ if (sfi > 0) {
86
+ const effectiveScaleFactor = biasedScaleFactors[sfi]
87
+ zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
88
+ }
89
+ }
90
+
91
+ let bestResult = null
92
+ let minTotalDistortion = Infinity
93
+
94
+ for (const candidateBfuCount of BFU_AMOUNTS) {
95
+ if (candidateBfuCount > maxBfuCount) continue
96
+
97
+ const availableBits =
98
+ FRAME_BITS -
99
+ FRAME_OVERHEAD_BITS -
100
+ candidateBfuCount * BITS_PER_BFU_METADATA
101
+
102
+ if (availableBits < 0) continue
103
+
104
+ const rdoResult = distributeBitsRDO(
105
+ candidateBfuCount,
106
+ bfuSizes,
107
+ availableBits,
108
+ biasedScaleFactors,
109
+ allScaleFactorIndices
110
+ )
111
+
112
+ const totalDistortion = calculateTotalDistortion(
113
+ candidateBfuCount,
114
+ maxBfuCount,
115
+ bfuSizes,
116
+ rdoResult.wordLengths,
117
+ rdoResult.scaleFactorIndices,
118
+ biasedScaleFactors,
119
+ zeroBitDistortions
120
+ )
121
+
122
+ if (totalDistortion < minTotalDistortion) {
123
+ minTotalDistortion = totalDistortion
124
+ bestResult = {
125
+ bfuCount: candidateBfuCount,
126
+ allocation: rdoResult.wordLengths,
127
+ scaleFactorIndices: rdoResult.scaleFactorIndices,
128
+ }
129
+ }
61
130
  }
131
+
132
+ if (!bestResult) {
133
+ const fallbackBfuCount = BFU_AMOUNTS[0]
134
+ return {
135
+ bfuCount: fallbackBfuCount,
136
+ allocation: new Int32Array(fallbackBfuCount),
137
+ scaleFactorIndices: new Int32Array(NUM_BFUS),
138
+ }
139
+ }
140
+
141
+ return bestResult
142
+ }
143
+
144
+ /**
145
+ * Calculates the total distortion for a given allocation.
146
+ * This includes quantization distortion for coded bands and truncation
147
+ * distortion for uncoded bands.
148
+ * @param {number} activeBfuCount
149
+ * @param {number} maxBfuCount
150
+ * @param {Int32Array} bfuSizes
151
+ * @param {Int32Array} wordLengths
152
+ * @param {Int32Array} scaleFactorIndices
153
+ * @param {number} biasedScaleFactors
154
+ * @param {Float32Array} zeroBitDistortions
155
+ * @returns {number}
156
+ */
157
+ function calculateTotalDistortion(
158
+ activeBfuCount,
159
+ maxBfuCount,
160
+ bfuSizes,
161
+ wordLengths,
162
+ scaleFactorIndices,
163
+ biasedScaleFactors,
164
+ zeroBitDistortions
165
+ ) {
166
+ let totalDistortion = 0.0
167
+
168
+ for (let i = 0; i < activeBfuCount; i++) {
169
+ const wl = wordLengths[i] | 0
170
+ const bits = WORD_LENGTH_BITS[wl] | 0
171
+ if (bits === 0) {
172
+ totalDistortion += zeroBitDistortions[i]
173
+ continue
174
+ }
175
+
176
+ const sfi = scaleFactorIndices[i]
177
+ if (sfi === 0) continue
178
+
179
+ const effectiveScaleFactor = biasedScaleFactors[sfi]
180
+ const distortionFactor = INV_POWER_OF_TWO[bits]
181
+
182
+ totalDistortion += effectiveScaleFactor * distortionFactor * bfuSizes[i]
183
+ }
184
+
185
+ for (let i = activeBfuCount; i < maxBfuCount; i++) {
186
+ totalDistortion += zeroBitDistortions[i]
187
+ }
188
+
189
+ return totalDistortion
62
190
  }
63
191
 
64
192
  /**
65
193
  * Distributes available bits across Band Frequency Units (BFUs) using Rate-Distortion Optimization.
66
- *
67
- * This function implements an optimized greedy RDO allocator that minimizes perceptual distortion
68
- * while staying within the bit budget. It uses a priority queue (max-heap) to greedily allocate
69
- * bits to the BFUs that provide the greatest distortion reduction per bit spent.
194
+ * (Modified to accept pre-calculated scale factors)
70
195
  *
71
196
  * @param {number} activeBfuCount
72
- * @param {Array<Float32Array>} bfuData
73
197
  * @param {Int32Array} bfuSizes
74
198
  * @param {number} remainingBits
75
- * @param {number} allocationBias
199
+ * @param {Float64Array} biasedScaleFactors
200
+ * @param {Int32Array} allScaleFactorIndices
76
201
  * @returns {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
77
202
  */
78
203
  function distributeBitsRDO(
79
204
  activeBfuCount,
80
- bfuData,
81
205
  bfuSizes,
82
206
  remainingBits,
83
- allocationBias
207
+ biasedScaleFactors,
208
+ allScaleFactorIndices
84
209
  ) {
85
- const scaleFactorTable = new Int32Array(NUM_BFUS)
86
210
  const wordLengths = new Int32Array(activeBfuCount)
87
211
 
88
- const heapIndices = []
89
- const heapPriorities = []
90
-
91
- const deltaDistPerCoeff = (bfuIndex, currentWl, nextWl) => {
92
- const sfi = scaleFactorTable[bfuIndex]
93
- const scaleFactor = SCALE_FACTORS[sfi]
94
- if (scaleFactor === 0) return 0
95
-
96
- const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
97
-
98
- const bits1 = WORD_LENGTH_BITS[currentWl] | 0
99
- const bits2 = WORD_LENGTH_BITS[nextWl] | 0
100
-
101
- const f1 = bits1 === 0 ? 2.0 : INV_POWER_OF_TWO[bits1]
102
- const f2 = INV_POWER_OF_TWO[bits2]
103
-
104
- return effectiveScaleFactor * (f1 - f2)
105
- }
212
+ const heapIndices = new Int32Array(activeBfuCount)
213
+ const heapPriorities = new Float32Array(activeBfuCount)
214
+ let heapSize = 0
106
215
 
107
216
  for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
108
217
  const sz = bfuSizes[bfuIndex] | 0
109
218
  if (sz === 0) continue
110
219
 
111
- const sfi = findScaleFactor(bfuData[bfuIndex].subarray(0, sz))
112
- scaleFactorTable[bfuIndex] = sfi
113
-
220
+ const sfi = allScaleFactorIndices[bfuIndex]
114
221
  if (sfi === 0) continue
115
222
 
116
- const currentWl = 0
117
- const nextWl = 1
118
- const deltaBits =
119
- (WORD_LENGTH_BITS[nextWl] - WORD_LENGTH_BITS[currentWl]) | 0
223
+ const deltaBits = WORD_LENGTH_DELTA_BITS[0]
120
224
  if (deltaBits <= 0) continue
121
225
 
122
- const ddPerCoeff = deltaDistPerCoeff(bfuIndex, currentWl, nextWl)
123
- heapIndices.push(bfuIndex)
124
- heapPriorities.push(ddPerCoeff / deltaBits)
226
+ const distortionDelta =
227
+ biasedScaleFactors[sfi] * DISTORTION_DELTA_FACTORS[0]
228
+
229
+ heapIndices[heapSize] = bfuIndex
230
+ heapPriorities[heapSize] = distortionDelta / deltaBits
231
+ heapSize++
232
+ }
233
+
234
+ if (heapSize === 0) {
235
+ return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
125
236
  }
126
237
 
127
238
  // Heapify
128
- for (let i = (heapIndices.length >> 1) - 1; i >= 0; i--)
129
- siftDown(heapIndices, heapPriorities, i)
239
+ for (let i = (heapSize >> 1) - 1; i >= 0; i--) {
240
+ siftDown(heapIndices, heapPriorities, i, heapSize)
241
+ }
130
242
 
131
- // Greedy spending
132
- while (remainingBits > 0 && heapIndices.length > 0) {
243
+ // Greedy spending loop
244
+ while (remainingBits > 0 && heapSize > 0) {
133
245
  const bfu = heapIndices[0]
134
246
  const cur = wordLengths[bfu] | 0
135
- const nxt = cur + 1
136
-
137
247
  const sz = bfuSizes[bfu] | 0
138
- const deltaBits = (WORD_LENGTH_BITS[nxt] - WORD_LENGTH_BITS[cur]) | 0
248
+ const deltaBits = WORD_LENGTH_DELTA_BITS[cur]
139
249
  const cost = deltaBits * sz
140
250
 
141
251
  if (cost > remainingBits || cost <= 0) {
142
- popRoot(heapIndices, heapPriorities)
252
+ const last = heapSize - 1
253
+ heapIndices[0] = heapIndices[last]
254
+ heapPriorities[0] = heapPriorities[last]
255
+ heapSize--
256
+ if (heapSize > 0) siftDown(heapIndices, heapPriorities, 0, heapSize)
143
257
  continue
144
258
  }
145
259
 
146
260
  remainingBits -= cost
261
+ const nxt = cur + 1
147
262
  wordLengths[bfu] = nxt
148
263
 
149
- if (nxt < MAX_WORD_LENGTH_INDEX) {
150
- const nxt2 = nxt + 1
151
- const deltaBits2 = (WORD_LENGTH_BITS[nxt2] - WORD_LENGTH_BITS[nxt]) | 0
152
- if (deltaBits2 > 0) {
153
- heapPriorities[0] = deltaDistPerCoeff(bfu, nxt, nxt2) / deltaBits2
154
- siftDown(heapIndices, heapPriorities, 0)
155
- } else {
156
- popRoot(heapIndices, heapPriorities)
157
- }
264
+ const deltaBitsNext = WORD_LENGTH_DELTA_BITS[nxt]
265
+ if (nxt < MAX_WORD_LENGTH_INDEX && deltaBitsNext > 0) {
266
+ const sfi = allScaleFactorIndices[bfu]
267
+ const distortionDelta =
268
+ biasedScaleFactors[sfi] * DISTORTION_DELTA_FACTORS[nxt]
269
+ heapPriorities[0] = distortionDelta / deltaBitsNext
270
+ siftDown(heapIndices, heapPriorities, 0, heapSize)
158
271
  } else {
159
- popRoot(heapIndices, heapPriorities)
272
+ const last = heapSize - 1
273
+ heapIndices[0] = heapIndices[last]
274
+ heapPriorities[0] = heapPriorities[last]
275
+ heapSize--
276
+ if (heapSize > 0) siftDown(heapIndices, heapPriorities, 0, heapSize)
160
277
  }
161
278
  }
162
279
 
163
- return { wordLengths, scaleFactorIndices: scaleFactorTable }
280
+ return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
164
281
  }
165
282
 
166
283
  /**
167
284
  * Find the optimal scale factor index for a set of coefficients.
168
285
  * Same semantics as the original.
169
286
  * @param {Float32Array} coefficients
287
+ * @param {number} length
170
288
  * @returns {number}
171
289
  */
172
- export function findScaleFactor(coefficients) {
290
+ export function findScaleFactor(coefficients, length) {
173
291
  let maxAmplitude = 0.0
174
- for (let i = 0; i < coefficients.length; i++) {
292
+ for (let i = 0; i < length; i++) {
175
293
  const a = Math.abs(coefficients[i])
176
294
  if (a > maxAmplitude) maxAmplitude = a
177
295
  }
@@ -180,25 +298,6 @@ export function findScaleFactor(coefficients) {
180
298
  return Math.max(0, Math.min(63, index))
181
299
  }
182
300
 
183
- /**
184
- * Removes the root element from a max-heap and maintains heap property.
185
- *
186
- * This function efficiently removes the maximum priority element from the heap
187
- * by replacing it with the last element and then restoring the heap property
188
- * through sifting down.
189
- *
190
- * @param {Array<number>} heapIndices
191
- * @param {Array<number>} heapPriorities
192
- */
193
- function popRoot(heapIndices, heapPriorities) {
194
- const last = heapIndices.length - 1
195
- heapIndices[0] = heapIndices[last]
196
- heapPriorities[0] = heapPriorities[last]
197
- heapIndices.pop()
198
- heapPriorities.pop()
199
- if (heapIndices.length) siftDown(heapIndices, heapPriorities, 0)
200
- }
201
-
202
301
  /**
203
302
  * Restores max-heap property by sifting an element down from a given position.
204
303
  *
@@ -207,12 +306,12 @@ function popRoot(heapIndices, heapPriorities) {
207
306
  * with its children and swaps with the larger child until the heap property
208
307
  * is satisfied, or it reaches a leaf position.
209
308
  *
210
- * @param {Array<number>} heapIndices
211
- * @param {Array<number>} heapPriorities
309
+ * @param {Int32Array} heapIndices
310
+ * @param {Float32Array} heapPriorities
212
311
  * @param {number} startIndex
312
+ * @param {number} heapSize
213
313
  */
214
- function siftDown(heapIndices, heapPriorities, startIndex) {
215
- const heapSize = heapIndices.length
314
+ function siftDown(heapIndices, heapPriorities, startIndex, heapSize) {
216
315
  let i = startIndex
217
316
  const idxVal = heapIndices[i]
218
317
  const prVal = heapPriorities[i]
@@ -158,3 +158,22 @@ export const INV_POWER_OF_TWO = (() => {
158
158
  }
159
159
  return table
160
160
  })()
161
+
162
+ export const WORD_LENGTH_DELTA_BITS = (() => {
163
+ const table = new Int32Array(MAX_WORD_LENGTH_INDEX)
164
+ for (let i = 0; i < MAX_WORD_LENGTH_INDEX; i++) {
165
+ table[i] = (WORD_LENGTH_BITS[i + 1] - WORD_LENGTH_BITS[i]) | 0
166
+ }
167
+ return table
168
+ })()
169
+
170
+ export const DISTORTION_DELTA_FACTORS = (() => {
171
+ const table = new Float64Array(MAX_WORD_LENGTH_INDEX)
172
+ table[0] = 2.0 - INV_POWER_OF_TWO[WORD_LENGTH_BITS[1]]
173
+ for (let i = 1; i < MAX_WORD_LENGTH_INDEX; i++) {
174
+ const bits1 = WORD_LENGTH_BITS[i] | 0
175
+ const bits2 = WORD_LENGTH_BITS[i + 1] | 0
176
+ table[i] = INV_POWER_OF_TWO[bits1] - INV_POWER_OF_TWO[bits2]
177
+ }
178
+ return table
179
+ })()
@@ -52,7 +52,7 @@ class EncoderOptions {
52
52
  name: 'Bit allocation bias',
53
53
  description:
54
54
  'Controls how bits are distributed across the frequency spectrum during allocation. Higher values focus more bits on loud spectral components, while lower values spread bits more evenly across frequencies.',
55
- range: [0.5, 3.0],
55
+ range: [0.0, 5.0],
56
56
  step: 0.01,
57
57
  },
58
58
  fixedBlockModes: {
@@ -392,11 +392,16 @@ export function quantizationStage(context) {
392
392
  scaleFactorIndices,
393
393
  } = allocateBits(bfuData, bfuSizes, bfuCount, options.allocationBias)
394
394
 
395
+ const slicedWordLengthIndices = allocation.slice(0, selectedBfuCount)
396
+ const slicedScaleFactorIndices = scaleFactorIndices.slice(
397
+ 0,
398
+ selectedBfuCount
399
+ )
395
400
  const quantizedCoefficients = []
396
401
 
397
402
  for (let bfu = 0; bfu < selectedBfuCount; bfu++) {
398
403
  const data = bfuData[bfu].subarray(0, bfuSizes[bfu])
399
- const wordLength = allocation[bfu]
404
+ const wordLength = slicedWordLengthIndices[bfu]
400
405
  const bitsPerSample = WORD_LENGTH_BITS[wordLength]
401
406
  const quantized = quantize(data, scaleFactorIndices[bfu], bitsPerSample)
402
407
  quantizedCoefficients.push(quantized)
@@ -404,8 +409,8 @@ export function quantizationStage(context) {
404
409
 
405
410
  return {
406
411
  nBfu: selectedBfuCount,
407
- scaleFactorIndices,
408
- wordLengthIndices: allocation.slice(0, selectedBfuCount),
412
+ scaleFactorIndices: slicedScaleFactorIndices,
413
+ wordLengthIndices: slicedWordLengthIndices,
409
414
  quantizedCoefficients,
410
415
  blockModes,
411
416
  }