carta1 1.0.21 → 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.
@@ -71,7 +71,6 @@ class Carta1Worker {
71
71
  * @param {number} [options.transientThresholdLow] - Low band transient threshold
72
72
  * @param {number} [options.transientThresholdMid] - Mid band transient threshold
73
73
  * @param {number} [options.transientThresholdHigh] - High band transient threshold
74
- * @param {number} [options.normalizationDb] - Normalization level in dB
75
74
  * @returns {Promise<Object>} Encoding result
76
75
  * @returns {Blob} returns.aeaBlob - ATRAC1 encoded audio blob
77
76
  * @returns {Object} returns.shortBlockData - Short block frame indices
@@ -1,373 +1,275 @@
1
1
  /**
2
2
  * Carta1 Audio Codec - Bit Allocation Module
3
3
  *
4
- * This module implements the bit allocation algorithm for ATRAC1 compression,
5
- * distributing available bits among Block Floating Units (BFUs) based on
6
- * psychoacoustic masking and signal-to-mask ratios.
4
+ * This module implements the bit allocation system for the Carta1 audio codec,
5
+ * responsible for distributing available bits across frequency bands to minimize
6
+ * perceptual distortion while maintaining the target bitrate.
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.
13
+ *
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
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.
7
23
  */
8
24
 
9
25
  import {
26
+ NUM_BFUS,
10
27
  FRAME_BITS,
11
28
  FRAME_OVERHEAD_BITS,
12
29
  BITS_PER_BFU_METADATA,
13
- BFU_AMOUNTS,
14
30
  MAX_WORD_LENGTH_INDEX,
15
31
  WORD_LENGTH_BITS,
16
- DISTORTION_DELTA_DB,
17
- NUM_BFUS,
18
- SAMPLE_RATE,
19
- BFU_FREQUENCIES,
20
- BFU_AMOUNTS_COUNT,
21
- PSYMODEL_MIN_POWER_DB,
22
- PSYMODEL_CB_FREQ_INDICES,
23
- INTERPOLATION_COMPENSATION_FACTOR,
24
- PSYMODEL_FFT_SIZE,
32
+ LP_DISTORTION_CONSTANT,
33
+ SF_POW_P,
34
+ POW2_POS_P,
35
+ POW2_NEG_P,
36
+ SCALE_FACTORS,
25
37
  } from '../core/constants.js'
26
38
 
27
39
  /**
28
- * Allocate bits to BFUs based on psychoacoustic analysis
29
- * @param {Object} psychoResults - Psychoacoustic analysis results
30
- * @param {Array} bfuData - BFU coefficient data
31
- * @param {Array} bfuSizes - Size of each BFU
32
- * @param {number} maxBfuCount - Maximum number of BFUs to use
33
- * @returns {Object} Allocation results with BFU count and bit distribution
40
+ * Allocate bits to BFUs using optimized greedy Lp-RDO
41
+ * @param {Array<Float32Array>} bfuData
42
+ * @param {Int32Array} bfuSizes
43
+ * @param {number} maxBfuCount
44
+ * @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
34
45
  */
35
- export function allocateBits(psychoResults, bfuData, bfuSizes, maxBfuCount) {
36
- // Calculate signal energy for each BFU
37
- const signalEnergyDb = calculateBfuEnergy(bfuData, bfuSizes, maxBfuCount)
38
-
39
- // Interpolate critical band thresholds to BFUs
40
- const maskThresholdDb = interpolateCriticalBandsToBfus(
41
- psychoResults.criticalBandThresholds
42
- )
43
-
44
- // Calculate Signal-to-Mask Ratios
45
- const smrValues = calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount)
46
-
47
- // Determine optimal number of BFUs to use
48
- const actualBfuCount = selectOptimalBfuCount(
49
- smrValues,
50
- signalEnergyDb,
51
- maxBfuCount
52
- )
53
-
54
- // Calculate available bits for allocation
55
- const availableBits =
56
- FRAME_BITS - FRAME_OVERHEAD_BITS - actualBfuCount * BITS_PER_BFU_METADATA
46
+ export function allocateBits(bfuData, bfuSizes, maxBfuCount) {
47
+ const usedBFU = maxBfuCount
48
+ const avail =
49
+ FRAME_BITS - FRAME_OVERHEAD_BITS - usedBFU * BITS_PER_BFU_METADATA
57
50
 
58
- // Perform actual bit allocation
59
- const allocation = performBitAllocation(
60
- actualBfuCount,
61
- smrValues,
62
- bfuSizes,
63
- availableBits
64
- )
51
+ const rdoResult = distributeBitsRDO(usedBFU, bfuData, bfuSizes, avail)
65
52
 
66
53
  return {
67
- bfuCount: actualBfuCount,
68
- allocation,
54
+ bfuCount: usedBFU,
55
+ allocation: rdoResult.wordLengths,
56
+ scaleFactorIndices: rdoResult.scaleFactorIndices,
69
57
  }
70
58
  }
71
59
 
72
60
  /**
73
- * Calculate energy in dB for each BFU
74
- * @param {Array} bfuData - BFU coefficient data
75
- * @param {Array} bfuSizes - Size of each BFU
76
- * @param {number} bfuCount - Number of BFUs to process
77
- * @returns {Float32Array} Energy values in dB
61
+ * Distributes available bits across Band Frequency Units (BFUs) using Rate-Distortion Optimization.
62
+ *
63
+ * This function implements an optimized greedy RDO allocator that minimizes perceptual distortion
64
+ * while staying within the bit budget. It uses a priority queue (max-heap) to greedily allocate
65
+ * bits to the BFUs that provide the greatest distortion reduction per bit spent.
66
+ *
67
+ * Mathematical Model:
68
+ * Distortion for b bits is modeled as:
69
+ * D(b) = C * 2^{-p * b} for b >= 1
70
+ * D(0) = C * 2^{ p } (finite large step for "mute" case)
71
+ * where C = N * (sf^p) * k, with N = BFU length, sf = scale factor, p and k constants.
72
+ *
73
+ * Hence, for an upgrade from b1 -> b2 (b2 > b1), ΔD = D(b1) - D(b2) can be computed with 2 table lookups.
74
+ *
75
+ * Precomputed tables:
76
+ * POW2_NEG_P[b] = 2^{-p*b} for b=0..maxBits
77
+ * POW2_POS_P = 2^{ p}
78
+ * SF_POW_P[sfIndex] = (SCALE_FACTORS[sfIndex])^p
79
+ *
80
+ * @param {number} activeBfuCount
81
+ * @param {Array<Float32Array>} bfuData
82
+ * @param {Int32Array} bfuSizes
83
+ * @param {number} remainingBits
84
+ * @returns {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
78
85
  */
79
- function calculateBfuEnergy(bfuData, bfuSizes, bfuCount) {
80
- const energyPerBfu = new Float32Array(bfuCount)
81
-
82
- for (let i = 0; i < bfuCount; i++) {
83
- const data = bfuData[i]
84
- const size = bfuSizes[i]
85
-
86
- if (size === 0) {
87
- energyPerBfu[i] = PSYMODEL_MIN_POWER_DB
88
- continue
89
- }
90
-
91
- // Calculate mean square energy
92
- let energy = 0
93
- for (let j = 0; j < size; j++) {
94
- energy += data[j] * data[j]
86
+ function distributeBitsRDO(activeBfuCount, bfuData, bfuSizes, remainingBits) {
87
+ const scaleFactorTable = new Int32Array(NUM_BFUS)
88
+ const wordLengths = new Int32Array(activeBfuCount)
89
+
90
+ const baseWeight = new Float64Array(activeBfuCount) // C above
91
+ for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
92
+ const sz = bfuSizes[bfuIndex] | 0
93
+ if (sz > 0) {
94
+ const sfi = findScaleFactor(bfuData[bfuIndex].subarray(0, sz))
95
+ scaleFactorTable[bfuIndex] = sfi
96
+ baseWeight[bfuIndex] = sz * SF_POW_P[sfi] * LP_DISTORTION_CONSTANT
97
+ } else {
98
+ scaleFactorTable[bfuIndex] = 0
99
+ baseWeight[bfuIndex] = 0
95
100
  }
96
-
97
- // Convert to dB correctly using log10
98
- energyPerBfu[i] =
99
- energy > 0 ? 10 * Math.log10(energy / size) : PSYMODEL_MIN_POWER_DB
100
101
  }
101
102
 
102
- return energyPerBfu
103
- }
103
+ const heapIndices = []
104
+ const heapPriorities = []
105
+ const deltaDist = (bfuIndex, currentWl, nextWl) => {
106
+ const base = baseWeight[bfuIndex]
107
+ if (base === 0) return 0
104
108
 
105
- /**
106
- * Interpolate critical band thresholds directly to BFU frequencies
107
- * @param {Float32Array} criticalBandThresholds - Critical band thresholds
108
- * @returns {Float32Array} Masking threshold per BFU
109
- */
110
- function interpolateCriticalBandsToBfus(criticalBandThresholds) {
111
- const maskThresholdPerBfu = new Float32Array(NUM_BFUS)
112
- const freqPerBin = SAMPLE_RATE / PSYMODEL_FFT_SIZE
113
-
114
- for (let i = 0; i < NUM_BFUS; i++) {
115
- const bfuFreq = BFU_FREQUENCIES[i]
116
- const fftBinIndex = Math.round(bfuFreq / freqPerBin)
117
-
118
- // Find which critical bands surround this FFT bin
119
- let bandIndex = 0
120
- while (
121
- bandIndex < PSYMODEL_CB_FREQ_INDICES.length - 1 &&
122
- PSYMODEL_CB_FREQ_INDICES[bandIndex + 1] < fftBinIndex
123
- ) {
124
- bandIndex++
125
- }
109
+ const bits1 = WORD_LENGTH_BITS[currentWl] | 0
110
+ const bits2 = WORD_LENGTH_BITS[nextWl] | 0
126
111
 
127
- if (fftBinIndex <= PSYMODEL_CB_FREQ_INDICES[0]) {
128
- maskThresholdPerBfu[i] = criticalBandThresholds[0]
129
- } else if (
130
- fftBinIndex >=
131
- PSYMODEL_CB_FREQ_INDICES[PSYMODEL_CB_FREQ_INDICES.length - 1]
132
- ) {
133
- maskThresholdPerBfu[i] =
134
- criticalBandThresholds[criticalBandThresholds.length - 1]
135
- } else {
136
- const x0 = PSYMODEL_CB_FREQ_INDICES[bandIndex]
137
- const x1 = PSYMODEL_CB_FREQ_INDICES[bandIndex + 1]
138
- const y0 = criticalBandThresholds[bandIndex]
139
- const y1 = criticalBandThresholds[bandIndex + 1]
140
- const bandWidth = x1 - x0
112
+ const f1 = bits1 === 0 ? POW2_POS_P : POW2_NEG_P[bits1]
113
+ const f2 = POW2_NEG_P[bits2]
141
114
 
142
- if (bandWidth > 0) {
143
- const t = (fftBinIndex - x0) / bandWidth
144
- const interpolated = y0 + (y1 - y0) * t
145
- const compensation =
146
- Math.abs(y1 - y0) * INTERPOLATION_COMPENSATION_FACTOR
147
- maskThresholdPerBfu[i] = interpolated + compensation
148
- } else {
149
- maskThresholdPerBfu[i] = y0
150
- }
151
- }
115
+ return base * (f1 - f2)
152
116
  }
153
117
 
154
- return maskThresholdPerBfu
155
- }
118
+ for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
119
+ const sz = bfuSizes[bfuIndex] | 0
120
+ if (sz === 0) continue
121
+ if (scaleFactorTable[bfuIndex] === 0) continue
156
122
 
157
- /**
158
- * Calculate Signal-to-Mask Ratio for each BFU
159
- * @param {Float32Array} signalEnergyDb - Signal energy per BFU
160
- * @param {Float32Array} maskThresholdDb - Masking threshold per BFU
161
- * @param {number} maxBfuCount - Maximum number of BFUs
162
- * @returns {Float32Array} SMR values in dB
163
- */
164
- function calculateSMR(signalEnergyDb, maskThresholdDb, maxBfuCount) {
165
- const smrValues = new Float32Array(maxBfuCount)
123
+ const currentWl = 0
124
+ const nextWl = 1
125
+ const deltaBits =
126
+ (WORD_LENGTH_BITS[nextWl] - WORD_LENGTH_BITS[currentWl]) | 0
127
+ if (deltaBits <= 0) continue
166
128
 
167
- for (let bfu = 0; bfu < maxBfuCount; bfu++) {
168
- smrValues[bfu] =
169
- signalEnergyDb[bfu] <= PSYMODEL_MIN_POWER_DB
170
- ? -Infinity
171
- : signalEnergyDb[bfu] - maskThresholdDb[bfu]
172
- }
129
+ const deltaCost = deltaBits * sz
130
+ const dd = deltaDist(bfuIndex, currentWl, nextWl)
173
131
 
174
- return smrValues
175
- }
132
+ heapIndices.push(bfuIndex)
133
+ heapPriorities.push(dd / deltaCost) // priority = ΔD / ΔR
134
+ }
176
135
 
177
- /**
178
- * Select optimal number of BFUs to use based on SMR distribution
179
- * @param {Float32Array} smrValues - Signal-to-Mask ratios
180
- * @param {Float32Array} signalEnergyDb - Signal energy per BFU
181
- * @param {number} maxBfuCount - Maximum available BFUs
182
- * @returns {number} Optimal BFU count
183
- */
184
- function selectOptimalBfuCount(smrValues, signalEnergyDb, maxBfuCount) {
185
- // Check if we have any audible content (positive SMR)
186
- const hasAudibleContent = smrValues.some((smr) => smr > 0)
136
+ // Heapify
137
+ for (let i = (heapIndices.length >> 1) - 1; i >= 0; i--)
138
+ siftDown(heapIndices, heapPriorities, i)
187
139
 
188
- if (hasAudibleContent) {
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
- }
195
- }
196
- contributions.sort((a, b) => b.smr - a.smr)
140
+ // Greedy spending
141
+ while (remainingBits > 0 && heapIndices.length > 0) {
142
+ const bfu = heapIndices[0]
143
+ const cur = wordLengths[bfu] | 0
144
+ const nxt = cur + 1
197
145
 
198
- if (contributions.length === 0) {
199
- return BFU_AMOUNTS[BFU_AMOUNTS.length - 1]
200
- }
146
+ const sz = bfuSizes[bfu] | 0
147
+ const deltaBits = (WORD_LENGTH_BITS[nxt] - WORD_LENGTH_BITS[cur]) | 0
148
+ const cost = deltaBits * sz
201
149
 
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)
150
+ if (cost > remainingBits || cost <= 0) {
151
+ popRoot(heapIndices, heapPriorities)
152
+ continue
208
153
  }
209
154
 
210
- // Find where the gradient becomes shallow (diminishing returns)
211
- for (const candidateCount of BFU_AMOUNTS) {
212
- if (candidateCount > maxBfuCount) continue
213
-
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)
218
-
219
- const maxRequiredIndex = Math.max(...requiredBfus, 0)
155
+ remainingBits -= cost
156
+ wordLengths[bfu] = nxt
220
157
 
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) {
228
- return candidateCount
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
- }
245
- }
246
- }
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
- }
258
- }
259
-
260
- // Find smallest BFU count that includes all signal
261
- for (const candidateCount of BFU_AMOUNTS) {
262
- if (candidateCount > highestSignalBfu) {
263
- return candidateCount
158
+ if (nxt < MAX_WORD_LENGTH_INDEX) {
159
+ const nxt2 = nxt + 1
160
+ const deltaBits2 = (WORD_LENGTH_BITS[nxt2] - WORD_LENGTH_BITS[nxt]) | 0
161
+ if (deltaBits2 > 0) {
162
+ heapPriorities[0] = deltaDist(bfu, nxt, nxt2) / (deltaBits2 * sz)
163
+ siftDown(heapIndices, heapPriorities, 0)
164
+ } else {
165
+ popRoot(heapIndices, heapPriorities)
264
166
  }
167
+ } else {
168
+ popRoot(heapIndices, heapPriorities)
265
169
  }
266
170
  }
267
171
 
268
- return BFU_AMOUNTS[BFU_AMOUNTS_COUNT - 1]
172
+ return { wordLengths, scaleFactorIndices: scaleFactorTable }
269
173
  }
