carta1 1.1.6 → 1.1.8

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.
package/bin/cli.js CHANGED
@@ -82,6 +82,57 @@ function validateTitle(title) {
82
82
  return { valid: true }
83
83
  }
84
84
 
85
+ /**
86
+ * Validate fixed block modes format and values
87
+ *
88
+ * @param {string} modes - Modes string in format "a,b,c"
89
+ * @returns {Object} Validation result
90
+ * @returns {boolean} returns.valid - Whether modes are valid
91
+ * @returns {Array<number>} [returns.modes] - Parsed modes if valid
92
+ * @returns {string} [returns.error] - Error message if invalid
93
+ */
94
+ function validateModes(modes) {
95
+ if (!modes) {
96
+ return { valid: true }
97
+ }
98
+
99
+ const parts = modes.split(',')
100
+ if (parts.length !== 3) {
101
+ return {
102
+ valid: false,
103
+ error: `Modes must have exactly 3 values separated by commas, got ${parts.length}`,
104
+ }
105
+ }
106
+
107
+ const parsedModes = []
108
+ const validValues = [
109
+ [0, 2], // Position 1: 0 or 2
110
+ [0, 2], // Position 2: 0 or 2
111
+ [0, 3], // Position 3: 0 or 3
112
+ ]
113
+
114
+ for (let i = 0; i < 3; i++) {
115
+ const mode = parseInt(parts[i], 10)
116
+ if (isNaN(mode)) {
117
+ return {
118
+ valid: false,
119
+ error: `Mode at position ${i + 1} is not a number: "${parts[i]}"`,
120
+ }
121
+ }
122
+
123
+ if (!validValues[i].includes(mode)) {
124
+ return {
125
+ valid: false,
126
+ error: `Mode at position ${i + 1} must be ${validValues[i].join(' or ')}, got ${mode}`,
127
+ }
128
+ }
129
+
130
+ parsedModes.push(mode)
131
+ }
132
+
133
+ return { valid: true, modes: parsedModes }
134
+ }
135
+
85
136
  /**
86
137
  * Progress tracking utility for encoding/decoding operations
87
138
  *
@@ -386,6 +437,16 @@ async function encodeFile(inputFile, outputFile, options) {
386
437
  }
387
438
  }
388
439
 
440
+ // Validate modes if provided
441
+ let fixedBlockModes = null
442
+ if (options.modes) {
443
+ const validation = validateModes(options.modes)
444
+ if (!validation.valid) {
445
+ throw new Error(validation.error)
446
+ }
447
+ fixedBlockModes = validation.modes
448
+ }
449
+
389
450
  if (!options.quiet) {
390
451
  const bitrate = Math.round((BITRATE_PER_CHANNEL * reader.channels) / 1000)
391
452
  console.log(
@@ -408,6 +469,9 @@ async function encodeFile(inputFile, outputFile, options) {
408
469
  if (options.bias !== undefined) {
409
470
  encoderOptions.setValue('allocationBias', options.bias)
410
471
  }
472
+ if (fixedBlockModes) {
473
+ encoderOptions.setValue('fixedBlockModes', fixedBlockModes)
474
+ }
411
475
 
412
476
  const encodedFrames = AudioProcessor.encodeStream(reader, {
413
477
  channelCount: reader.channels,
@@ -642,6 +706,10 @@ async function main() {
642
706
  'Bit allocation bias (default: 1.0)',
643
707
  parseFloat
644
708
  )
709
+ .option(
710
+ '-m, --modes <modes>',
711
+ 'Fixed block modes [0/2],[0/2],[0/3] for low,mid,high bands (skips transient detection)'
712
+ )
645
713
  .argument('<input>', 'Input file path')
646
714
  .argument('<output>', 'Output file path')
647
715
  .parse()
@@ -693,3 +761,5 @@ main().catch((error) => {
693
761
  console.error(error.stack)
694
762
  process.exit(1)
695
763
  })
764
+
765
+ export { validateModes }
@@ -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,58 +30,158 @@ import {
31
30
  WORD_LENGTH_BITS,
32
31
  INV_POWER_OF_TWO,
33
32
  SCALE_FACTORS,
33
+ BFU_AMOUNTS,
34
34
  } from '../core/constants.js'
35
35
 
36
36
  /**
37
- * Allocate bits to BFUs using optimized greedy Lp-RDO
37
+ * Allocate bits by finding the optimal BFU count and distribution using RDO.
38
+ * This function searches through the valid BFU counts to find the one that
39
+ * minimizes the total perceptual distortion.
40
+ *
38
41
  * @param {Array<Float32Array>} bfuData
39
42
  * @param {Int32Array} bfuSizes
40
- * @param {number} maxBfuCount
43
+ * @param {number} maxBfuCount The maximum BFU index to consider (e.g., from BFU_AMOUNTS)
41
44
  * @param {number} allocationBias
42
45
  * @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
43
46
  */
