carta1 1.0.13 → 1.0.15

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.
@@ -17,6 +17,7 @@ import {
17
17
  NUM_BFUS,
18
18
  SAMPLE_RATE,
19
19
  BFU_FREQUENCIES,
20
+ BFU_AMOUNTS_COUNT,
20
21
  PSYMODEL_MIN_POWER_DB,
21
22
  PSYMODEL_CB_FREQ_INDICES,
22
23
  INTERPOLATION_COMPENSATION_FACTOR,
@@ -93,7 +94,7 @@ function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
93
94
  energy += data[j] * data[j]
94
95
  }
95
96
 
96
- // Convert to dB (10 * log10(energy/size))
97
+ // Convert to dB correctly using log10
97
98
  energyPerBfu[i] =
98
99
  energy > 0 ? 10 * Math.log10(energy / size) : PSYMODEL_MIN_POWER_DB
99
100
  }
@@ -108,36 +109,32 @@ function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
108
109
  */
109
110
  function interpolateCriticalBandsToBfus(criticalBandThresholds) {
110
111
  const maskThresholdPerBfu = new Float32Array(NUM_BFUS)
111
- maskThresholdPerBfu.fill(PSYMODEL_MIN_POWER_DB)
112
-
113
112
  const freqPerBin = SAMPLE_RATE / PSYMODEL_FFT_SIZE
114
113
 
115
- // Get frequency indices for critical bands (from constants)
116
- const freqIndices = PSYMODEL_CB_FREQ_INDICES
117
-
118
114
  for (let i = 0; i < NUM_BFUS; i++) {
119
115
  const bfuFreq = BFU_FREQUENCIES[i]
120
- // Convert BFU frequency to FFT bin index (rounded like current implementation)
121
116
  const fftBinIndex = Math.round(bfuFreq / freqPerBin)
122
117
 
123
118
  // Find which critical bands surround this FFT bin
124
119
  let bandIndex = 0
125
120
  while (
126
- bandIndex < freqIndices.length - 1 &&
127
- freqIndices[bandIndex + 1] < fftBinIndex
121
+ bandIndex < PSYMODEL_CB_FREQ_INDICES.length - 1 &&
122
+ PSYMODEL_CB_FREQ_INDICES[bandIndex + 1] < fftBinIndex
128
123
  ) {
129
124
  bandIndex++
130
125
  }
131
126
 
132
- // Apply same interpolation logic as interpolateThresholdCompensated
133
- if (fftBinIndex <= freqIndices[0]) {
127
+ if (fftBinIndex <= PSYMODEL_CB_FREQ_INDICES[0]) {
134
128
  maskThresholdPerBfu[i] = criticalBandThresholds[0]
135
- } else if (fftBinIndex >= freqIndices[freqIndices.length - 1]) {
129
+ } else if (
130
+ fftBinIndex >=
131
+ PSYMODEL_CB_FREQ_INDICES[PSYMODEL_CB_FREQ_INDICES.length - 1]
132
+ ) {
136
133
  maskThresholdPerBfu[i] =
137
134
  criticalBandThresholds[criticalBandThresholds.length - 1]
138
135
  } else {
139
- const x0 = freqIndices[bandIndex]
140
- const x1 = freqIndices[bandIndex + 1]
136
+ const x0 = PSYMODEL_CB_FREQ_INDICES[bandIndex]
137
+ const x1 = PSYMODEL_CB_FREQ_INDICES[bandIndex + 1]
141
138
  const y0 = criticalBandThresholds[bandIndex]
142
139
  const y1 = criticalBandThresholds[bandIndex + 1]
143
140
  const bandWidth = x1 - x0
@@ -168,7 +165,6 @@ function calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount) {
168
165
  const smrValues = new Float32Array(maxBfuCount)
169
166
 
170
167
  for (let bfu = 0; bfu < maxBfuCount; bfu++) {
171
- // BFUs with no signal have undefined SMR
172
168
  smrValues[bfu] =
173
169
  signalEnergyDb[bfu] <= PSYMODEL_MIN_POWER_DB
174
170
  ? -Infinity
@@ -190,111 +186,86 @@ function selectOptimalBfuCount(smrValues, signalEnergyDb, maxBfuCount) {
190
186
  const hasAudibleContent = smrValues.some((smr) => smr > 0)
191
187
 
192
188
  if (hasAudibleContent) {
193
- return selectBfuCountForAudibleContent(smrValues, maxBfuCount)
194
- } else {
195
- return selectBfuCountForInaudibleContent(
196
- smrValues,
197
- signalEnergyDb,
198
- maxBfuCount
199
- )
200
- }
201
- }
202
-
203
- /**
204
- * Select BFU count when content is above masking threshold
205
- * Uses diminishing returns approach - find where adding more BFUs
206
- * provides minimal benefit
207
- */
208
- function selectBfuCountForAudibleContent(smrValues, maxBfuCount) {
209
- // Create sorted list of BFUs by their contribution (positive SMR only)
210
- const contributions = []
211
- for (let i = 0; i < maxBfuCount; i++) {
212
- if (smrValues[i] > 0) {
213
- contributions.push({ index: i, smr: smrValues[i] })
189
+ // Create sorted list of BFUs by their contribution (positive SMR only)
190
+ const contributions = []
191
+ for (let i = 0; i < maxBfuCount; i++) {
192
+ if (smrValues[i] > 0) {
193
+ contributions.push({ index: i, smr: smrValues[i] })
194
+ }
214
195
  }
215
- }
216
- contributions.sort((a, b) => b.smr - a.smr)
217
-
218
- if (contributions.length === 0) {
219
- return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
220
- }
221
-
222
- // Find the natural cutoff using gradient of cumulative SMR
223
- const cumulativeSMR = []
224
- let sum = 0
225
- for (let i = 0; i < contributions.length; i++) {
226
- sum += contributions[i].smr
227
- cumulativeSMR.push(sum)
228
- }
196
+ contributions.sort((a, b) => b.smr - a.smr)
229
197
 
230
- // Find where the gradient becomes shallow (diminishing returns)
231
- for (const candidateCount of BFU_AMOUNTS) {
232
- if (candidateCount > maxBfuCount) continue
198
+ if (contributions.length === 0) {
199
+ return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
200
+ }
233
201
 
234
- // Check if all significant contributions fit within this count
235
- const requiredBfus = contributions
236
- .slice(0, Math.min(contributions.length, candidateCount))
237
- .map((c) => c.index)
202
+ // Find the natural cutoff using gradient of cumulative SMR
203
+ const cumulativeSMR = []
204
+ let sum = 0
205
+ for (let i = 0; i < contributions.length; i++) {
206
+ sum += contributions[i].smr
207
+ cumulativeSMR.push(sum)
208
+ }
238
209
 
239
- const maxRequiredIndex = Math.max(...requiredBfus, 0)
210
+ // Find where the gradient becomes shallow (diminishing returns)
211
+ for (const candidateCount of BFU_AMOUNTS) {
212
+ if (candidateCount > maxBfuCount) continue
240
213
 
241
- if (maxRequiredIndex < candidateCount) {
242
- // Check if we've captured the bulk of the contribution
243
- const capturedIndex = Math.min(
244
- candidateCount - 1,
245
- contributions.length - 1
246
- )
247
- if (capturedIndex >= contributions.length - 1) {
248
- return candidateCount
249
- }
214
+ // Check if all significant contributions fit within this count
215
+ const requiredBfus = contributions
216
+ .slice(0, Math.min(contributions.length, candidateCount))
217
+ .map((c) => c.index)
250
218
 
251
- // Use gradient to detect diminishing returns
252
- const currentValue = cumulativeSMR[capturedIndex]
253
- const totalValue = cumulativeSMR[cumulativeSMR.length - 1]
254
- const remainingIndices = contributions.length - capturedIndex - 1
219
+ const maxRequiredIndex = Math.max(...requiredBfus, 0)
255
220
 
256
- if (remainingIndices > 0) {
257
- const averageRemaining = (totalValue - currentValue) / remainingIndices
258
- const averageCaptured = currentValue / (capturedIndex + 1)
259
-
260
- // If remaining BFUs contribute much less on average, we've found our cutoff
261
- if (averageRemaining < averageCaptured * 0.1) {
221
+ if (maxRequiredIndex < candidateCount) {
222
+ // Check if we've captured the bulk of the contribution
223
+ const capturedIndex = Math.min(
224
+ candidateCount - 1,
225
+ contributions.length - 1
226
+ )
227
+ if (capturedIndex >= contributions.length - 1) {
262
228
  return candidateCount
263
229
  }
230
+
231
+ // Use gradient to detect diminishing returns
232
+ const remainingIndices = contributions.length - capturedIndex - 1
233
+ if (remainingIndices > 0) {
234
+ const avgRemaining =
235
+ (cumulativeSMR[cumulativeSMR.length - 1] -
236
+ cumulativeSMR[capturedIndex]) /
237
+ remainingIndices
238
+ const avgCaptured = cumulativeSMR[capturedIndex] / (capturedIndex + 1)
239
+
240
+ // If remaining BFUs contribute much less on average, we've found our cutoff
241
+ if (avgRemaining < avgCaptured * 0.1) {
242
+ return candidateCount
243
+ }
244
+ }
264
245
  }
265
246
  }
266
- }
267
-
268
- return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
269
- }
270
-
271
- /**
272
- * Select BFU count when all content is below masking threshold
273
- * Simply finds the smallest count that includes all BFUs with signal
274
- */
275
- function selectBfuCountForInaudibleContent(
276
- smrValues,
277
- signalEnergyDb,
278
- maxBfuCount
279
- ) {
280
- // Find the highest BFU index that contains actual signal
281
- let highestSignalBfu = -1
282
-
283
- for (let i = maxBfuCount - 1; i >= 0; i--) {
284
- if (signalEnergyDb[i] > PSYMODEL_MIN_POWER_DB && smrValues[i] > -Infinity) {
285
- highestSignalBfu = i
286
- break
247
+ } else {
248
+ // Find the highest BFU index that contains actual signal
249
+ let highestSignalBfu = -1
250
+ for (let i = maxBfuCount - 1; i >= 0; i--) {
251
+ if (
252
+ signalEnergyDb[i] > PSYMODEL_MIN_POWER_DB &&
253
+ smrValues[i] > -Infinity
254
+ ) {
255
+ highestSignalBfu = i
256
+ break
257
+ }
287
258
  }
288
- }
289
259
 
290
- // Find smallest BFU count that includes all signal
291
- for (const candidateCount of BFU_AMOUNTS) {
292
- if (candidateCount > highestSignalBfu) {
293
- return candidateCount
260
+ // Find smallest BFU count that includes all signal
261
+ for (const candidateCount of BFU_AMOUNTS) {
262
+ if (candidateCount > highestSignalBfu) {
263
+ return candidateCount
264
+ }
294
265
  }
295
266
  }
296
267
 
297
- return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
268
+ return BFU_AMOUNTS[BFU_AMOUNTS_COUNT - 1]
298
269
  }
299
270
 
300
271
  /**
@@ -311,36 +282,44 @@ function performBitAllocation(
311
282
  bfuSizes,
312
283
  availableBits
313
284
  ) {
314
- const allocation = new Int32Array(actualBfuCount).fill(0)
285
+ const allocation = new Int32Array(actualBfuCount)
286
+ const heap = []
315
287
 
316
- // Initialize priority queue with BFUs that have signal
317
- const priorityQueue = createPriorityQueue(smrValues, actualBfuCount)
288
+ // Initialize heap with BFUs that have signal
289
+ for (let i = 0; i < actualBfuCount; i++) {
290
+ if (smrValues[i] > -Infinity) {
291
+ heap.push({ bfu: i, priority: smrValues[i] })
292
+ }
293
+ }
294
+
295
+ // Build max heap
296
+ for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
297
+ heapifyDown(heap, i)
298
+ }
318
299
 
319
- // Greedy allocation: always upgrade BFU with highest priority
320
- while (!priorityQueue.isEmpty()) {
321
- const bestBfu = priorityQueue.peek()
300
+ // Greedy allocation
301
+ while (heap.length > 0 && availableBits > 0) {
302
+ const bestBfu = heap[0]
322
303
  const currentWl = allocation[bestBfu.bfu]
323
304
 
324
- // Check if BFU is already at maximum word length
325
305
  if (currentWl >= MAX_WORD_LENGTH_INDEX) {
326
- priorityQueue.pop()
306
+ heap[0] = heap[heap.length - 1]
307
+ heap.pop()
308
+ if (heap.length > 0) heapifyDown(heap, 0)
327
309
  continue
328
310
  }
329
311
 
330
- // Calculate cost to upgrade this BFU
331
312
  const upgradeCost = calculateUpgradeCost(bestBfu.bfu, currentWl, bfuSizes)
332
313
 
333
314
  if (upgradeCost <= availableBits) {
334
- // Perform upgrade
335
315
  availableBits -= upgradeCost
336
316
  allocation[bestBfu.bfu]++
337
-
338
- // Update priority based on distortion reduction
339
317
  bestBfu.priority += DISTORTION_DELTA_DB[currentWl]
340
- priorityQueue.updateTop()
318
+ heapifyDown(heap, 0)
341
319
  } else {
342
- // Not enough bits for this BFU
343
- priorityQueue.pop()
320
+ heap[0] = heap[heap.length - 1]
321
+ heap.pop()
322
+ if (heap.length > 0) heapifyDown(heap, 0)
344
323
  }
345
324
  }
346
325
 
@@ -349,6 +328,10 @@ function performBitAllocation(
349
328
 
350
329
  /**
351
330
  * Calculate bit cost to upgrade a BFU's word length
331
+ * @param {number} bfuIndex - Index of the BFU to upgrade
332
+ * @param {number} currentWl - Current word length of the BFU
333
+ * @param {number[]} bfuSizes - Array of BFU sizes (number of coefficients per BFU)
334
+ * @returns {number} Additional bits required to upgrade word length by 1
352
335
  */
353
336
  function calculateUpgradeCost(bfuIndex, currentWl, bfuSizes) {
354
337
  const currentBits = WORD_LENGTH_BITS[currentWl] * bfuSizes[bfuIndex]
@@ -356,52 +339,21 @@ function calculateUpgradeCost(bfuIndex, currentWl, bfuSizes) {
356
339
  return nextBits - currentBits
357
340
  }
358
341
 
359
- /**
360
- * Create a priority queue for BFU allocation
361
- * @param {Float32Array} smrValues - Initial priorities (SMR values)
362
- * @param {number} bfuCount - Number of BFUs
363
- * @returns {Object} Priority queue interface
364
- */
365
- function createPriorityQueue(smrValues, bfuCount) {
366
- const heap = []
367
-
368
- // Initialize with BFUs that have signal
369
- for (let i = 0; i < bfuCount; i++) {
370
- if (smrValues[i] > -Infinity) {
371
- heap.push({ bfu: i, priority: smrValues[i] })
372
- }
373
- }
374
-
375
- // Build max heap
376
- for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
377
- heapifyDown(heap, i)
378
- }
379
-
380
- return {
381
- isEmpty: () => heap.length === 0,
382
- peek: () => heap[0],
383
- pop: () => popMax(heap),
384
- updateTop: () => heapifyDown(heap, 0),
385
- }
386
- }
387
-
388
342
  /**
389
343
  * Maintain max heap property by moving element down
390
344
  * @param {Array} heap - Heap array
391
345
  * @param {number} startIdx - Starting index
392
346
  */
393
347
  function heapifyDown(heap, startIdx) {
394
- let idx = startIdx
395
- const element = heap[idx]
396
348
  const length = heap.length
349
+ const element = heap[startIdx]
397
350
 
398
351
  while (true) {
399
- const leftChild = 2 * idx + 1
400
- const rightChild = 2 * idx + 2
401
- let largest = idx
352
+ const leftChild = 2 * startIdx + 1
353
+ const rightChild = 2 * startIdx + 2
354
+ let largest = startIdx
402
355
  let largestPriority = element.priority
403
356
 
404
- // Find largest among parent and children
405
357
  if (leftChild < length && heap[leftChild].priority > largestPriority) {
406
358
  largest = leftChild
407
359
  largestPriority = heap[leftChild].priority
@@ -411,30 +363,11 @@ function heapifyDown(heap, startIdx) {
411
363
  largest = rightChild
412
364
  }
413
365
 
414
- // If parent is largest, we're done
415
- if (largest === idx) break
416
-
417
- // Swap and continue
418
- heap[idx] = heap[largest]
419
- idx = largest
420
- }
421
-
422
- heap[idx] = element
423
- }
424
-
425
- /**
426
- * Remove and return the maximum element from heap
427
- * @param {Array} heap - Heap array
428
- * @returns {Object} Maximum element
429
- */
430
- function popMax(heap) {
431
- const max = heap[0]
432
- const last = heap.pop()
366
+ if (largest === startIdx) break
433
367
 
434
- if (heap.length > 0) {
435
- heap[0] = last
436
- heapifyDown(heap, 0)
368
+ heap[startIdx] = heap[largest]
369
+ startIdx = largest
437
370
  }
438
371
 
439
- return max
372
+ heap[startIdx] = element
440
373
  }
@@ -41,9 +41,9 @@ export class BufferPool {
41
41
 
42
42
  // Transient detector
43
43
  this.transientDetection = {
44
- prevLowCoeffs: new Float32Array(128),
45
- prevMidCoeffs: new Float32Array(128),
46
- prevHighCoeffs: new Float32Array(256),
44
+ prevLowCoeffs: new Float32Array(64),
45
+ prevMidCoeffs: new Float32Array(64),
46
+ prevHighCoeffs: new Float32Array(128),
47
47
  }
48
48
 
49
49
  // MDCT/IMDCT work buffers
@@ -33,6 +33,7 @@ export const SPECS_PER_BFU = new Int32Array([
33
33
  20, 20, 20, 20, 20,
34
34
  ])
35
35
 
36
+ export const BFU_AMOUNTS_COUNT = 8
36
37
  export const BFU_AMOUNTS = new Int32Array([20, 28, 32, 36, 40, 44, 48, 52])
37
38
  export const BFU_BAND_BOUNDARIES = new Int32Array([20, 36, 52])
38
39
 
@@ -191,10 +192,57 @@ export const SPREADING_MATRIX = (() => {
191
192
  export const PSYMODEL_MIN_POWER_DB = -200
192
193
  export const PSYMODEL_FFT_SIZE = 2048
193
194
 
195
+ // Pre-computes lookup tables for resampling the MDCT power spectrum into a PSD.
196
+ // For each PSD bin, these tables provide the two source MDCT indices and the
197
+ // linear interpolation weight needed to calculate the final power value.
198
+ export const [
199
+ PSYMODEL_PSD_SOURCE_IDX0,
200
+ PSYMODEL_PSD_SOURCE_IDX1,
201
+ PSYMODEL_PSD_INTERP_WEIGHT,
202
+ ] = (() => {
203
+ const half = PSYMODEL_FFT_SIZE >>> 1
204
+ const sizeMinus1 = PSYMODEL_FFT_SIZE - 1
205
+ const halfDiv2 = half / 2
206
+ const scale = sizeMinus1 / half
207
+
208
+ const idx0 = new Uint16Array(half + 1)
209
+ const idx1 = new Uint16Array(half + 1)
210
+ const w1 = new Float32Array(half + 1)
211
+
212
+ for (let i = 0; i <= half; i++) {
213
+ const src = i * scale
214
+ const base = src | 0
215
+
216
+ const j0 = base
217
+ idx0[i] =
218
+ j0 < half
219
+ ? j0 < halfDiv2
220
+ ? j0
221
+ : half - 1 - (j0 - halfDiv2)
222
+ : sizeMinus1 - (j0 - half)
223
+
224
+ const j1 = base + 1
225
+ if (j1 >= PSYMODEL_FFT_SIZE) {
226
+ idx1[i] = idx0[i]
227
+ } else {
228
+ idx1[i] =
229
+ j1 < half
230
+ ? j1 < halfDiv2
231
+ ? j1
232
+ : half - 1 - (j1 - halfDiv2)
233
+ : sizeMinus1 - (j1 - half)
234
+ }
235
+
236
+ w1[i] = src - base
237
+ }
238
+
239
+ return [idx0, idx1, w1]
240
+ })()
241
+
194
242
  // FFT sizes for transient detection
195
- export const FFT_SIZE_LOW = 256
196
- export const FFT_SIZE_MID = 256
197
- export const FFT_SIZE_HIGH = 512
243
+ export const FFT_SIZE_LOW = 128
244
+ export const FFT_SIZE_MID = 128
245
+ export const FFT_SIZE_HIGH = 256
198
246
 
199
247
  // Quantization
200
248
  export const QUANTIZATION_SIGN_BIT_SHIFT = 1