carta1 1.0.22 → 1.1.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.
@@ -1,9 +1,18 @@
1
1
  /**
2
- * Carta1 Audio Codec - Quantization
2
+ * Carta1 Audio Codec - Quantization Module
3
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.
4
+ * This module handles the quantization and dequantization of MDCT frequency coefficients
5
+ * for the Carta1 audio codec. It provides functions to convert floating-point frequency
6
+ * domain data into integer representations suitable for bitstream encoding.
7
+ *
8
+ * Key functions:
9
+ * - Quantization of coefficients using scale factors and bit depths
10
+ * - Dequantization for reconstruction during decoding
11
+ * - BFU (Band Frequency Unit) organization for psychoacoustic processing
12
+ *
13
+ * The quantization process uses configurable scale factors to maintain audio quality
14
+ * while achieving the target compression ratio. The module supports both uniform
15
+ * and non-uniform quantization schemes based on perceptual importance.
7
16
  */
8
17
 
9
18
  import {
@@ -16,70 +25,43 @@ import {
16
25
  } from '../core/constants.js'
17
26
 
18
27
  /**
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
28
+ * Quantize coefficients.
29
+ * @param {Float32Array} coefficients
30
+ * @param {number} scaleFactorIndex
31
+ * @param {number} bitsPerSample
32
+ * @returns {Int32Array} out
54
33
  */
55
34
  export function quantize(coefficients, scaleFactorIndex, bitsPerSample) {
35
+ const length = coefficients.length
36
+ const out = new Int32Array(length)
56
37
  if (bitsPerSample === 0 || scaleFactorIndex === 0) {
57
- return new Int32Array(coefficients.length)
38
+ // Zero fast path
39
+ out.fill(0, 0, length)
40
+ return out
58
41
  }
59
42
 
60
43
  const scaleFactor = SCALE_FACTORS[scaleFactorIndex]
61
44
  const quantRange = (1 << (bitsPerSample - QUANTIZATION_SIGN_BIT_SHIFT)) - 1
62
45
  const normFactor = quantRange / scaleFactor
63
46
 
64
- const quantized = new Int32Array(coefficients.length)
47
+ const hi = quantRange
48
+ const lo = -quantRange - 1
65
49
 
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
- )
50
+ for (let i = 0; i < length; i++) {
51
+ const x = coefficients[i] * normFactor
52
+ const y = (x + (x >= 0 ? 0.5 : -0.5)) | 0
53
+ out[i] = y > hi ? hi : y < lo ? lo : y
72
54
  }
73
55
 
74
- return quantized
56
+ return out
75
57
  }
76
58
 
77
59
  /**
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
60
+ * Dequantize coefficients.
61
+ * @param {Int32Array} quantized
62
+ * @param {number} scaleFactorIndex
63
+ * @param {number} bitsPerSample
64
+ * @returns {Float32Array}
83
65
  */