44
47
  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,
48
+ const allScaleFactorIndices = new Int32Array(maxBfuCount)
49
+ const zeroBitDistortions = new Float32Array(maxBfuCount)
50
+
51
+ for (let i = 0; i < maxBfuCount; i++) {
52
+ const sz = bfuSizes[i] | 0
53
+ if (sz === 0) continue
54
+
55
+ const sfi = findScaleFactor(bfuData[i].subarray(0, sz))
56
+ allScaleFactorIndices[i] = sfi
57
+ if (sfi > 0) {
58
+ const scaleFactor = SCALE_FACTORS[sfi]
59
+ const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
60
+ zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
61
+ }
62
+ }
63
+
64
+ let bestResult = null
65
+ let minTotalDistortion = Infinity
66
+
67
+ for (const candidateBfuCount of BFU_AMOUNTS) {
68
+ if (candidateBfuCount > maxBfuCount) continue
69
+
70
+ const availableBits =
71
+ FRAME_BITS -
72
+ FRAME_OVERHEAD_BITS -
73
+ candidateBfuCount * BITS_PER_BFU_METADATA
74
+
75
+ if (availableBits < 0) continue
76
+
77
+ const rdoResult = distributeBitsRDO(
78
+ candidateBfuCount,
79
+ bfuSizes,
80
+ availableBits,
81
+ allocationBias,
82
+ allScaleFactorIndices
83
+ )
84
+
85
+ const totalDistortion = calculateTotalDistortion(
86
+ candidateBfuCount,
87
+ maxBfuCount,
88
+ bfuSizes,
89
+ rdoResult.wordLengths,
90
+ rdoResult.scaleFactorIndices,
91
+ allocationBias,
92
+ zeroBitDistortions
93
+ )
94
+
95
+ if (totalDistortion < minTotalDistortion) {
96
+ minTotalDistortion = totalDistortion
97
+ bestResult = {
98
+ bfuCount: candidateBfuCount,
99
+ allocation: rdoResult.wordLengths,
100
+ scaleFactorIndices: rdoResult.scaleFactorIndices,
101
+ }
102
+ }
61
103
  }