270
174
 
271
175
  /**
272
- * Allocate available bits among BFUs using a greedy algorithm
273
- * @param {number} actualBfuCount - Number of BFUs to allocate
274
- * @param {Float32Array} smrValues - Signal-to-Mask ratios
275
- * @param {Array} bfuSizes - Size of each BFU
276
- * @param {number} availableBits - Total bits available for allocation
277
- * @returns {Int32Array} Word length index allocation per BFU
176
+ * Find the optimal scale factor index for a set of coefficients.
177
+ * Same semantics as the original.
178
+ * @param {Float32Array} coefficients
179
+ * @returns {number}
278
180
  */
279
- function performBitAllocation(
280
- actualBfuCount,
281
- smrValues,
282
- bfuSizes,
283
- availableBits
284
- ) {
285
- const allocation = new Int32Array(actualBfuCount)
286
- const heap = []
287
-
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
- }
181
+ export function findScaleFactor(coefficients) {
182
+ let maxAmplitude = 0.0
183
+ const n = coefficients.length | 0
184
+ let i = 0
185
+ const n4 = n & ~3
186
+ for (; i < n4; i += 4) {
187
+ const a0 = Math.abs(coefficients[i])
188
+ const a1 = Math.abs(coefficients[i + 1])
189
+ const a2 = Math.abs(coefficients[i + 2])
190
+ const a3 = Math.abs(coefficients[i + 3])
191
+ if (a0 > maxAmplitude) maxAmplitude = a0
192
+ if (a1 > maxAmplitude) maxAmplitude = a1
193
+ if (a2 > maxAmplitude) maxAmplitude = a2
194
+ if (a3 > maxAmplitude) maxAmplitude = a3
293
195
  }
294
-
295
- // Build max heap
296
- for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
297
- heapifyDown(heap, i)
196
+ for (; i < n; i++) {
197
+ const a = Math.abs(coefficients[i])
198
+ if (a > maxAmplitude) maxAmplitude = a
298
199
  }
299
-
300
- // Greedy allocation
301
- while (heap.length > 0 && availableBits > 0) {
302
- const bestBfu = heap[0]
303
- const currentWl = allocation[bestBfu.bfu]
304
-
305
- if (currentWl >= MAX_WORD_LENGTH_INDEX) {
306
- heap[0] = heap[heap.length - 1]
307
- heap.pop()
308
- if (heap.length > 0) heapifyDown(heap, 0)
309
- continue
310
- }
311
-
312
- const upgradeCost = calculateUpgradeCost(bestBfu.bfu, currentWl, bfuSizes)
313
-
314
- if (upgradeCost <= availableBits) {
315
- availableBits -= upgradeCost
316
- allocation[bestBfu.bfu]++
317
- bestBfu.priority += DISTORTION_DELTA_DB[currentWl]
318
- heapifyDown(heap, 0)
200
+ if (maxAmplitude === 0) return 0
201
+
202
+ // Binary search
203
+ let low = 0
204
+ let high = SCALE_FACTORS.length - 1
205
+ while (low < high) {
206
+ const mid = (low + high) >> 1
207
+ if (maxAmplitude <= SCALE_FACTORS[mid]) {
208
+ high = mid
319
209
  } else {
320
- heap[0] = heap[heap.length - 1]
321
- heap.pop()
322
- if (heap.length > 0) heapifyDown(heap, 0)
210
+ low = mid + 1
323
211
  }
324
212
  }
325
-
326
- return allocation
213
+ return low
327
214
  }
328
215
 
329
216
  /**
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
217
+ * Removes the root element from a max-heap and maintains heap property.
218
+ *
219
+ * This function efficiently removes the maximum priority element from the heap
220
+ * by replacing it with the last element and then restoring the heap property
221
+ * through sifting down.
222
+ *
223
+ * @param {Array<number>} heapIndices
224
+ * @param {Array<number>} heapPriorities
335
225
  */
336
- function calculateUpgradeCost(bfuIndex, currentWl, bfuSizes) {
337
- const currentBits = WORD_LENGTH_BITS[currentWl] * bfuSizes[bfuIndex]
338
- const nextBits = WORD_LENGTH_BITS[currentWl + 1] * bfuSizes[bfuIndex]
339
- return nextBits - currentBits
226
+ function popRoot(heapIndices, heapPriorities) {
227
+ const last = heapIndices.length - 1
228
+ heapIndices[0] = heapIndices[last]
229
+ heapPriorities[0] = heapPriorities[last]
230
+ heapIndices.pop()
231
+ heapPriorities.pop()
232
+ if (heapIndices.length) siftDown(heapIndices, heapPriorities, 0)
340
233
  }
341
234
 
342
235
  /**
343
- * Maintain max heap property by moving element down
344
- * @param {Array} heap - Heap array
345
- * @param {number} startIdx - Starting index
236
+ * Restores max-heap property by sifting an element down from a given position.
237
+ *
238
+ * This function maintains the heap invariant where parent nodes have higher
239
+ * priority values than their children. It compares the element at startIndex
240
+ * with its children and swaps with the larger child until the heap property
241
+ * is satisfied, or it reaches a leaf position.
242
+ *
243
+ * @param {Array<number>} heapIndices
244
+ * @param {Array<number>} heapPriorities
245
+ * @param {number} startIndex
346
246
  */
347
- function heapifyDown(heap, startIdx) {
348
- const length = heap.length
349
- const element = heap[startIdx]
247
+ function siftDown(heapIndices, heapPriorities, startIndex) {
248
+ const heapSize = heapIndices.length
249
+ let i = startIndex
250
+ const idxVal = heapIndices[i]
251
+ const prVal = heapPriorities[i]
350
252
 
351
253
  while (true) {
352
- const leftChild = 2 * startIdx + 1
353
- const rightChild = 2 * startIdx + 2
354
- let largest = startIdx
355
- let largestPriority = element.priority
356
-
357
- if (leftChild < length && heap[leftChild].priority > largestPriority) {
358
- largest = leftChild
359
- largestPriority = heap[leftChild].priority
254
+ const l = (i << 1) + 1
255
+ const r = l + 1
256
+ let maxI = i
257
+ let maxP = prVal
258
+
259
+ if (l < heapSize && heapPriorities[l] > maxP) {
260
+ maxI = l
261
+ maxP = heapPriorities[l]
360
262
  }
361
-
362
- if (rightChild < length && heap[rightChild].priority > largestPriority) {
363
- largest = rightChild
263
+ if (r < heapSize && heapPriorities[r] > maxP) {
264
+ maxI = r
364
265
  }
266
+ if (maxI === i) break
365
267
 
366
- if (largest === startIdx) break
367
-
368
- heap[startIdx] = heap[largest]
369
- startIdx = largest
268
+ heapIndices[i] = heapIndices[maxI]
269
+ heapPriorities[i] = heapPriorities[maxI]
270
+ i = maxI
370
271
  }
371
272
 
372
- heap[startIdx] = element
273
+ heapIndices[i] = idxVal
274
+ heapPriorities[i] = prVal
373
275
  }