84
66
  export function dequantize(quantized, scaleFactorIndex, bitsPerSample) {
85
67
  if (bitsPerSample === 0 || scaleFactorIndex === 0) {
@@ -90,24 +72,41 @@ export function dequantize(quantized, scaleFactorIndex, bitsPerSample) {
90
72
  const quantRange = (1 << (bitsPerSample - QUANTIZATION_SIGN_BIT_SHIFT)) - 1
91
73
  const denormFactor = scaleFactor / quantRange
92
74
 
93
- const dequantized = new Float32Array(quantized.length)
94
-
75
+ const deq = new Float32Array(quantized.length)
95
76
  for (let i = 0; i < quantized.length; i++) {
96
- dequantized[i] = quantized[i] * denormFactor
77
+ deq[i] = quantized[i] * denormFactor
97
78
  }
98
-
99
- return dequantized
79
+ return deq
100
80
  }
101
81
 
102
82
  /**
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
83
+ * Groups MDCT coefficients into Band Frequency Units (BFUs) for bit allocation.
84
+ *
85
+ * This function organizes frequency-domain coefficients into perceptually meaningful
86
+ * frequency bands (BFUs) based on the block modes for each of the three frequency bands.
87
+ * It handles both long and short blocks, with different frequency resolutions and
88
+ * BFU start positions for each block type.
89
+ *
90
+ * The function implements copy-avoidance optimization: when a BFU slice is fully
91
+ * contained within a frequency band, it directly copies the data without buffer
92
+ * filling. For BFUs that span band boundaries or extend beyond the available
93
+ * coefficients, it handles partial copying and zero-padding appropriately.
94
+ *
95
+ * Frequency Band Structure:
96
+ * - Band 0: 0-128 coefficients (low frequencies)
97
+ * - Band 1: 128-256 coefficients (mid frequencies)
98
+ * - Band 2: 256-512 coefficients (high frequencies)
99
+ *
100
+ * Block Types:
101
+ * - Long blocks (mode 0): Higher frequency resolution, fewer temporal segments
102
+ * - Short blocks (mode 1): Lower frequency resolution, more temporal segments
103
+ *
104
+ * @param {Float32Array} coefficients
105
+ * @param {Array<number>} blockModes
106
+ * @returns {{bfuData: Array<Float32Array>, bfuSizes: Array<number>, bfuCount: number}}
108
107
  */
109
- export function groupIntoBFUs(coefficients, blockModes, bfuBuffers = null) {
110
- const bfuData = bfuBuffers ?? []
108
+ export function groupIntoBFUs(coefficients, blockModes) {
109
+ const bfuData = []
111
110
  const bfuSizes = []
112
111
 
113
112
  let coeffIndex = 0
@@ -123,22 +122,24 @@ export function groupIntoBFUs(coefficients, blockModes, bfuBuffers = null) {
123
122
  while (bfuIndex < bandEnd) {
124
123
  const size = SPECS_PER_BFU[bfuIndex]
125
124
  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]
125
+ const endPos = startPos + size
126
+ const bfu = new Float32Array(size)
127
+
128
+ if (startPos >= 0 && endPos <= bandSize) {
129
+ bfu.set(coefficients.subarray(bandStart + startPos, bandStart + endPos))
130
+ } else {
131
+ bfu.fill(0)
132
+ if (startPos < bandSize && endPos > 0) {
133
+ const srcStart = Math.max(0, startPos)
134
+ const srcEnd = Math.min(bandSize, endPos)
135
+ bfu.set(
136
+ coefficients.subarray(bandStart + srcStart, bandStart + srcEnd),
137
+ Math.max(0, -startPos)
138
+ )
136
139
  }
137
140
  }
138
141
 
139
- if (!bfuBuffers) {
140
- bfuData.push(bfu)
141
- }
142
+ bfuData.push(bfu)
142
143
  bfuSizes.push(size)
143
144
  bfuIndex++
144
145
  }
@@ -7,9 +7,6 @@ import {
7
7
  MAX_BFU_SIZE,
8
8
  QMF_DELAY,
9
9
  QMF_HIGH_BAND_DELAY,
10
- PSYMODEL_CRITICAL_BANDS,
11
- PSYMODEL_HALF_FFT_SIZE,
12
- PSYMODEL_NUM_BINS,
13
10
  } from './constants.js'
14
11
 
15
12
  export class BufferPool {
@@ -91,20 +88,5 @@ export class BufferPool {
91
88
  for (let i = 0; i < NUM_BFUS; i++) {
92
89
  this.bfuData[i] = new Float32Array(MAX_BFU_SIZE)
93
90
  }
94
-
95
- // Psychoacoustic model buffers (large Float32Arrays)
96
- this.psychoBuffers = {
97
- psdDb: new Float32Array(PSYMODEL_NUM_BINS),
98
- linPower: new Float32Array(PSYMODEL_NUM_BINS),
99
- tonalSPL: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
100
- nonTonalSPL: new Float32Array(PSYMODEL_CRITICAL_BANDS.length),
101
- maskSpl: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
102
- maskPsd: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
103
- maskBark: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
104
- maskTonalOffset: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
105
- maskPsdF1: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
106
- maskPsdF2: new Float32Array(PSYMODEL_HALF_FFT_SIZE),
107
- criticalBandThresholds: new Float32Array(PSYMODEL_CRITICAL_BANDS.length),
108
- }
109
91
  }
110
92
  }
@@ -151,59 +151,6 @@ export const SPREADING_MATRIX = (() => {
151
151
  return matrix
152
152
  })()
153
153
 
154
- // ISO/IEC 11172-3:1993 Psychoacoustic Model Constants
155
- export const PSYMODEL_MIN_POWER_DB = -200
156
- export const PSYMODEL_FFT_SIZE = 2048
157
- export const PSYMODEL_HALF_FFT_SIZE = PSYMODEL_FFT_SIZE >>> 1
158
- export const PSYMODEL_NUM_BINS = PSYMODEL_HALF_FFT_SIZE + 1
159
-
160
- // Pre-computes lookup tables for resampling the MDCT power spectrum into a PSD.
161
- // For each PSD bin, these tables provide the two source MDCT indices and the
162
- // linear interpolation weight needed to calculate the final power value.
163
- export const [
164
- PSYMODEL_PSD_SOURCE_IDX0,
165
- PSYMODEL_PSD_SOURCE_IDX1,
166
- PSYMODEL_PSD_INTERP_WEIGHT,
167
- ] = (() => {
168
- const half = PSYMODEL_FFT_SIZE >>> 1
169
- const sizeMinus1 = PSYMODEL_FFT_SIZE - 1
170
- const halfDiv2 = half / 2
171
- const scale = sizeMinus1 / half
172
-
173
- const idx0 = new Uint16Array(half + 1)
174
- const idx1 = new Uint16Array(half + 1)
175
- const w1 = new Float32Array(half + 1)
176
-
177
- for (let i = 0; i <= half; i++) {
178
- const src = i * scale
179
- const base = src | 0
180
-
181
- const j0 = base
182
- idx0[i] =
183
- j0 < half
184
- ? j0 < halfDiv2
185
- ? j0
186
- : half - 1 - (j0 - halfDiv2)
187
- : sizeMinus1 - (j0 - half)
188
-
189
- const j1 = base + 1
190
- if (j1 >= PSYMODEL_FFT_SIZE) {
191
- idx1[i] = idx0[i]
192
- } else {
193
- idx1[i] =
194
- j1 < half
195
- ? j1 < halfDiv2
196
- ? j1
197
- : half - 1 - (j1 - halfDiv2)
198
- : sizeMinus1 - (j1 - half)
199
- }
200
-
201
- w1[i] = src - base
202
- }
203
-
204
- return [idx0, idx1, w1]
205
- })()
206
-
207
154
  // FFT sizes for transient detection
208
155
  export const FFT_SIZE_LOW = 128
209
156
  export const FFT_SIZE_MID = 128
@@ -236,133 +183,6 @@ export const FRAME_HEADER_BITS = 16
236
183
  export const FRAME_WORD_LENGTH_BITS = 4
237
184
  export const FRAME_SCALE_FACTOR_BITS = 6
238
185
 
239
- // Tonal analysis flags
240
- export const PSYMODEL_NOT_EXAMINED = 0
241
- export const PSYMODEL_TONAL = 1
242
- export const PSYMODEL_NON_TONAL = 2
243
- export const PSYMODEL_IRRELEVANT = 3
244
-
245
- // Pre-computed conversion factors
246
- export const PSYMODEL_LOG10_FACTOR = 10 / Math.LN10
247
- export const PSYMODEL_POW10_FACTOR = Math.LN10 / 10
248
-
249
- // Critical band boundaries (0-24)
250
- export const PSYMODEL_CRITICAL_BANDS = new Uint8Array([
251
- 0, 1, 2, 4, 5, 7, 8, 10, 12, 14, 16, 19, 22, 26, 31, 36, 44, 49, 54, 60, 67,
252
- 74, 80, 92, 105,
253
- ])
254
-
255
- // Threshold table: [frequency_index, bark_value, threshold_value]
256
- // prettier-ignore
257
- export const PSYMODEL_THRESHOLD_TABLE = [
258
- [1.0, 0.85, 13.87], [2.0, 1.694, 2.85], [3.0, 2.525, -1.28], [4.0, 3.337, -3.5],
259
- [5.0, 4.124, -4.9], [6.0, 4.882, -5.89], [7.0, 5.608, -6.63], [8.0, 6.301, -7.21],
260
- [9.0, 6.959, -7.68], [10.0, 7.581, -8.08], [11.0, 8.169, -8.43], [12.0, 8.723, -8.75],
261
- [13.0, 9.244, -9.05], [14.0, 9.734, -9.33], [15.0, 10.195, -9.61], [16.0, 10.629, -9.89],
262
- [17.0, 11.037, -10.17], [18.0, 11.421, -10.47], [19.0, 11.783, -10.77], [20.0, 12.125, -11.1],
263
- [21.0, 12.448, -11.44], [22.0, 12.753, -11.79], [23.0, 13.042, -12.17], [24.0, 13.317, -12.56],
264
- [25.0, 13.577, -12.96], [26.0, 13.825, -13.38], [27.0, 14.062, -13.79], [28.0, 14.288, -14.21],
265
- [29.0, 14.504, -14.63], [30.0, 14.711, -15.03], [31.0, 14.909, -15.41], [32.0, 15.1, -15.77],
266
- [33.0, 15.283, -16.09], [34.0, 15.46, -16.37], [35.0, 15.631, -16.6], [36.0, 15.795, -16.78],
267
- [37.0, 15.955, -16.91], [38.0, 16.11, -16.97], [39.0, 16.26, -16.98], [40.0, 16.405, -16.92],
268
- [41.0, 16.547, -16.81], [42.0, 16.685, -16.65], [43.0, 16.82, -16.43], [44.0, 16.951, -16.17],
269
- [45.0, 17.079, -15.87], [46.0, 17.204, -15.54], [47.0, 17.327, -15.19], [48.0, 17.447, -14.82],
270
- [50.0, 17.68, -14.06], [52.0, 17.904, -13.32], [54.0, 18.121, -12.64], [56.0, 18.331, -12.04],
271
- [58.0, 18.534, -11.53], [60.0, 18.73, -11.11], [62.0, 18.922, -10.77], [64.0, 19.108, -10.49],
272
- [66.0, 19.288, -10.26], [68.0, 19.464, -10.07], [70.0, 19.635, -9.89], [72.0, 19.801, -9.72],
273
- [74.0, 19.963, -9.54], [76.0, 20.12, -9.37], [78.0, 20.273, -9.18], [80.0, 20.421, -8.97],
274
- [82.0, 20.565, -8.75], [84.0, 20.705, -8.51], [86.0, 20.84, -8.26], [88.0, 20.971, -7.98],
275
- [90.0, 21.099, -7.68], [92.0, 21.222, -7.36], [94.0, 21.341, -7.02], [96.0, 21.457, -6.65],
276
- [100.0, 21.676, -5.85], [104.0, 21.882, -4.93], [108.0, 22.074, -3.9], [112.0, 22.253, -2.75],
277
- [116.0, 22.42, -1.46], [120.0, 22.575, -0.03], [124.0, 22.721, 1.56], [128.0, 22.857, 3.31],
278
- [132.0, 22.984, 5.23], [136.0, 23.102, 7.34], [140.0, 23.213, 9.64], [144.0, 23.317, 12.15],
279
- [148.0, 23.414, 14.88], [152.0, 23.506, 17.84], [156.0, 23.592, 21.05], [160.0, 23.673, 24.52],
280
- [164.0, 23.749, 28.25], [168.0, 23.821, 32.27], [172.0, 23.888, 36.59], [176.0, 23.952, 41.22],
281
- [180.0, 24.013, 46.18], [184.0, 24.07, 51.49], [188.0, 24.124, 56.0], [192.0, 24.176, 56.0],
282
- [196.0, 24.225, 56.0], [200.0, 24.271, 56.0], [204.0, 24.316, 56.0], [208.0, 24.358, 56.0],
283
- [212.0, 24.398, 56.0], [216.0, 24.436, 56.0], [220.0, 24.473, 56.0], [224.0, 24.508, 56.0],
284
- [228.0, 24.541, 56.0], [232.0, 24.573, 56.0]
285
- ]
286
- export const PSYMODEL_CB_FREQ_INDICES = PSYMODEL_CRITICAL_BANDS.map(
287
- (cbIdx) => PSYMODEL_THRESHOLD_TABLE[cbIdx][0] - 1
288
- )
289
-
290
- // Frequency to critical band mapping
291
- export const PSYMODEL_FREQ_TO_CB_MAP = new Uint8Array([
292
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
293
- 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
294
- 41, 42, 43, 44, 45, 46, 47, 47, 48, 48, 49, 49, 50, 50, 51, 51, 52, 52, 53,
295
- 53, 54, 54, 55, 55, 56, 56, 57, 57, 58, 58, 59, 59, 60, 60, 61, 61, 62, 62,
296
- 63, 63, 64, 64, 65, 65, 66, 66, 67, 67, 68, 68, 69, 69, 70, 70, 71, 71, 71,
297
- 71, 72, 72, 72, 72, 73, 73, 73, 73, 74, 74, 74, 74, 75, 75, 75, 75, 76, 76,
298
- 76, 76, 77, 77, 77, 77, 78, 78, 78, 78, 79, 79, 79, 79, 80, 80, 80, 80, 81,
299
- 81, 81, 81, 82, 82, 82, 82, 83, 83, 83, 83, 84, 84, 84, 84, 85, 85, 85, 85,
300
- 86, 86, 86, 86, 87, 87, 87, 87, 88, 88, 88, 88, 89, 89, 89, 89, 90, 90, 90,
301
- 90, 91, 91, 91, 91, 92, 92, 92, 92, 93, 93, 93, 93, 94, 94, 94, 94, 95, 95,
302
- 95, 95, 96, 96, 96, 96, 97, 97, 97, 97, 98, 98, 98, 98, 99, 99, 99, 99, 100,
303
- 100, 100, 100, 101, 101, 101, 101, 102, 102, 102, 102, 103, 103, 103, 103,
304
- 104, 104, 104, 104, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105,
305
- 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105,
306
- 105,
307
- ])
308
-
309
- // Tonal detection neighbor offset patterns for different frequency ranges
310
- // These avoid per-iteration array allocations during tonal masker detection
311
- export const PSYMODEL_TONAL_OFFSETS_LOW_FREQ = Int8Array.of(-2, 2)
312
- export const PSYMODEL_TONAL_OFFSETS_MID_FREQ = Int8Array.of(-3, -2, 2, 3)
313
- export const PSYMODEL_TONAL_OFFSETS_HIGH_FREQ = Int8Array.of(-6, -5, -4, -3, -2 , 2, 3, 4, 5, 6) // prettier-ignore
314
-
315
- // Bit allocation
316
- export const INTERPOLATION_COMPENSATION_FACTOR = 0.25
317
-
318
- /**
319
- * Precomputed interpolation data for psychoacoustic mask interpolation
320
- * Creates lookup table for interpolating psychoacoustic mask values across BFUs
321
- */
322
- export const BFU_MASK_INTERPOLATION_TABLE = (() => {
323
- const interpolationEntries = new Array(NUM_BFUS)
324
- const frequencyPerBin = SAMPLE_RATE / PSYMODEL_FFT_SIZE
325
- const criticalBandIndices = PSYMODEL_CB_FREQ_INDICES
326
-
327
- for (let bfuIndex = 0; bfuIndex < NUM_BFUS; bfuIndex++) {
328
- const fftBinIndex = Math.round(BFU_FREQUENCIES[bfuIndex] / frequencyPerBin)
329
-
330
- // Find critical band index using binary search
331
- let lowIndex = 0
332
- let highIndex = criticalBandIndices.length - 1
333
-
334
- while (lowIndex < highIndex) {
335
- const midIndex = (lowIndex + highIndex + 1) >> 1
336
- if (criticalBandIndices[midIndex] <= fftBinIndex) {
337
- lowIndex = midIndex
338
- } else {
339
- highIndex = midIndex - 1
340
- }
341
- }
342
-
343
- const currentBandIndex = lowIndex
344
- const nextBandIndex = Math.min(
345
- currentBandIndex + 1,
346
- criticalBandIndices.length - 1
347
- )
348
- const currentBandFreq = criticalBandIndices[currentBandIndex]
349
- const nextBandFreq = criticalBandIndices[nextBandIndex]
350
- const bandWidth = nextBandFreq - currentBandFreq || 1
351
- const inverseBandWidth = 1 / bandWidth
352
-
353
- interpolationEntries[bfuIndex] = {
354
- band: currentBandIndex,
355
- next: nextBandIndex,
356
- x0: currentBandFreq,
357
- y0: currentBandIndex,
358
- y1: nextBandIndex,
359
- tInv: inverseBandWidth,
360
- }
361
- }
362
-
363
- return interpolationEntries
364
- })()
365
-
366
186
  // Quantization
367
187
  export const MAX_WORD_LENGTH_INDEX = 15
368
188
  export const WORD_LENGTH_BITS = new Int32Array([
@@ -396,3 +216,27 @@ export const SCALE_FACTORS = (() => {
396
216
  }
397
217
  return table
398
218
  })()
219
+
220
+ // RDO bit allocation
221
+ export const LP_DISTORTION_P = 4
222
+ export const LP_DISTORTION_CONSTANT = 1 / 80 // For L4 (p=4), it's 1/80.
223
+ export const { POW2_NEG_P, POW2_POS_P, SF_POW_P } = (() => {
224
+ // Calculate the 2^{-p*b} table
225
+ const maxBits = WORD_LENGTH_BITS[MAX_WORD_LENGTH_INDEX]
226
+ const pow2_neg_p = new Float64Array(maxBits + 1)
227
+ for (let b = 0; b <= maxBits; b++) {
228
+ pow2_neg_p[b] = Math.pow(2, -LP_DISTORTION_P * b)
229
+ }
230
+
231
+ // Calculate the 2^{p} constant for the D(0) case
232
+ const pow2_pos_p = Math.pow(2, LP_DISTORTION_P)
233
+
234
+ // Calculate the (scale_factor)^p table
235
+ const sfPowP = new Float64Array(SCALE_FACTORS.length)
236
+ for (let i = 0; i < SCALE_FACTORS.length; i++) {
237
+ sfPowP[i] = Math.pow(SCALE_FACTORS[i], LP_DISTORTION_P)
238
+ }
239
+
240
+ // Return all computed tables in a single object
241
+ return { POW2_NEG_P: pow2_neg_p, POW2_POS_P: pow2_pos_p, SF_POW_P: sfPowP }
242
+ })()
@@ -18,7 +18,6 @@ class EncoderOptions {
18
18
  transientThresholdLow: 1.0,
19
19
  transientThresholdMid: 1.5,
20
20
  transientThresholdHigh: 2.0,
21
- normalizationDb: 68,
22
21
  }
23
22
 
24
23
  this.metadata = {
@@ -46,14 +45,6 @@ class EncoderOptions {
46
45
  range: [0.01, 4],
47
46
  step: 0.01,
48
47
  },
49
- normalizationDb: {
50
- default: this.values.normalizationDb,
51
- name: 'Normalization Target (dB)',
52
- description:
53
- 'Sets the target maximum level in decibels for psychoacoustic model normalization. This affects the dynamic range allocation and quantization precision. Higher values may increase bitrate but preserve more detail.',
54
- range: [60, 80],
55
- step: 1,
56
- },
57
48
  }
58
49
 
59
50
  // Apply user-provided options
@@ -122,10 +113,6 @@ class EncoderOptions {
122
113
  return this.values.transientThresholdHigh
123
114
  }
124
115
 
125
- get normalizationDb() {
126
- return this.values.normalizationDb
127
- }
128
-
129
116
  getMetadata(key) {
130
117
  return this.metadata[key]
131
118
  }
@@ -122,7 +122,14 @@ export function imdctStage(context) {
122
122
  const TRANSFORM_FUNCS = [imdct256, imdct256, imdct512]
123
123
 
124
124
  /**
125
- * Inverse transform a single frequency band
125
+ * Inverse transform a single frequency band.
126
+ * @param {Float32Array} coeffs
127
+ * @param {number} bandIndex
128
+ * @param {number} blockMode
129
+ * @param {Float32Array} overlapBuffer
130
+ * @param {Object} bufferPool
131
+ * @param {Object} config
132
+ * @returns {Float32Array}
126
133
  */
127
134
  function transformBand(
128
135
  coeffs,
@@ -156,7 +163,14 @@ export function imdctStage(context) {
156
163
  }
157
164
 
158
165
  /**
159
- * Inverse transform using long block
166
+ * Inverse transform using long block.
167
+ * @param {Float32Array} coeffs
168
+ * @param {number} bandIndex
169
+ * @param {Float32Array} overlapBuffer
170
+ * @param {Object} bufferPool
171
+ * @param {Object} config
172
+ * @param {Object} transformFunc
173
+ * @returns {Float32Array}
160
174
  */
161
175
  function inverseLongBlock(
162
176
  coeffs,
@@ -219,7 +233,13 @@ export function imdctStage(context) {
219
233
  }
220
234
 
221
235
  /**
222
- * Inverse transform using short blocks
236
+ * Inverse transform using short blocks.
237
+ * @param {Float32Array} coeffs
238
+ * @param {number} bandIndex
239
+ * @param {Float32Array} overlapBuffer
240
+ * @param {Object} bufferPool
241
+ * @param {Object} config
242
+ * @returns {Float32Array}
223
243
  */
224
244
  function inverseShortBlocks(
225
245
  coeffs,
@@ -22,11 +22,7 @@ import {
22
22
  } from '../utils.js'
23
23
  import { qmfAnalysis } from '../transforms/qmf.js'
24
24
  import { mdct64, mdct256, mdct512 } from '../transforms/mdct.js'
25
- import {
26
- groupIntoBFUs,
27
- findScaleFactor,
28
- quantize,
29
- } from '../coding/quantization.js'
25
+ import { groupIntoBFUs, quantize } from '../coding/quantization.js'
30
26
  import { allocateBits } from '../coding/bitallocation.js'
31
27
  import { BufferPool } from '../core/buffers.js'
32
28
  import { EncoderOptions } from '../core/options.js'
@@ -44,7 +40,6 @@ import {
44
40
  MDCT_OVERLAP_SIZE,
45
41
  } from '../core/constants.js'
46
42
  import { performFFT, detectTransient } from '../analysis/transient.js'
47
- import { psychoAnalysis } from '../analysis/psychoacoustics.js'
48
43
 
49
44
  /**
50
45
  * QMF Analysis Stage - Splits audio into frequency bands
@@ -174,7 +169,13 @@ export function mdctStage(context) {
174
169
  const TRANSFORM_FUNCS = [mdct256, mdct256, mdct512]
175
170
 
176
171
  /**
177
- * Transform a single frequency band using MDCT
172
+ * Transform a single frequency band using MDCT.
173
+ * @param {Float32Array} samples
174
+ * @param {number} bandIndex
175
+ * @param {number} blockMode
176
+ * @param {Float32Array} overlapBuffer
177
+ * @param {Object} bufferPool
178
+ * @returns {Float32Array}
178
179
  */
179
180
  function transformBand(
180
181
  samples,
@@ -208,7 +209,14 @@ export function mdctStage(context) {
208
209
  }
209
210
 
210
211
  /**
211
- * Transform using long block (better frequency resolution)
212
+ * Transform using long block (better frequency resolution).
213
+ * @param {Float32Array} samples
214
+ * @param {number} bandIndex
215
+ * @param {Object} config
216
+ * @param {Object} transformFunc
217
+ * @param {Float32Array} overlapBuffer
218
+ * @param {Object} bufferPool
219
+ * @returns {Float32Array}
212
220
  */
213
221
  function transformLongBlock(
214
222
  samples,
@@ -243,7 +251,13 @@ export function mdctStage(context) {
243
251
  }
244
252
 
245
253
  /**
246
- * Transform using short blocks (better time resolution for transients)
254
+ * Transform using short blocks (better time resolution for transients).
255
+ * @param {Float32Array} samples
256
+ * @param {number} bandIndex
257
+ * @param {Object} config
258
+ * @param {Float32Array} overlapBuffer
259
+ * @param {Object} bufferPool
260
+ * @returns {Float32Array}
247
261
  */
248
262
  function transformShortBlocks(
249
263
  samples,
@@ -328,32 +342,22 @@ export function mdctStage(context) {
328
342
  }
329
343
 
330
344
  /**
331
- * Quantization Stage - Performs psychoacoustic analysis and quantization
345
+ * Quantization Stage - Performs RDO bit allocation and quantization
332
346
  *
333
347
  * The final encoding stage that:
334
348
  * 1. Groups coefficients into Block Floating Units (BFUs)
335
- * 2. Performs psychoacoustic analysis to determine masking thresholds
336
- * 3. Allocates bits based on perceptual importance
337
- * 4. Quantizes coefficients using scale factors and word lengths
349
+ * 2. Allocates bits using Rate-Distortion Optimization (RDO)
350
+ * 3. Quantizes coefficients using scale factors and word lengths
338
351
  *
339
- * Uses a perceptual model to ensure quantization noise remains below
340
- * the masking threshold, maximizing quality within the bit budget.
352
+ * Uses RDO to optimize the allocation of bits for maximum quality
353
+ * within the available bit budget.
341
354
  *
342
- * @param {Object} context - Pipeline context containing bufferPool and options
343
- * @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
344
- * @param {EncoderOptions} context.options - Encoding configuration options
345
355
  * @returns {Function} Stage function that processes MDCT results
346
- * @throws {Error} If bufferPool or options are not provided in context
356
+ * @throws {Error} If bufferPool is not provided in context
347
357
  */
348
- export function quantizationStage(context) {
349
- const bufferPool =
350
- context?.bufferPool ??
351
- throwError('quantizationStage: bufferPool is required')
352
- const options =
353
- context?.options ?? throwError('quantizationStage: options is required')
354
-
358
+ export function quantizationStage() {
355
359
  /**
356
- * Perform psychoacoustic analysis and quantization
360
+ * Perform RDO bit allocation and quantization
357
361
  * @param {Object} input - MDCT transform results
358
362
  * @param {Float32Array} input.coefficients - MDCT coefficients
359
363
  * @param {Array<number>} input.blockModes - Block modes
@@ -369,31 +373,21 @@ export function quantizationStage(context) {
369
373
 
370
374
  const { bfuData, bfuSizes, bfuCount } = groupIntoBFUs(
371
375
  coefficients,
372
- blockModes,
373
- bufferPool.bfuData
376
+ blockModes
374
377
  )
375
378
 
376
- const psychoResults = psychoAnalysis(
377
- coefficients,
378
- options.normalizationDb,
379
- bufferPool.psychoBuffers
380
- )
381
- const { bfuCount: selectedBfuCount, allocation } = allocateBits(
382
- psychoResults,
383
- bfuData,
384
- bfuSizes,
385
- bfuCount
386
- )
379
+ const {
380
+ bfuCount: selectedBfuCount,
381
+ allocation,
382
+ scaleFactorIndices,
383
+ } = allocateBits(bfuData, bfuSizes, bfuCount)
387
384
 
388
- const scaleFactorIndices = new Int32Array(selectedBfuCount)
389
385
  const quantizedCoefficients = []
390
386
 
391
387
  for (let bfu = 0; bfu < selectedBfuCount; bfu++) {
392
388
  const data = bfuData[bfu].subarray(0, bfuSizes[bfu])
393
389
  const wordLength = allocation[bfu]
394
390
  const bitsPerSample = WORD_LENGTH_BITS[wordLength]
395
-
396
- scaleFactorIndices[bfu] = findScaleFactor(data)
397
391
  const quantized = quantize(data, scaleFactorIndices[bfu], bitsPerSample)
398
392
  quantizedCoefficients.push(quantized)
399
393
  }