104
+
105
+ if (!bestResult) {
106
+ const fallbackBfuCount = BFU_AMOUNTS[0]
107
+ return {
108
+ bfuCount: fallbackBfuCount,
109
+ allocation: new Int32Array(fallbackBfuCount),
110
+ scaleFactorIndices: new Int32Array(NUM_BFUS),
111
+ }
112
+ }
113
+
114
+ return bestResult
115
+ }
116
+
117
+ /**
118
+ * Calculates the total distortion for a given allocation.
119
+ * This includes quantization distortion for coded bands and truncation
120
+ * distortion for uncoded bands.
121
+ * @param {number} activeBfuCount - The number of BFUs that were coded.
122
+ * @param {number} maxBfuCount - The total number of BFUs considered.
123
+ * @param {Int32Array} bfuSizes - Sizes of each BFU.
124
+ * @param {Int32Array} wordLengths - The allocated word length for each active BFU.
125
+ * @param {Int32Array} scaleFactorIndices - The scale factor index for each BFU.
126
+ * @param {number} allocationBias - The RDO bias factor.
127
+ * @param {Float32Array} zeroBitDistortions - Pre-calculated distortion for uncoded bands.
128
+ * @returns {number} The total calculated distortion.
129
+ */
130
+ function calculateTotalDistortion(
131
+ activeBfuCount,
132
+ maxBfuCount,
133
+ bfuSizes,
134
+ wordLengths,
135
+ scaleFactorIndices,
136
+ allocationBias,
137
+ zeroBitDistortions
138
+ ) {
139
+ let totalDistortion = 0.0
140
+
141
+ for (let i = 0; i < activeBfuCount; i++) {
142
+ const wl = wordLengths[i] | 0
143
+ const bits = WORD_LENGTH_BITS[wl] | 0
144
+ if (bits === 0) {
145
+ totalDistortion += zeroBitDistortions[i]
146
+ continue
147
+ }
148
+
149
+ const sfi = scaleFactorIndices[i]
150
+ if (sfi === 0) continue
151
+
152
+ const scaleFactor = SCALE_FACTORS[sfi]
153
+ const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
154
+ const distortionFactor = INV_POWER_OF_TWO[bits]
155
+
156
+ totalDistortion += effectiveScaleFactor * distortionFactor * bfuSizes[i]
157
+ }
158
+
159
+ for (let i = activeBfuCount; i < maxBfuCount; i++) {
160
+ totalDistortion += zeroBitDistortions[i]
161
+ }
162
+
163
+ return totalDistortion
62
164
  }
63
165
 
64
166
  /**
65
167
  * 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.
168
+ * (Modified to accept pre-calculated scale factors)
70
169
  *
71
170
  * @param {number} activeBfuCount
72
- * @param {Array<Float32Array>} bfuData
73
171
  * @param {Int32Array} bfuSizes
74
172
  * @param {number} remainingBits
75
173
  * @param {number} allocationBias
174
+ * @param {Int32Array} allScaleFactorIndices - Pre-calculated SFIs for all potential BFUs.
76
175
  * @returns {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
77
176
  */
