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 +70 -0
- package/codec/coding/bitallocation.js +138 -41
- package/codec/core/options.js +20 -5
- package/codec/pipeline/encoder.js +26 -14
- package/dist/carta1-worker.min.js +1 -1
- package/dist/carta1-worker.min.js.map +1 -1
- package/dist/carta1.min.js +1 -1
- package/dist/carta1.min.js.map +1 -1
- package/package.json +1 -1
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)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
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
|
|
21
|
-
*
|
|
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
|
|
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
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 =
|
|
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 =
|
|
112
|
-
scaleFactorTable[bfuIndex] = sfi
|
|
113
|
-
|
|
210
|
+
const sfi = scaleFactorTable[bfuIndex]
|
|
114
211
|
if (sfi === 0) continue
|
|
115
212
|
|
|
116
213
|
const currentWl = 0
|
package/codec/core/options.js
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
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 =
|
|
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:
|
|
412
|
+
scaleFactorIndices: slicedScaleFactorIndices,
|
|
413
|
+
wordLengthIndices: slicedWordLengthIndices,
|
|
402
414
|
quantizedCoefficients,
|
|
403
415
|
blockModes,
|
|
404
416
|
}
|