carta1 1.0.13 → 1.0.14
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/codec/analysis/psychoacoustics.js +119 -147
- package/codec/coding/bitallocation.js +110 -177
- package/codec/core/constants.js +48 -0
- 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
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
PSYMODEL_CRITICAL_BANDS,
|
|
18
18
|
PSYMODEL_THRESHOLD_TABLE,
|
|
19
19
|
PSYMODEL_FREQ_TO_CB_MAP,
|
|
20
|
+
PSYMODEL_PSD_SOURCE_IDX0,
|
|
21
|
+
PSYMODEL_PSD_INTERP_WEIGHT,
|
|
22
|
+
PSYMODEL_PSD_SOURCE_IDX1,
|
|
20
23
|
} from '../core/constants.js'
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -48,11 +51,8 @@ export function fromDb(db) {
|
|
|
48
51
|
export function psychoAnalysis(mdctCoeffs, normalizationDb) {
|
|
49
52
|
const fftScale = PSYMODEL_FFT_SIZE / 512
|
|
50
53
|
|
|
51
|
-
// Apply frequency band reversal correction
|
|
52
|
-
const correctedCoeffs = applyMdctCorrection(mdctCoeffs)
|
|
53
|
-
|
|
54
54
|
// Calculate power spectral density
|
|
55
|
-
const psd = calculatePSDFromMDCT(
|
|
55
|
+
const psd = calculatePSDFromMDCT(mdctCoeffs, normalizationDb)
|
|
56
56
|
|
|
57
57
|
// Find tonal and non-tonal maskers
|
|
58
58
|
const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(psd, fftScale)
|
|
@@ -67,83 +67,48 @@ export function psychoAnalysis(mdctCoeffs, normalizationDb) {
|
|
|
67
67
|
return {
|
|
68
68
|
psd,
|
|
69
69
|
tonalMaskers,
|
|
70
|
+
tonalMaskersCount: tonalMaskers.length,
|
|
70
71
|
nonTonalMaskers,
|
|
72
|
+
nonTonalMaskersCount: nonTonalMaskers.length,
|
|
71
73
|
criticalBandThresholds,
|
|
72
74
|
flags,
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
/**
|
|
77
|
-
* Apply frequency band reversal correction to MDCT coefficients
|
|
78
|
-
* @param {Float32Array} mdctCoeffs - Original MDCT coefficients
|
|
79
|
-
* @returns {Float32Array} Corrected coefficients
|
|
80
|
-
*/
|
|
81
|
-
function applyMdctCorrection(mdctCoeffs) {
|
|
82
|
-
const corrected = new Float32Array(512)
|
|
83
|
-
|
|
84
|
-
// Low band: direct copy
|
|
85
|
-
corrected.set(mdctCoeffs.subarray(0, 128), 0)
|
|
86
|
-
|
|
87
|
-
// Mid band: reversed
|
|
88
|
-
for (let i = 0; i < 128; i++) {
|
|
89
|
-
corrected[128 + i] = mdctCoeffs[255 - i]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// High band: reversed
|
|
93
|
-
for (let i = 0; i < 256; i++) {
|
|
94
|
-
corrected[256 + i] = mdctCoeffs[511 - i]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return corrected
|
|
98
|
-
}
|
|
99
|
-
|
|
100
78
|
/**
|
|
101
79
|
* Calculate Power Spectral Density from MDCT coefficients
|
|
102
80
|
* @param {Float32Array} mdctCoeffs - MDCT coefficients
|
|
103
81
|
* @returns {Float32Array} Power spectral density in dB
|
|
104
82
|
*/
|
|
105
83
|
function calculatePSDFromMDCT(mdctCoeffs, normalizationDb) {
|
|
106
|
-
const
|
|
107
|
-
const psd = new Float32Array(
|
|
108
|
-
|
|
109
|
-
// Calculate power values and find maximum
|
|
110
|
-
const powers = new Float32Array(512)
|
|
111
|
-
let maxPower = 0
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < 512; i++) {
|
|
114
|
-
const power = mdctCoeffs[i] * mdctCoeffs[i]
|
|
115
|
-
powers[i] = power
|
|
116
|
-
maxPower = Math.max(maxPower, power)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const normFactor = 1 / (maxPower || 1)
|
|
120
|
-
let maxPsdValue = -Infinity
|
|
84
|
+
const half = PSYMODEL_FFT_SIZE >>> 1
|
|
85
|
+
const psd = new Float32Array(half + 1)
|
|
86
|
+
const linearPower = new Float32Array(half + 1)
|
|
121
87
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const srcIdx = i * scaleFactor
|
|
127
|
-
const srcIdxInt = Math.floor(srcIdx)
|
|
128
|
-
const frac = srcIdx - srcIdxInt
|
|
88
|
+
let maxLinearPower = 0
|
|
89
|
+
for (let i = 0; i <= half; i++) {
|
|
90
|
+
const a = mdctCoeffs[PSYMODEL_PSD_SOURCE_IDX0[i]]
|
|
91
|
+
const b = mdctCoeffs[PSYMODEL_PSD_SOURCE_IDX1[i]]
|
|
129
92
|
|
|
130
93
|
const power =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const normalizedPower = power * normFactor
|
|
136
|
-
psd[i] = normalizedPower > 0 ? toDb(normalizedPower) : PSYMODEL_MIN_POWER_DB
|
|
94
|
+
a * a * (1 - PSYMODEL_PSD_INTERP_WEIGHT[i]) +
|
|
95
|
+
b * b * PSYMODEL_PSD_INTERP_WEIGHT[i]
|
|
96
|
+
linearPower[i] = power
|
|
137
97
|
|
|
138
|
-
|
|
98
|
+
if (power > maxLinearPower) {
|
|
99
|
+
maxLinearPower = power
|
|
100
|
+
}
|
|
139
101
|
}
|
|
140
102
|
|
|
141
|
-
|
|
142
|
-
|
|
103
|
+
const offset =
|
|
104
|
+
normalizationDb - PSYMODEL_LOG10_FACTOR * Math.log(maxLinearPower || 1)
|
|
143
105
|
|
|
144
|
-
for (let i = 0; i
|
|
145
|
-
|
|
146
|
-
|
|
106
|
+
for (let i = 0; i <= half; i++) {
|
|
107
|
+
const power = linearPower[i]
|
|
108
|
+
if (power > 0) {
|
|
109
|
+
psd[i] = PSYMODEL_LOG10_FACTOR * Math.log(power) + offset
|
|
110
|
+
} else {
|
|
111
|
+
psd[i] = PSYMODEL_MIN_POWER_DB
|
|
147
112
|
}
|
|
148
113
|
}
|
|
149
114
|
|
|
@@ -162,10 +127,10 @@ function findAllMaskers(psd, fftScale) {
|
|
|
162
127
|
flags.fill(PSYMODEL_NOT_EXAMINED)
|
|
163
128
|
|
|
164
129
|
// Find tonal maskers first
|
|
165
|
-
|
|
130
|
+
const tonalList = findTonalMaskers(psd, flags, fftScale)
|
|
166
131
|
|
|
167
132
|
// Find non-tonal maskers
|
|
168
|
-
|
|
133
|
+
const nonTonalList = findNonTonalMaskers(flags, psd)
|
|
169
134
|
|
|
170
135
|
// Apply decimation to remove weak maskers
|
|
171
136
|
const decimated = decimateMaskers(flags, tonalList, nonTonalList)
|
|
@@ -187,68 +152,67 @@ function findAllMaskers(psd, fftScale) {
|
|
|
187
152
|
function findTonalMaskers(psd, flags, fftScale) {
|
|
188
153
|
const tonalList = []
|
|
189
154
|
const maxTonalBin = Math.floor(249 * fftScale)
|
|
190
|
-
const fftSize = (psd.length - 1) * 2
|
|
191
155
|
|
|
192
156
|
// Frequency range boundaries
|
|
193
157
|
const RANGE_LOW = Math.floor(63 * fftScale)
|
|
194
158
|
const RANGE_MID = Math.floor(127 * fftScale)
|
|
195
159
|
const RANGE_HIGH = Math.floor(150 * fftScale)
|
|
196
160
|
|
|
197
|
-
for (let k = 1; k <
|
|
161
|
+
for (let k = 1; k < psd.length - 1 && k <= maxTonalBin; k++) {
|
|
198
162
|
// Check for local maximum
|
|
199
163
|
if (psd[k] <= psd[k - 1] || psd[k] < psd[k + 1]) continue
|
|
200
164
|
|
|
201
165
|
// Determine search range based on frequency
|
|
202
|
-
|
|
203
|
-
if (
|
|
166
|
+
let searchOffsets
|
|
167
|
+
if (k > 2 && k < RANGE_LOW) {
|
|
168
|
+
searchOffsets = [-2, 2]
|
|
169
|
+
} else if (k >= RANGE_LOW && k < RANGE_MID) {
|
|
170
|
+
searchOffsets = [-3, -2, 2, 3]
|
|
171
|
+
} else if (k >= RANGE_MID && k < RANGE_HIGH) {
|
|
172
|
+
searchOffsets = [-6, -5, -4, -3, -2, 2, 3, 4, 5, 6]
|
|
173
|
+
} else {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
204
176
|
|
|
205
177
|
// Check tonality criteria
|
|
206
178
|
if (isTonal(psd, k, searchOffsets)) {
|
|
207
179
|
// Calculate combined SPL
|
|
208
180
|
const spl = toDb(fromDb(psd[k - 1]) + fromDb(psd[k]) + fromDb(psd[k + 1]))
|
|
209
|
-
|
|
210
181
|
tonalList.push({ index: k, spl })
|
|
211
182
|
markTonalComponent(flags, k, searchOffsets)
|
|
212
183
|
}
|
|
213
184
|
}
|
|
214
185
|
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get search offsets for tonality check based on frequency
|
|
220
|
-
*/
|
|
221
|
-
function getSearchOffsets(k, rangeLow, rangeMid, rangeHigh) {
|
|
222
|
-
if (k > 2 && k < rangeLow) return [-2, 2]
|
|
223
|
-
if (k >= rangeLow && k < rangeMid) return [-3, -2, 2, 3]
|
|
224
|
-
if (k >= rangeMid && k < rangeHigh) return [-6, -5, -4, -3, -2, 2, 3, 4, 5, 6]
|
|
225
|
-
return null
|
|
186
|
+
return tonalList
|
|
226
187
|
}
|
|
227
188
|
|
|
228
189
|
/**
|
|
229
190
|
* Check if a peak is tonal based on surrounding bins
|
|
191
|
+
* @param {Float32Array} psd - Power spectral density array
|
|
192
|
+
* @param {number} k - Index of the peak to test
|
|
193
|
+
* @param {number[]} offsets - Array of offset indices to check around the peak
|
|
194
|
+
* @returns {boolean} True if the peak qualifies as tonal
|
|
230
195
|
*/
|
|
231
196
|
function isTonal(psd, k, offsets) {
|
|
232
|
-
const TONAL_THRESHOLD = 7
|
|
233
|
-
|
|
197
|
+
const TONAL_THRESHOLD = 7.0
|
|
234
198
|
for (const offset of offsets) {
|
|
235
199
|
const idx = k + offset
|
|
236
|
-
if (idx >= 0 && idx < psd.length) {
|
|
237
|
-
|
|
200
|
+
if (idx >= 0 && idx < psd.length && psd[k] - psd[idx] < TONAL_THRESHOLD) {
|
|
201
|
+
return false
|
|
238
202
|
}
|
|
239
203
|
}
|
|
240
|
-
|
|
241
204
|
return true
|
|
242
205
|
}
|
|
243
206
|
|
|
244
207
|
/**
|
|
245
208
|
* Mark component and neighbors as tonal/irrelevant
|
|
209
|
+
* @param {Uint8Array} flags - Component classification flags array
|
|
210
|
+
* @param {number} k - Index of the tonal component
|
|
211
|
+
* @param {number[]} searchOffsets - Array of offset indices used for tonal detection
|
|
246
212
|
*/
|
|
247
213
|
function markTonalComponent(flags, k, searchOffsets) {
|
|
248
214
|
flags[k] = PSYMODEL_TONAL
|
|
249
|
-
|
|
250
|
-
const allOffsets = [...searchOffsets, -1, 1]
|
|
251
|
-
for (const offset of allOffsets) {
|
|
215
|
+
for (const offset of [...searchOffsets, -1, 1]) {
|
|
252
216
|
const idx = k + offset
|
|
253
217
|
if (idx >= 0 && idx < flags.length && idx !== k) {
|
|
254
218
|
flags[idx] = PSYMODEL_IRRELEVANT
|
|
@@ -270,7 +234,6 @@ function findNonTonalMaskers(flags, psd) {
|
|
|
270
234
|
const cbEnd =
|
|
271
235
|
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i + 1]][0] - 1
|
|
272
236
|
|
|
273
|
-
// Calculate band energy and centroid
|
|
274
237
|
const bandAnalysis = analyzeCriticalBand(
|
|
275
238
|
psd,
|
|
276
239
|
flags,
|
|
@@ -294,11 +257,17 @@ function findNonTonalMaskers(flags, psd) {
|
|
|
294
257
|
}
|
|
295
258
|
}
|
|
296
259
|
|
|
297
|
-
return
|
|
260
|
+
return nonTonalList
|
|
298
261
|
}
|
|
299
262
|
|
|
300
263
|
/**
|
|
301
264
|
* Analyze a critical band for non-tonal masking
|
|
265
|
+
* @param {Float32Array} psd - Power spectral density array
|
|
266
|
+
* @param {Uint8Array} flags - Component classification flags array
|
|
267
|
+
* @param {number} startBin - Starting frequency bin of the critical band
|
|
268
|
+
* @param {number} endBin - Ending frequency bin of the critical band
|
|
269
|
+
* @param {number} bandIndex - Critical band index for threshold table lookup
|
|
270
|
+
* @returns {Object} Analysis results with power and centroid offset
|
|
302
271
|
*/
|
|
303
272
|
function analyzeCriticalBand(psd, flags, startBin, endBin, bandIndex) {
|
|
304
273
|
let totalPower = 0
|
|
@@ -310,9 +279,11 @@ function analyzeCriticalBand(psd, flags, startBin, endBin, bandIndex) {
|
|
|
310
279
|
const power = fromDb(psd[k])
|
|
311
280
|
totalPower += power
|
|
312
281
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
282
|
+
if (k < PSYMODEL_FREQ_TO_CB_MAP.length) {
|
|
283
|
+
const barkDiff =
|
|
284
|
+
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[k]][1] - baseBark
|
|
285
|
+
weightedPower += power * barkDiff
|
|
286
|
+
}
|
|
316
287
|
|
|
317
288
|
flags[k] = PSYMODEL_IRRELEVANT
|
|
318
289
|
}
|
|
@@ -385,6 +356,33 @@ function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
|
|
|
385
356
|
PSYMODEL_CRITICAL_BANDS.length
|
|
386
357
|
)
|
|
387
358
|
|
|
359
|
+
// Pre-collect all maskers with their properties
|
|
360
|
+
const maskers = []
|
|
361
|
+
|
|
362
|
+
for (const masker of tonalMaskers) {
|
|
363
|
+
const j = masker.index
|
|
364
|
+
if (j < PSYMODEL_FREQ_TO_CB_MAP.length) {
|
|
365
|
+
maskers.push({
|
|
366
|
+
spl: masker.spl,
|
|
367
|
+
psd: psd[j],
|
|
368
|
+
bark: PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[j]][1],
|
|
369
|
+
isTonal: true,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const masker of nonTonalMaskers) {
|
|
375
|
+
const j = masker.index
|
|
376
|
+
if (j < PSYMODEL_FREQ_TO_CB_MAP.length) {
|
|
377
|
+
maskers.push({
|
|
378
|
+
spl: masker.spl,
|
|
379
|
+
psd: psd[j],
|
|
380
|
+
bark: PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[j]][1],
|
|
381
|
+
isTonal: false,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
388
386
|
// Calculate threshold for each critical band
|
|
389
387
|
for (let i = 0; i < PSYMODEL_CRITICAL_BANDS.length; i++) {
|
|
390
388
|
const bandData = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i]]
|
|
@@ -395,19 +393,21 @@ function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
|
|
|
395
393
|
let summedEnergy = fromDb(quietThreshold)
|
|
396
394
|
|
|
397
395
|
// Add contributions from all maskers
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
396
|
+
for (const masker of maskers) {
|
|
397
|
+
const barkDistance = maskedBark - masker.bark
|
|
398
|
+
|
|
399
|
+
// Check if within masking range
|
|
400
|
+
if (barkDistance >= -3 && barkDistance < 8) {
|
|
401
|
+
const maskedDb = calculateMaskingLevel(
|
|
402
|
+
masker.spl,
|
|
403
|
+
masker.psd,
|
|
404
|
+
masker.bark,
|
|
405
|
+
barkDistance,
|
|
406
|
+
masker.isTonal
|
|
407
|
+
)
|
|
408
|
+
summedEnergy += fromDb(maskedDb)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
411
|
|
|
412
412
|
criticalBandThresholds[i] = toDb(summedEnergy)
|
|
413
413
|
}
|
|
@@ -417,37 +417,12 @@ function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
|
|
|
417
417
|
|
|
418
418
|
/**
|
|
419
419
|
* Calculate energy contributions from a set of maskers
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (j >= PSYMODEL_FREQ_TO_CB_MAP.length) continue
|
|
427
|
-
|
|
428
|
-
const maskerBark = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[j]][1]
|
|
429
|
-
const barkDistance = maskedBark - maskerBark
|
|
430
|
-
|
|
431
|
-
// Check if within masking range
|
|
432
|
-
if (barkDistance < -3 || barkDistance >= 8) continue
|
|
433
|
-
|
|
434
|
-
// Calculate masking level
|
|
435
|
-
const maskedDb = calculateMaskingLevel(
|
|
436
|
-
masker.spl,
|
|
437
|
-
psd[j],
|
|
438
|
-
maskerBark,
|
|
439
|
-
barkDistance,
|
|
440
|
-
isTonal
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
totalEnergy += fromDb(maskedDb)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return totalEnergy
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Calculate masking level based on masker properties
|
|
420
|
+
* @param {number} maskerSpl - Sound pressure level of the masker in dB
|
|
421
|
+
* @param {number} maskerPsd - Power spectral density of the masker in dB
|
|
422
|
+
* @param {number} maskerBark - Bark frequency of the masker
|
|
423
|
+
* @param {number} barkDistance - Distance in Bark scale from masker to maskee
|
|
424
|
+
* @param {boolean} isTonal - Whether the masker is tonal or non-tonal
|
|
425
|
+
* @returns {number} Masking threshold contribution in dB
|
|
451
426
|
*/
|
|
452
427
|
function calculateMaskingLevel(
|
|
453
428
|
maskerSpl,
|
|
@@ -456,25 +431,22 @@ function calculateMaskingLevel(
|
|
|
456
431
|
barkDistance,
|
|
457
432
|
isTonal
|
|
458
433
|
) {
|
|
459
|
-
// Masking index
|
|
460
434
|
const tonalOffset = isTonal
|
|
461
435
|
? -1.525 - 0.275 * maskerBark - 4.5
|
|
462
436
|
: -1.525 - 0.175 * maskerBark - 0.5
|
|
463
437
|
|
|
464
|
-
|
|
465
|
-
const psdFactor1 = 0.4 * maskerPsd + 6
|
|
438
|
+
const psdFactor1 = 0.4 * maskerPsd + 6.0
|
|
466
439
|
const psdFactor2 = 0.15 * maskerPsd
|
|
467
440
|
|
|
468
|
-
// Calculate masking function
|
|
469
441
|
let maskingFunction
|
|
470
|
-
if (barkDistance < -1) {
|
|
471
|
-
maskingFunction = 17 * (barkDistance + 1) - psdFactor1
|
|
472
|
-
} else if (barkDistance < 0) {
|
|
442
|
+
if (barkDistance < -1.0) {
|
|
443
|
+
maskingFunction = 17.0 * (barkDistance + 1.0) - psdFactor1
|
|
444
|
+
} else if (barkDistance < 0.0) {
|
|
473
445
|
maskingFunction = psdFactor1 * barkDistance
|
|
474
|
-
} else if (barkDistance < 1) {
|
|
475
|
-
maskingFunction = -17 * barkDistance
|
|
446
|
+
} else if (barkDistance < 1.0) {
|
|
447
|
+
maskingFunction = -17.0 * barkDistance
|
|
476
448
|
} else {
|
|
477
|
-
maskingFunction = -(barkDistance - 1) * (17 - psdFactor2) - 17
|
|
449
|
+
maskingFunction = -(barkDistance - 1.0) * (17.0 - psdFactor2) - 17.0
|
|
478
450
|
}
|
|
479
451
|
|
|
480
452
|
return maskerSpl + tonalOffset + maskingFunction
|