carta1 1.0.11 → 1.0.13

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.
@@ -9,12 +9,12 @@ import {
9
9
  PSYMODEL_MIN_POWER_DB,
10
10
  PSYMODEL_NOT_EXAMINED,
11
11
  PSYMODEL_TONAL,
12
+ PSYMODEL_FFT_SIZE,
12
13
  PSYMODEL_NON_TONAL,
13
14
  PSYMODEL_IRRELEVANT,
14
15
  PSYMODEL_LOG10_FACTOR,
15
16
  PSYMODEL_POW10_FACTOR,
16
17
  PSYMODEL_CRITICAL_BANDS,
17
- PSYMODEL_CB_FREQ_INDICES,
18
18
  PSYMODEL_THRESHOLD_TABLE,
19
19
  PSYMODEL_FREQ_TO_CB_MAP,
20
20
  } from '../core/constants.js'
@@ -42,45 +42,33 @@ export function fromDb(db) {
42
42
  /**
43
43
  * Perform complete psychoacoustic analysis on MDCT coefficients
44
44
  * @param {Float32Array} mdctCoeffs - MDCT coefficients (512 samples)
45
- * @param {number} targetSize - Target FFT size for analysis
46
- * @param {Object} psychoBuffers - Reusable buffers for performance
45
+ * @param {number} normalizationDb - Target maximum power level in dB
47
46
  * @returns {Object} Analysis results including PSD, maskers, and thresholds
48
47
  */
49
- export function psychoAnalysis(
50
- mdctCoeffs,
51
- targetSize,
52
- psychoBuffers,
53
- normalizationDb
54
- ) {
55
- const fftScale = targetSize / 512
48
+ export function psychoAnalysis(mdctCoeffs, normalizationDb) {
49
+ const fftScale = PSYMODEL_FFT_SIZE / 512
56
50
 
57
51
  // Apply frequency band reversal correction
58
52
  const correctedCoeffs = applyMdctCorrection(mdctCoeffs)
59
53
 
60
54
  // Calculate power spectral density
61
- const psd = calculatePSDFromMDCT(correctedCoeffs, targetSize, normalizationDb)
55
+ const psd = calculatePSDFromMDCT(correctedCoeffs, normalizationDb)
62
56
 
63
57
  // Find tonal and non-tonal maskers
64
- const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(
65
- psd,
66
- psychoBuffers,
67
- fftScale
68
- )
58
+ const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(psd, fftScale)
69
59
 
70
- // Calculate global masking threshold
71
- const globalThreshold = calculateGlobalThreshold(
60
+ // Calculate critical band masking thresholds
61
+ const criticalBandThresholds = calculateCriticalBandThresholds(
72
62
  psd,
73
63
  tonalMaskers,
74
- nonTonalMaskers,
75
- psychoBuffers.threshold,
76
- targetSize
64
+ nonTonalMaskers
77
65
  )
78
66
 
79
67
  return {
80
68
  psd,
81
69
  tonalMaskers,
82
70
  nonTonalMaskers,
83
- globalThreshold,
71
+ criticalBandThresholds,
84
72
  flags,
85
73
  }
86
74
  }
@@ -112,11 +100,10 @@ function applyMdctCorrection(mdctCoeffs) {
112
100
  /**
113
101
  * Calculate Power Spectral Density from MDCT coefficients
114
102
  * @param {Float32Array} mdctCoeffs - MDCT coefficients
115
- * @param {number} targetSize - Target FFT size
116
103
  * @returns {Float32Array} Power spectral density in dB
117
104
  */
118
- function calculatePSDFromMDCT(mdctCoeffs, targetSize, normalizationDb) {
119
- const halfTargetSize = targetSize / 2
105
+ function calculatePSDFromMDCT(mdctCoeffs, normalizationDb) {
106
+ const halfTargetSize = PSYMODEL_FFT_SIZE / 2
120
107
  const psd = new Float32Array(halfTargetSize + 1)
121
108
 
122
109
  // Calculate power values and find maximum
@@ -132,36 +119,23 @@ function calculatePSDFromMDCT(mdctCoeffs, targetSize, normalizationDb) {
132
119
  const normFactor = 1 / (maxPower || 1)
133
120
  let maxPsdValue = -Infinity
134
121
 
135
- if (targetSize === 512) {
136
- // Direct averaging for same size
137
- for (let i = 0; i < 256; i++) {
138
- const avgPower = (powers[i * 2] + powers[i * 2 + 1]) * 0.5 * normFactor
139
- psd[i] = avgPower > 0 ? toDb(avgPower) : PSYMODEL_MIN_POWER_DB
140
- maxPsdValue = Math.max(maxPsdValue, psd[i])
141
- }
142
- psd[256] = psd[255]
143
- // Check if last value is the max
144
- maxPsdValue = Math.max(maxPsdValue, psd[256])
145
- } else {
146
- // Interpolate for different sizes
147
- const scaleFactor = 512 / targetSize
122
+ // Interpolate for different sizes
123
+ const scaleFactor = 512 / PSYMODEL_FFT_SIZE
148
124
 
149
- for (let i = 0; i <= halfTargetSize; i++) {
150
- const srcIdx = i * scaleFactor
151
- const srcIdxInt = Math.floor(srcIdx)
152
- const frac = srcIdx - srcIdxInt
125
+ for (let i = 0; i <= halfTargetSize; i++) {
126
+ const srcIdx = i * scaleFactor
127
+ const srcIdxInt = Math.floor(srcIdx)
128
+ const frac = srcIdx - srcIdxInt
153
129
 
154
- const power =
155
- srcIdxInt < 511
156
- ? powers[srcIdxInt] * (1 - frac) + powers[srcIdxInt + 1] * frac
157
- : powers[511]
130
+ const power =
131
+ srcIdxInt < 511
132
+ ? powers[srcIdxInt] * (1 - frac) + powers[srcIdxInt + 1] * frac
133
+ : powers[511]
158
134
 
159
- const normalizedPower = power * normFactor
160
- psd[i] =
161
- normalizedPower > 0 ? toDb(normalizedPower) : PSYMODEL_MIN_POWER_DB
135
+ const normalizedPower = power * normFactor
136
+ psd[i] = normalizedPower > 0 ? toDb(normalizedPower) : PSYMODEL_MIN_POWER_DB
162
137
 
163
- maxPsdValue = Math.max(maxPsdValue, psd[i])
164
- }
138
+ maxPsdValue = Math.max(maxPsdValue, psd[i])
165
139
  }
166
140
 
167
141
  // Apply normalization to target maximum
@@ -179,13 +153,12 @@ function calculatePSDFromMDCT(mdctCoeffs, targetSize, normalizationDb) {
179
153
  /**
180
154
  * Find all maskers (tonal and non-tonal) in the spectrum
181
155
  * @param {Float32Array} psd - Power spectral density
182
- * @param {Object} psychoBuffers - Reusable buffers
183
156
  * @param {number} fftScale - FFT scaling factor
184
157
  * @returns {Object} Flags array and lists of maskers
185
158
  */
186
- function findAllMaskers(psd, psychoBuffers, fftScale) {
159
+ function findAllMaskers(psd, fftScale) {
187
160
  // Initialize flags
188
- const flags = psychoBuffers.flags
161
+ const flags = new Uint8Array(PSYMODEL_FFT_SIZE / 2)
189
162
  flags.fill(PSYMODEL_NOT_EXAMINED)
190
163
 
191
164
  // Find tonal maskers first
@@ -401,52 +374,18 @@ function decimateMaskers(flags, tonalList, nonTonalList) {
401
374
  }
402
375
 
403
376
  /**
404
- * Calculate global masking threshold
377
+ * Calculate critical band masking thresholds
405
378
  * @param {Float32Array} psd - Power spectral density
406
379
  * @param {Array} tonalMaskers - List of tonal maskers
407
380
  * @param {Array} nonTonalMaskers - List of non-tonal maskers
408
- * @param {Float32Array} thresholdBuffer - Output buffer for threshold
409
- * @param {number} fftSize - FFT size
410
- * @returns {Float32Array} Global masking threshold
381
+ * @returns {Float32Array} Critical band thresholds
411
382
  */
412
- function calculateGlobalThreshold(
413
- psd,
414
- tonalMaskers,
415
- nonTonalMaskers,
416
- thresholdBuffer,
417
- fftSize
418
- ) {
383
+ function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
419
384
  const criticalBandThresholds = new Float32Array(
420
385
  PSYMODEL_CRITICAL_BANDS.length
421
386
  )
422
387
 
423
388
  // Calculate threshold for each critical band
424
- calculateCriticalBandThresholds(
425
- psd,
426
- tonalMaskers,
427
- nonTonalMaskers,
428
- criticalBandThresholds
429
- )
430
-
431
- // Interpolate to full spectrum with compensation
432
- interpolateThresholdCompensated(
433
- criticalBandThresholds,
434
- thresholdBuffer,
435
- fftSize
436
- )
437
-
438
- return thresholdBuffer
439
- }
440
-
441
- /**
442
- * Calculate masking threshold for each critical band
443
- */
444
- function calculateCriticalBandThresholds(
445
- psd,
446
- tonalMaskers,
447
- nonTonalMaskers,
448
- output
449
- ) {
450
389
  for (let i = 0; i < PSYMODEL_CRITICAL_BANDS.length; i++) {
451
390
  const bandData = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i]]
452
391
  const maskedBark = bandData[1]
@@ -470,8 +409,10 @@ function calculateCriticalBandThresholds(
470
409
  false // isTonal
471
410
  )
472
411
 
473
- output[i] = toDb(summedEnergy)
412
+ criticalBandThresholds[i] = toDb(summedEnergy)
474
413
  }
414
+
415
+ return criticalBandThresholds
475
416
  }
476
417
 
477
418
  /**
@@ -538,52 +479,3 @@ function calculateMaskingLevel(
538
479
 
539
480
  return maskerSpl + tonalOffset + maskingFunction
540
481
  }
541
-
542
- /**
543
- * Interpolate critical band thresholds to full spectrum
544
- * @param {Float32Array} cbThresholds - Critical band thresholds
545
- * @param {Float32Array} output - Output buffer
546
- * @param {number} fftSize - FFT size
547
- */
548
- function interpolateThresholdCompensated(cbThresholds, output, fftSize) {
549
- const COMPENSATION_FACTOR = 0.25
550
- const halfSize = fftSize / 2
551
-
552
- // Get frequency indices for critical bands
553
- const freqIndices = PSYMODEL_CB_FREQ_INDICES
554
-
555
- let bandIndex = 0
556
- for (let i = 0; i <= halfSize; i++) {
557
- if (i <= freqIndices[0]) {
558
- // Before first critical band
559
- output[i] = cbThresholds[0]
560
- } else if (i >= freqIndices[freqIndices.length - 1]) {
561
- // After last critical band
562
- output[i] = cbThresholds[cbThresholds.length - 1]
563
- } else {
564
- // Find current band
565
- while (
566
- bandIndex < freqIndices.length - 1 &&
567
- freqIndices[bandIndex + 1] < i
568
- ) {
569
- bandIndex++
570
- }
571
-
572
- // Interpolate with compensation
573
- const x0 = freqIndices[bandIndex]
574
- const x1 = freqIndices[bandIndex + 1]
575
- const y0 = cbThresholds[bandIndex]
576
- const y1 = cbThresholds[bandIndex + 1]
577
- const bandWidth = x1 - x0
578
-
579
- if (bandWidth > 0) {
580
- const t = (i - x0) / bandWidth
581
- const interpolated = y0 + (y1 - y0) * t
582
- const compensation = Math.abs(y1 - y0) * COMPENSATION_FACTOR
583
- output[i] = interpolated + compensation
584
- } else {
585
- output[i] = y0
586
- }
587
- }
588
- }
589
- }
@@ -18,6 +18,9 @@ import {
18
18
  SAMPLE_RATE,
19
19
  BFU_FREQUENCIES,
20
20
  PSYMODEL_MIN_POWER_DB,
21
+ PSYMODEL_CB_FREQ_INDICES,
22
+ INTERPOLATION_COMPENSATION_FACTOR,
23
+ PSYMODEL_FFT_SIZE,
21
24
  } from '../core/constants.js'
22
25
 
23
26
  /**
@@ -32,10 +35,10 @@ export function allocateBits(psychoResults, bfuData, bfuSizes, maxBfuCount) {
32
35
  // Calculate signal energy for each BFU
33
36
  const signalEnergyDb = calculateBfuEnergy(bfuData, bfuSizes, maxBfuCount)
34
37
 
35
- // Map psychoacoustic masking threshold to BFUs
36
- const maskingThreshold =
37
- psychoResults.maskingThreshold || psychoResults.globalThreshold
38
- const maskThresholdDb = mapThresholdToBfus(maskingThreshold)
38
+ // Interpolate critical band thresholds to BFUs
39
+ const maskThresholdDb = interpolateCriticalBandsToBfus(
40
+ psychoResults.criticalBandThresholds
41
+ )
39
42
 
40
43
  // Calculate Signal-to-Mask Ratios
41
44
  const smrValues = calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount)
@@ -99,25 +102,55 @@ function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
99
102
  }
100
103
 
101
104
  /**
102
- * Map frequency-domain masking threshold to BFU indices
103
- * @param {Float32Array} globalThreshold - Frequency-domain masking threshold
105
+ * Interpolate critical band thresholds directly to BFU frequencies
106
+ * @param {Float32Array} criticalBandThresholds - Critical band thresholds
104
107
  * @returns {Float32Array} Masking threshold per BFU
105
108
  */
106
- function mapThresholdToBfus(globalThreshold) {
109
+ function interpolateCriticalBandsToBfus(criticalBandThresholds) {
107
110
  const maskThresholdPerBfu = new Float32Array(NUM_BFUS)
108
111
  maskThresholdPerBfu.fill(PSYMODEL_MIN_POWER_DB)
109
112
 
110
- // Calculate frequency resolution
111
- const fftSize = (globalThreshold.length - 1) * 2
112
- const freqPerBin = SAMPLE_RATE / fftSize
113
+ const freqPerBin = SAMPLE_RATE / PSYMODEL_FFT_SIZE
114
+
115
+ // Get frequency indices for critical bands (from constants)
116
+ const freqIndices = PSYMODEL_CB_FREQ_INDICES
113
117
 
114
- // Map each BFU's center frequency to nearest FFT bin
115
118
  for (let i = 0; i < NUM_BFUS; i++) {
116
119
  const bfuFreq = BFU_FREQUENCIES[i]
120
+ // Convert BFU frequency to FFT bin index (rounded like current implementation)
117
121
  const fftBinIndex = Math.round(bfuFreq / freqPerBin)
118
122
 
119
- if (fftBinIndex >= 0 && fftBinIndex < globalThreshold.length) {
120
- maskThresholdPerBfu[i] = globalThreshold[fftBinIndex]
123
+ // Find which critical bands surround this FFT bin
124
+ let bandIndex = 0
125
+ while (
126
+ bandIndex < freqIndices.length - 1 &&
127
+ freqIndices[bandIndex + 1] < fftBinIndex
128
+ ) {
129
+ bandIndex++
130
+ }
131
+
132
+ // Apply same interpolation logic as interpolateThresholdCompensated
133
+ if (fftBinIndex <= freqIndices[0]) {
134
+ maskThresholdPerBfu[i] = criticalBandThresholds[0]
135
+ } else if (fftBinIndex >= freqIndices[freqIndices.length - 1]) {
136
+ maskThresholdPerBfu[i] =
137
+ criticalBandThresholds[criticalBandThresholds.length - 1]
138
+ } else {
139
+ const x0 = freqIndices[bandIndex]
140
+ const x1 = freqIndices[bandIndex + 1]
141
+ const y0 = criticalBandThresholds[bandIndex]
142
+ const y1 = criticalBandThresholds[bandIndex + 1]
143
+ const bandWidth = x1 - x0
144
+
145
+ if (bandWidth > 0) {
146
+ const t = (fftBinIndex - x0) / bandWidth
147
+ const interpolated = y0 + (y1 - y0) * t
148
+ const compensation =
149
+ Math.abs(y1 - y0) * INTERPOLATION_COMPENSATION_FACTOR
150
+ maskThresholdPerBfu[i] = interpolated + compensation
151
+ } else {
152
+ maskThresholdPerBfu[i] = y0
153
+ }
121
154
  }
122
155
  }
123
156
 
@@ -7,7 +7,6 @@ import {
7
7
  MAX_BFU_SIZE,
8
8
  QMF_DELAY,
9
9
  QMF_HIGH_BAND_DELAY,
10
- PSYMODEL_FFT_SIZE,
11
10
  } from './constants.js'
12
11
 
13
12
  export class BufferPool {
@@ -84,12 +83,6 @@ export class BufferPool {
84
83
  256: new Float32Array(256),
85
84
  }
86
85
 
87
- // Psychoacoustic model buffers
88
- this.psychoAnalysis = {
89
- flags: new Uint8Array(PSYMODEL_FFT_SIZE / 2),
90
- threshold: new Float32Array(PSYMODEL_FFT_SIZE / 2 + 1),
91
- }
92
-
93
86
  // Bit allocation buffers
94
87
  this.bfuData = new Array(NUM_BFUS)
95
88
  for (let i = 0; i < NUM_BFUS; i++) {
@@ -116,6 +116,9 @@ export const QMF_ODD = (() => {
116
116
  return odd
117
117
  })()
118
118
 
119
+ // Bit allocation
120
+ export const INTERPOLATION_COMPENSATION_FACTOR = 0.25
121
+
119
122
  // Quantization
120
123
  export const MAX_WORD_LENGTH_INDEX = 15
121
124
  export const WORD_LENGTH_BITS = new Int32Array([
package/codec/index.js CHANGED
@@ -19,6 +19,7 @@ import { FFT } from './transforms/fft.js'
19
19
  import {
20
20
  WORD_LENGTH_BITS,
21
21
  SPECS_PER_BFU,
22
+ SCALE_FACTORS,
22
23
  BFU_START_LONG,
23
24
  } from './core/constants.js'
24
25
 
@@ -39,5 +40,6 @@ export {
39
40
  FFT,
40
41
  WORD_LENGTH_BITS,
41
42
  SPECS_PER_BFU,
43
+ SCALE_FACTORS,
42
44
  BFU_START_LONG,
43
45
  }
@@ -31,7 +31,6 @@ import {
31
31
  FFT_SIZE_LOW,
32
32
  FFT_SIZE_MID,
33
33
  FFT_SIZE_HIGH,
34
- PSYMODEL_FFT_SIZE,
35
34
  MDCT_SIZE_SHORT,
36
35
  MDCT_SIZE_MID,
37
36
  MDCT_SIZE_LONG,
@@ -300,12 +299,7 @@ export function quantizationStage(context) {
300
299
  bufferPool.bfuData
301
300
  )
302
301
 
303
- const psychoResults = psychoAnalysis(
304
- coefficients,
305
- PSYMODEL_FFT_SIZE,
306
- bufferPool.psychoAnalysis,
307
- options.normalizationDb
308
- )
302
+ const psychoResults = psychoAnalysis(coefficients, options.normalizationDb)
309
303
  const { bfuCount: selectedBfuCount, allocation } = allocateBits(
310
304
  psychoResults,
311
305
  bfuData,