78
177
  function distributeBitsRDO(
79
178
  activeBfuCount,
80
- bfuData,
81
179
  bfuSizes,
82
180
  remainingBits,
83
- allocationBias
181
+ allocationBias,
182
+ allScaleFactorIndices
84
183
  ) {
85
- const scaleFactorTable = new Int32Array(NUM_BFUS)
184
+ const scaleFactorTable = allScaleFactorIndices
86
185
  const wordLengths = new Int32Array(activeBfuCount)
87
186
 
88
187
  const heapIndices = []
@@ -108,9 +207,7 @@ function distributeBitsRDO(
108
207
  const sz = bfuSizes[bfuIndex] | 0
109
208
  if (sz === 0) continue
110
209
 
111
- const sfi = findScaleFactor(bfuData[bfuIndex].subarray(0, sz))
112
- scaleFactorTable[bfuIndex] = sfi
113
-
210
+ const sfi = scaleFactorTable[bfuIndex]
114
211
  if (sfi === 0) continue
115
212
 
116
213
  const currentWl = 0
@@ -19,6 +19,7 @@ class EncoderOptions {
19
19
  transientThresholdMid: 1.5,
20
20
  transientThresholdHigh: 2.0,
21
21
  allocationBias: 1.0,
22
+ fixedBlockModes: null,
22
23
  }
23
24
 
24
25
  this.metadata = {
@@ -54,6 +55,13 @@ class EncoderOptions {
54
55
  range: [0.5, 3.0],
55
56
  step: 0.01,
56
57
  },
58
+ fixedBlockModes: {
59
+ default: this.values.fixedBlockModes,
60
+ name: 'Fixed block modes',
61
+ description:
62
+ 'Override transient detection and use fixed block modes. Format: [low, mid, high] where low/mid can be 0 or 2, high can be 0 or 3. When set, transient detection is skipped.',
63
+ type: 'array',
64
+ },
57
65
  }
58
66
 
59
67
  // Apply user-provided options
@@ -86,12 +94,15 @@ class EncoderOptions {
86
94
  }
87
95
 
88
96
  const meta = this.metadata[key]
89
- const [min, max] = meta.range
90
97
 
91
- if (value < min || value > max) {
92
- throw new Error(
93
- `Value for ${key} must be between ${min} and ${max}, got ${value}`
94
- )
98
+ // Skip range validation for array types
99
+ if (meta.type !== 'array') {
100
+ const [min, max] = meta.range
101
+ if (value < min || value > max) {
102
+ throw new Error(
103
+ `Value for ${key} must be between ${min} and ${max}, got ${value}`
104
+ )
105
+ }
95
106
  }
96
107
 
97
108
  this.values[key] = value
@@ -126,6 +137,10 @@ class EncoderOptions {
126
137
  return this.values.allocationBias
127
138
  }
128
139
 
140
+ get fixedBlockModes() {
141
+ return this.values.fixedBlockModes
142
+ }
143
+
129
144
  getMetadata(key) {
130
145
  return this.metadata[key]
131
146
  }
@@ -125,17 +125,24 @@ export function blockSelectorStage(context) {
125
125
  */
126
126
  return (input) => {
127
127
  const { bands } = input
128
- const fftSizes = [FFT_SIZE_LOW, FFT_SIZE_MID, FFT_SIZE_HIGH]
129
- const blockModes = bands.map((bandSamples, bandIndex) => {
130
- const coeffs = performFFT(bandSamples, fftSizes[bandIndex])
131
- const transient = detectTransient(
132
- coeffs,
133
- bufferPool.transientDetection[bandIndex],
134
- options.transientThresholdLow
135
- )
136
- bufferPool.transientDetection[bandIndex] = coeffs
137
- return transient * Math.max(bandIndex + 1, 2)
138
- })
128
+
129
+ // Use fixed block modes if provided, otherwise do transient detection
130
+ let blockModes
131
+ if (options.fixedBlockModes) {
132
+ blockModes = options.fixedBlockModes
133
+ } else {
134
+ const fftSizes = [FFT_SIZE_LOW, FFT_SIZE_MID, FFT_SIZE_HIGH]
135
+ blockModes = bands.map((bandSamples, bandIndex) => {
136
+ const coeffs = performFFT(bandSamples, fftSizes[bandIndex])
137
+ const transient = detectTransient(
138
+ coeffs,
139
+ bufferPool.transientDetection[bandIndex],
140
+ options.transientThresholdLow
141
+ )
142
+ bufferPool.transientDetection[bandIndex] = coeffs
143
+ return transient * Math.max(bandIndex + 1, 2)
144
+ })
145
+ }
139
146
 
140
147
  return {
141
148
  bands,
@@ -385,11 +392,16 @@ export function quantizationStage(context) {
385
392
  scaleFactorIndices,
386
393
  } = allocateBits(bfuData, bfuSizes, bfuCount, options.allocationBias)
387
394
 
395
+ const slicedWordLengthIndices = allocation.slice(0, selectedBfuCount)
396
+ const slicedScaleFactorIndices = scaleFactorIndices.slice(
397
+ 0,
398
+ selectedBfuCount
399
+ )
388
400
  const quantizedCoefficients = []
389
401
 
390
402
  for (let bfu = 0; bfu < selectedBfuCount; bfu++) {
391
403
  const data = bfuData[bfu].subarray(0, bfuSizes[bfu])
392
- const wordLength = allocation[bfu]
404
+ const wordLength = slicedWordLengthIndices[bfu]
393
405
  const bitsPerSample = WORD_LENGTH_BITS[wordLength]
394
406
  const quantized = quantize(data, scaleFactorIndices[bfu], bitsPerSample)
395
407
  quantizedCoefficients.push(quantized)
@@ -397,8 +409,8 @@ export function quantizationStage(context) {
397
409
 
398
410
  return {
399
411
  nBfu: selectedBfuCount,
400
- scaleFactorIndices,
401
- wordLengthIndices: allocation.slice(0, selectedBfuCount),
412
+ scaleFactorIndices: slicedScaleFactorIndices,
413
+ wordLengthIndices: slicedWordLengthIndices,
402
414
  quantizedCoefficients,
403
415
  blockModes,
404
416
  }