carta1 1.0.12 → 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 +138 -274
- package/codec/coding/bitallocation.js +145 -179
- package/codec/core/buffers.js +0 -7
- package/codec/core/constants.js +51 -0
- package/codec/pipeline/encoder.js +1 -7
- 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
|
@@ -9,14 +9,17 @@ import {
|
|
|
9
9
|
PSYMODEL_MIN_POWER_DB,
|
|
10
10
|
PSYMODEL_NOT_EXAMINED,
|
|
11
11
|
PSYMODEL_TONAL,
|
|
12
|
+
PSYMODEL_FFT_SIZE,
|
|
12
13
|
PSYMODEL_NON_TONAL,
|
|
13
14
|
PSYMODEL_IRRELEVANT,
|
|
14
15
|
PSYMODEL_LOG10_FACTOR,
|
|
15
16
|
PSYMODEL_POW10_FACTOR,
|
|
16
17
|
PSYMODEL_CRITICAL_BANDS,
|
|
17
|
-
PSYMODEL_CB_FREQ_INDICES,
|
|
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
|
/**
|
|
@@ -42,134 +45,70 @@ export function fromDb(db) {
|
|
|
42
45
|
/**
|
|
43
46
|
* Perform complete psychoacoustic analysis on MDCT coefficients
|
|
44
47
|
* @param {Float32Array} mdctCoeffs - MDCT coefficients (512 samples)
|
|
45
|
-
* @param {number}
|
|
46
|
-
* @param {Object} psychoBuffers - Reusable buffers for performance
|
|
48
|
+
* @param {number} normalizationDb - Target maximum power level in dB
|
|
47
49
|
* @returns {Object} Analysis results including PSD, maskers, and thresholds
|
|
48
50
|
*/
|
|
49
|
-
export function psychoAnalysis(
|
|
50
|
-
|
|
51
|
-
targetSize,
|
|
52
|
-
psychoBuffers,
|
|
53
|
-
normalizationDb
|
|
54
|
-
) {
|
|
55
|
-
const fftScale = targetSize / 512
|
|
56
|
-
|
|
57
|
-
// Apply frequency band reversal correction
|
|
58
|
-
const correctedCoeffs = applyMdctCorrection(mdctCoeffs)
|
|
51
|
+
export function psychoAnalysis(mdctCoeffs, normalizationDb) {
|
|
52
|
+
const fftScale = PSYMODEL_FFT_SIZE / 512
|
|
59
53
|
|
|
60
54
|
// Calculate power spectral density
|
|
61
|
-
const psd = calculatePSDFromMDCT(
|
|
55
|
+
const psd = calculatePSDFromMDCT(mdctCoeffs, normalizationDb)
|
|
62
56
|
|
|
63
57
|
// Find tonal and non-tonal maskers
|
|
64
|
-
const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(
|
|
65
|
-
psd,
|
|
66
|
-
psychoBuffers,
|
|
67
|
-
fftScale
|
|
68
|
-
)
|
|
58
|
+
const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(psd, fftScale)
|
|
69
59
|
|
|
70
|
-
// Calculate
|
|
71
|
-
const
|
|
60
|
+
// Calculate critical band masking thresholds
|
|
61
|
+
const criticalBandThresholds = calculateCriticalBandThresholds(
|
|
72
62
|
psd,
|
|
73
63
|
tonalMaskers,
|
|
74
|
-
nonTonalMaskers
|
|
75
|
-
psychoBuffers.threshold,
|
|
76
|
-
targetSize
|
|
64
|
+
nonTonalMaskers
|
|
77
65
|
)
|
|
78
66
|
|
|
79
67
|
return {
|
|
80
68
|
psd,
|
|
81
69
|
tonalMaskers,
|
|
70
|
+
tonalMaskersCount: tonalMaskers.length,
|
|
82
71
|
nonTonalMaskers,
|
|
83
|
-
|
|
72
|
+
nonTonalMaskersCount: nonTonalMaskers.length,
|
|
73
|
+
criticalBandThresholds,
|
|
84
74
|
flags,
|
|
85
75
|
}
|
|
86
76
|
}
|
|
87
77
|
|
|
88
|
-
/**
|
|
89
|
-
* Apply frequency band reversal correction to MDCT coefficients
|
|
90
|
-
* @param {Float32Array} mdctCoeffs - Original MDCT coefficients
|
|
91
|
-
* @returns {Float32Array} Corrected coefficients
|
|
92
|
-
*/
|
|
93
|
-
function applyMdctCorrection(mdctCoeffs) {
|
|
94
|
-
const corrected = new Float32Array(512)
|
|
95
|
-
|
|
96
|
-
// Low band: direct copy
|
|
97
|
-
corrected.set(mdctCoeffs.subarray(0, 128), 0)
|
|
98
|
-
|
|
99
|
-
// Mid band: reversed
|
|
100
|
-
for (let i = 0; i < 128; i++) {
|
|
101
|
-
corrected[128 + i] = mdctCoeffs[255 - i]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// High band: reversed
|
|
105
|
-
for (let i = 0; i < 256; i++) {
|
|
106
|
-
corrected[256 + i] = mdctCoeffs[511 - i]
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return corrected
|
|
110
|
-
}
|
|
111
|
-
|
|
112
78
|
/**
|
|
113
79
|
* Calculate Power Spectral Density from MDCT coefficients
|
|
114
80
|
* @param {Float32Array} mdctCoeffs - MDCT coefficients
|
|
115
|
-
* @param {number} targetSize - Target FFT size
|
|
116
81
|
* @returns {Float32Array} Power spectral density in dB
|
|
117
82
|
*/
|
|
118
|
-
function calculatePSDFromMDCT(mdctCoeffs,
|
|
119
|
-
const
|
|
120
|
-
const psd = new Float32Array(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (targetSize === 512) {
|
|
136
|
-
// Direct averaging for same size
|
|
137
|
-
for (let i = 0; i < 256; i++) {
|
|
138
|
-
const avgPower = (powers[i * 2] + powers[i * 2 + 1]) * 0.5 * normFactor
|
|
139
|
-
psd[i] = avgPower > 0 ? toDb(avgPower) : PSYMODEL_MIN_POWER_DB
|
|
140
|
-
maxPsdValue = Math.max(maxPsdValue, psd[i])
|
|
141
|
-
}
|
|
142
|
-
psd[256] = psd[255]
|
|
143
|
-
// Check if last value is the max
|
|
144
|
-
maxPsdValue = Math.max(maxPsdValue, psd[256])
|
|
145
|
-
} else {
|
|
146
|
-
// Interpolate for different sizes
|
|
147
|
-
const scaleFactor = 512 / targetSize
|
|
148
|
-
|
|
149
|
-
for (let i = 0; i <= halfTargetSize; i++) {
|
|
150
|
-
const srcIdx = i * scaleFactor
|
|
151
|
-
const srcIdxInt = Math.floor(srcIdx)
|
|
152
|
-
const frac = srcIdx - srcIdxInt
|
|
153
|
-
|
|
154
|
-
const power =
|
|
155
|
-
srcIdxInt < 511
|
|
156
|
-
? powers[srcIdxInt] * (1 - frac) + powers[srcIdxInt + 1] * frac
|
|
157
|
-
: powers[511]
|
|
158
|
-
|
|
159
|
-
const normalizedPower = power * normFactor
|
|
160
|
-
psd[i] =
|
|
161
|
-
normalizedPower > 0 ? toDb(normalizedPower) : PSYMODEL_MIN_POWER_DB
|
|
162
|
-
|
|
163
|
-
maxPsdValue = Math.max(maxPsdValue, psd[i])
|
|
83
|
+
function calculatePSDFromMDCT(mdctCoeffs, normalizationDb) {
|
|
84
|
+
const half = PSYMODEL_FFT_SIZE >>> 1
|
|
85
|
+
const psd = new Float32Array(half + 1)
|
|
86
|
+
const linearPower = new Float32Array(half + 1)
|
|
87
|
+
|
|
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]]
|
|
92
|
+
|
|
93
|
+
const power =
|
|
94
|
+
a * a * (1 - PSYMODEL_PSD_INTERP_WEIGHT[i]) +
|
|
95
|
+
b * b * PSYMODEL_PSD_INTERP_WEIGHT[i]
|
|
96
|
+
linearPower[i] = power
|
|
97
|
+
|
|
98
|
+
if (power > maxLinearPower) {
|
|
99
|
+
maxLinearPower = power
|
|
164
100
|
}
|
|
165
101
|
}
|
|
166
102
|
|
|
167
|
-
|
|
168
|
-
|
|
103
|
+
const offset =
|
|
104
|
+
normalizationDb - PSYMODEL_LOG10_FACTOR * Math.log(maxLinearPower || 1)
|
|
169
105
|
|
|
170
|
-
for (let i = 0; i
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
173
112
|
}
|
|
174
113
|
}
|
|
175
114
|
|
|
@@ -179,20 +118,19 @@ function calculatePSDFromMDCT(mdctCoeffs, targetSize, normalizationDb) {
|
|
|
179
118
|
/**
|
|
180
119
|
* Find all maskers (tonal and non-tonal) in the spectrum
|
|
181
120
|
* @param {Float32Array} psd - Power spectral density
|
|
182
|
-
* @param {Object} psychoBuffers - Reusable buffers
|
|
183
121
|
* @param {number} fftScale - FFT scaling factor
|
|
184
122
|
* @returns {Object} Flags array and lists of maskers
|
|
185
123
|
*/
|
|
186
|
-
function findAllMaskers(psd,
|
|
124
|
+
function findAllMaskers(psd, fftScale) {
|
|
187
125
|
// Initialize flags
|
|
188
|
-
const flags =
|
|
126
|
+
const flags = new Uint8Array(PSYMODEL_FFT_SIZE / 2)
|
|
189
127
|
flags.fill(PSYMODEL_NOT_EXAMINED)
|
|
190
128
|
|
|
191
129
|
// Find tonal maskers first
|
|
192
|
-
|
|
130
|
+
const tonalList = findTonalMaskers(psd, flags, fftScale)
|
|
193
131
|
|
|
194
132
|
// Find non-tonal maskers
|
|
195
|
-
|
|
133
|
+
const nonTonalList = findNonTonalMaskers(flags, psd)
|
|
196
134
|
|
|
197
135
|
// Apply decimation to remove weak maskers
|
|
198
136
|
const decimated = decimateMaskers(flags, tonalList, nonTonalList)
|
|
@@ -214,68 +152,67 @@ function findAllMaskers(psd, psychoBuffers, fftScale) {
|
|
|
214
152
|
function findTonalMaskers(psd, flags, fftScale) {
|
|
215
153
|
const tonalList = []
|
|
216
154
|
const maxTonalBin = Math.floor(249 * fftScale)
|
|
217
|
-
const fftSize = (psd.length - 1) * 2
|
|
218
155
|
|
|
219
156
|
// Frequency range boundaries
|
|
220
157
|
const RANGE_LOW = Math.floor(63 * fftScale)
|
|
221
158
|
const RANGE_MID = Math.floor(127 * fftScale)
|
|
222
159
|
const RANGE_HIGH = Math.floor(150 * fftScale)
|
|
223
160
|
|
|
224
|
-
for (let k = 1; k <
|
|
161
|
+
for (let k = 1; k < psd.length - 1 && k <= maxTonalBin; k++) {
|
|
225
162
|
// Check for local maximum
|
|
226
163
|
if (psd[k] <= psd[k - 1] || psd[k] < psd[k + 1]) continue
|
|
227
164
|
|
|
228
165
|
// Determine search range based on frequency
|
|
229
|
-
|
|
230
|
-
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
|
+
}
|
|
231
176
|
|
|
232
177
|
// Check tonality criteria
|
|
233
178
|
if (isTonal(psd, k, searchOffsets)) {
|
|
234
179
|
// Calculate combined SPL
|
|
235
180
|
const spl = toDb(fromDb(psd[k - 1]) + fromDb(psd[k]) + fromDb(psd[k + 1]))
|
|
236
|
-
|
|
237
181
|
tonalList.push({ index: k, spl })
|
|
238
182
|
markTonalComponent(flags, k, searchOffsets)
|
|
239
183
|
}
|
|
240
184
|
}
|
|
241
185
|
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Get search offsets for tonality check based on frequency
|
|
247
|
-
*/
|
|
248
|
-
function getSearchOffsets(k, rangeLow, rangeMid, rangeHigh) {
|
|
249
|
-
if (k > 2 && k < rangeLow) return [-2, 2]
|
|
250
|
-
if (k >= rangeLow && k < rangeMid) return [-3, -2, 2, 3]
|
|
251
|
-
if (k >= rangeMid && k < rangeHigh) return [-6, -5, -4, -3, -2, 2, 3, 4, 5, 6]
|
|
252
|
-
return null
|
|
186
|
+
return tonalList
|
|
253
187
|
}
|
|
254
188
|
|
|
255
189
|
/**
|
|
256
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
|
|
257
195
|
*/
|
|
258
196
|
function isTonal(psd, k, offsets) {
|
|
259
|
-
const TONAL_THRESHOLD = 7
|
|
260
|
-
|
|
197
|
+
const TONAL_THRESHOLD = 7.0
|
|
261
198
|
for (const offset of offsets) {
|
|
262
199
|
const idx = k + offset
|
|
263
|
-
if (idx >= 0 && idx < psd.length) {
|
|
264
|
-
|
|
200
|
+
if (idx >= 0 && idx < psd.length && psd[k] - psd[idx] < TONAL_THRESHOLD) {
|
|
201
|
+
return false
|
|
265
202
|
}
|
|
266
203
|
}
|
|
267
|
-
|
|
268
204
|
return true
|
|
269
205
|
}
|
|
270
206
|
|
|
271
207
|
/**
|
|
272
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
|
|
273
212
|
*/
|
|
274
213
|
function markTonalComponent(flags, k, searchOffsets) {
|
|
275
214
|
flags[k] = PSYMODEL_TONAL
|
|
276
|
-
|
|
277
|
-
const allOffsets = [...searchOffsets, -1, 1]
|
|
278
|
-
for (const offset of allOffsets) {
|
|
215
|
+
for (const offset of [...searchOffsets, -1, 1]) {
|
|
279
216
|
const idx = k + offset
|
|
280
217
|
if (idx >= 0 && idx < flags.length && idx !== k) {
|
|
281
218
|
flags[idx] = PSYMODEL_IRRELEVANT
|
|
@@ -297,7 +234,6 @@ function findNonTonalMaskers(flags, psd) {
|
|
|
297
234
|
const cbEnd =
|
|
298
235
|
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i + 1]][0] - 1
|
|
299
236
|
|
|
300
|
-
// Calculate band energy and centroid
|
|
301
237
|
const bandAnalysis = analyzeCriticalBand(
|
|
302
238
|
psd,
|
|
303
239
|
flags,
|
|
@@ -321,11 +257,17 @@ function findNonTonalMaskers(flags, psd) {
|
|
|
321
257
|
}
|
|
322
258
|
}
|
|
323
259
|
|
|
324
|
-
return
|
|
260
|
+
return nonTonalList
|
|
325
261
|
}
|
|
326
262
|
|
|
327
263
|
/**
|
|
328
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
|
|
329
271
|
*/
|
|
330
272
|
function analyzeCriticalBand(psd, flags, startBin, endBin, bandIndex) {
|
|
331
273
|
let totalPower = 0
|
|
@@ -337,9 +279,11 @@ function analyzeCriticalBand(psd, flags, startBin, endBin, bandIndex) {
|
|
|
337
279
|
const power = fromDb(psd[k])
|
|
338
280
|
totalPower += power
|
|
339
281
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
287
|
|
|
344
288
|
flags[k] = PSYMODEL_IRRELEVANT
|
|
345
289
|
}
|
|
@@ -401,52 +345,45 @@ function decimateMaskers(flags, tonalList, nonTonalList) {
|
|
|
401
345
|
}
|
|
402
346
|
|
|
403
347
|
/**
|
|
404
|
-
* Calculate
|
|
348
|
+
* Calculate critical band masking thresholds
|
|
405
349
|
* @param {Float32Array} psd - Power spectral density
|
|
406
350
|
* @param {Array} tonalMaskers - List of tonal maskers
|
|
407
351
|
* @param {Array} nonTonalMaskers - List of non-tonal maskers
|
|
408
|
-
* @
|
|
409
|
-
* @param {number} fftSize - FFT size
|
|
410
|
-
* @returns {Float32Array} Global masking threshold
|
|
352
|
+
* @returns {Float32Array} Critical band thresholds
|
|
411
353
|
*/
|
|
412
|
-
function
|
|
413
|
-
psd,
|
|
414
|
-
tonalMaskers,
|
|
415
|
-
nonTonalMaskers,
|
|
416
|
-
thresholdBuffer,
|
|
417
|
-
fftSize
|
|
418
|
-
) {
|
|
354
|
+
function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
|
|
419
355
|
const criticalBandThresholds = new Float32Array(
|
|
420
356
|
PSYMODEL_CRITICAL_BANDS.length
|
|
421
357
|
)
|
|
422
358
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
psd,
|
|
426
|
-
tonalMaskers,
|
|
427
|
-
nonTonalMaskers,
|
|
428
|
-
criticalBandThresholds
|
|
429
|
-
)
|
|
359
|
+
// Pre-collect all maskers with their properties
|
|
360
|
+
const maskers = []
|
|
430
361
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
+
}
|
|
437
373
|
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
}
|
|
440
385
|
|
|
441
|
-
|
|
442
|
-
* Calculate masking threshold for each critical band
|
|
443
|
-
*/
|
|
444
|
-
function calculateCriticalBandThresholds(
|
|
445
|
-
psd,
|
|
446
|
-
tonalMaskers,
|
|
447
|
-
nonTonalMaskers,
|
|
448
|
-
output
|
|
449
|
-
) {
|
|
386
|
+
// Calculate threshold for each critical band
|
|
450
387
|
for (let i = 0; i < PSYMODEL_CRITICAL_BANDS.length; i++) {
|
|
451
388
|
const bandData = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i]]
|
|
452
389
|
const maskedBark = bandData[1]
|
|
@@ -456,57 +393,36 @@ function calculateCriticalBandThresholds(
|
|
|
456
393
|
let summedEnergy = fromDb(quietThreshold)
|
|
457
394
|
|
|
458
395
|
// Add contributions from all maskers
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Calculate energy contributions from a set of maskers
|
|
479
|
-
*/
|
|
480
|
-
function calculateMaskerContributions(psd, maskers, maskedBark, isTonal) {
|
|
481
|
-
let totalEnergy = 0
|
|
482
|
-
|
|
483
|
-
for (const masker of maskers) {
|
|
484
|
-
const j = masker.index
|
|
485
|
-
if (j >= PSYMODEL_FREQ_TO_CB_MAP.length) continue
|
|
486
|
-
|
|
487
|
-
const maskerBark = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[j]][1]
|
|
488
|
-
const barkDistance = maskedBark - maskerBark
|
|
489
|
-
|
|
490
|
-
// Check if within masking range
|
|
491
|
-
if (barkDistance < -3 || barkDistance >= 8) continue
|
|
492
|
-
|
|
493
|
-
// Calculate masking level
|
|
494
|
-
const maskedDb = calculateMaskingLevel(
|
|
495
|
-
masker.spl,
|
|
496
|
-
psd[j],
|
|
497
|
-
maskerBark,
|
|
498
|
-
barkDistance,
|
|
499
|
-
isTonal
|
|
500
|
-
)
|
|
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
|
+
}
|
|
501
411
|
|
|
502
|
-
|
|
412
|
+
criticalBandThresholds[i] = toDb(summedEnergy)
|
|
503
413
|
}
|
|
504
414
|
|
|
505
|
-
return
|
|
415
|
+
return criticalBandThresholds
|
|
506
416
|
}
|
|
507
417
|
|
|
508
418
|
/**
|
|
509
|
-
* Calculate
|
|
419
|
+
* Calculate energy contributions from a set of maskers
|
|
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
|
|
510
426
|
*/
|
|
511
427
|
function calculateMaskingLevel(
|
|
512
428
|
maskerSpl,
|
|
@@ -515,75 +431,23 @@ function calculateMaskingLevel(
|
|
|
515
431
|
barkDistance,
|
|
516
432
|
isTonal
|
|
517
433
|
) {
|
|
518
|
-
// Masking index
|
|
519
434
|
const tonalOffset = isTonal
|
|
520
435
|
? -1.525 - 0.275 * maskerBark - 4.5
|
|
521
436
|
: -1.525 - 0.175 * maskerBark - 0.5
|
|
522
437
|
|
|
523
|
-
|
|
524
|
-
const psdFactor1 = 0.4 * maskerPsd + 6
|
|
438
|
+
const psdFactor1 = 0.4 * maskerPsd + 6.0
|
|
525
439
|
const psdFactor2 = 0.15 * maskerPsd
|
|
526
440
|
|
|
527
|
-
// Calculate masking function
|
|
528
441
|
let maskingFunction
|
|
529
|
-
if (barkDistance < -1) {
|
|
530
|
-
maskingFunction = 17 * (barkDistance + 1) - psdFactor1
|
|
531
|
-
} else if (barkDistance < 0) {
|
|
442
|
+
if (barkDistance < -1.0) {
|
|
443
|
+
maskingFunction = 17.0 * (barkDistance + 1.0) - psdFactor1
|
|
444
|
+
} else if (barkDistance < 0.0) {
|
|
532
445
|
maskingFunction = psdFactor1 * barkDistance
|
|
533
|
-
} else if (barkDistance < 1) {
|
|
534
|
-
maskingFunction = -17 * barkDistance
|
|
446
|
+
} else if (barkDistance < 1.0) {
|
|
447
|
+
maskingFunction = -17.0 * barkDistance
|
|
535
448
|
} else {
|
|
536
|
-
maskingFunction = -(barkDistance - 1) * (17 - psdFactor2) - 17
|
|
449
|
+
maskingFunction = -(barkDistance - 1.0) * (17.0 - psdFactor2) - 17.0
|
|
537
450
|
}
|
|
538
451
|
|
|
539
452
|
return maskerSpl + tonalOffset + maskingFunction
|
|
540
453
|
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Interpolate critical band thresholds to full spectrum
|
|
544
|
-
* @param {Float32Array} cbThresholds - Critical band thresholds
|
|
545
|
-
* @param {Float32Array} output - Output buffer
|
|
546
|
-
* @param {number} fftSize - FFT size
|
|
547
|
-
*/
|
|
548
|
-
function interpolateThresholdCompensated(cbThresholds, output, fftSize) {
|
|
549
|
-
const COMPENSATION_FACTOR = 0.25
|
|
550
|
-
const halfSize = fftSize / 2
|
|
551
|
-
|
|
552
|
-
// Get frequency indices for critical bands
|
|
553
|
-
const freqIndices = PSYMODEL_CB_FREQ_INDICES
|
|
554
|
-
|
|
555
|
-
let bandIndex = 0
|
|
556
|
-
for (let i = 0; i <= halfSize; i++) {
|
|
557
|
-
if (i <= freqIndices[0]) {
|
|
558
|
-
// Before first critical band
|
|
559
|
-
output[i] = cbThresholds[0]
|
|
560
|
-
} else if (i >= freqIndices[freqIndices.length - 1]) {
|
|
561
|
-
// After last critical band
|
|
562
|
-
output[i] = cbThresholds[cbThresholds.length - 1]
|
|
563
|
-
} else {
|
|
564
|
-
// Find current band
|
|
565
|
-
while (
|
|
566
|
-
bandIndex < freqIndices.length - 1 &&
|
|
567
|
-
freqIndices[bandIndex + 1] < i
|
|
568
|
-
) {
|
|
569
|
-
bandIndex++
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Interpolate with compensation
|
|
573
|
-
const x0 = freqIndices[bandIndex]
|
|
574
|
-
const x1 = freqIndices[bandIndex + 1]
|
|
575
|
-
const y0 = cbThresholds[bandIndex]
|
|
576
|
-
const y1 = cbThresholds[bandIndex + 1]
|
|
577
|
-
const bandWidth = x1 - x0
|
|
578
|
-
|
|
579
|
-
if (bandWidth > 0) {
|
|
580
|
-
const t = (i - x0) / bandWidth
|
|
581
|
-
const interpolated = y0 + (y1 - y0) * t
|
|
582
|
-
const compensation = Math.abs(y1 - y0) * COMPENSATION_FACTOR
|
|
583
|
-
output[i] = interpolated + compensation
|
|
584
|
-
} else {
|
|
585
|
-
output[i] = y0
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|