carta1 1.0.22 → 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.
- package/codec/browser/interface.js +0 -1
- package/codec/coding/bitallocation.js +206 -248
- package/codec/coding/quantization.js +79 -78
- package/codec/core/buffers.js +0 -18
- package/codec/core/constants.js +24 -180
- package/codec/core/options.js +0 -13
- package/codec/pipeline/decoder.js +23 -3
- package/codec/pipeline/encoder.js +37 -43
- package/dist/carta1-worker-interface.min.js.map +1 -1
- 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/codec/analysis/psychoacoustics.js +0 -631
package/package.json
CHANGED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Carta1 Audio Codec - MDCT-based Psychoacoustic Analysis (Optimized)
|
|
3
|
-
*
|
|
4
|
-
* This module implements the psychoacoustic model for ATRAC1 compression,
|
|
5
|
-
* analyzing audio signals to determine masking thresholds for perceptual coding.
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
PSYMODEL_MIN_POWER_DB,
|
|
9
|
-
PSYMODEL_NOT_EXAMINED,
|
|
10
|
-
PSYMODEL_TONAL,
|
|
11
|
-
PSYMODEL_FFT_SIZE,
|
|
12
|
-
PSYMODEL_NON_TONAL,
|
|
13
|
-
PSYMODEL_IRRELEVANT,
|
|
14
|
-
PSYMODEL_LOG10_FACTOR,
|
|
15
|
-
PSYMODEL_POW10_FACTOR,
|
|
16
|
-
PSYMODEL_CRITICAL_BANDS,
|
|
17
|
-
PSYMODEL_THRESHOLD_TABLE,
|
|
18
|
-
PSYMODEL_FREQ_TO_CB_MAP,
|
|
19
|
-
PSYMODEL_PSD_SOURCE_IDX0,
|
|
20
|
-
PSYMODEL_PSD_INTERP_WEIGHT,
|
|
21
|
-
PSYMODEL_PSD_SOURCE_IDX1,
|
|
22
|
-
PSYMODEL_TONAL_OFFSETS_LOW_FREQ,
|
|
23
|
-
PSYMODEL_TONAL_OFFSETS_MID_FREQ,
|
|
24
|
-
PSYMODEL_TONAL_OFFSETS_HIGH_FREQ,
|
|
25
|
-
PSYMODEL_HALF_FFT_SIZE,
|
|
26
|
-
PSYMODEL_NUM_BINS,
|
|
27
|
-
} from '../core/constants.js'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Convert linear power value to decibels
|
|
31
|
-
* @param {number} value - Linear power value
|
|
32
|
-
* @returns {number} Power in decibels, or minimum threshold if value <= 0
|
|
33
|
-
*/
|
|
34
|
-
export function toDb(value) {
|
|
35
|
-
return value > 0
|
|
36
|
-
? PSYMODEL_LOG10_FACTOR * Math.log(value)
|
|
37
|
-
: PSYMODEL_MIN_POWER_DB
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Convert decibels to linear power value
|
|
42
|
-
* @param {number} db - Power in decibels
|
|
43
|
-
* @returns {number} Linear power value
|
|
44
|
-
*/
|
|
45
|
-
export function fromDb(db) {
|
|
46
|
-
return Math.exp(db * PSYMODEL_POW10_FACTOR)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Perform complete psychoacoustic analysis on MDCT coefficients
|
|
51
|
-
*
|
|
52
|
-
* This function implements the ISO/IEC 11172-3 psychoacoustic model to:
|
|
53
|
-
* 1. Calculate power spectral density from MDCT coefficients
|
|
54
|
-
* 2. Detect tonal and non-tonal maskers in the frequency domain
|
|
55
|
-
* 3. Calculate critical band masking thresholds for bit allocation
|
|
56
|
-
*
|
|
57
|
-
* @param {Float32Array} mdctCoeffs - MDCT coefficients (512 samples)
|
|
58
|
-
* @param {number} normalizationDb - Target maximum power level in dB for normalization
|
|
59
|
-
* @param {Object} psychoBuffers - Psychoacoustics working buffers from buffer poool
|
|
60
|
-
* @returns {Object} Analysis results with PSD, maskers, and critical band thresholds
|
|
61
|
-
*/
|
|
62
|
-
export function psychoAnalysis(mdctCoeffs, normalizationDb, psychoBuffers) {
|
|
63
|
-
const fftScale = PSYMODEL_FFT_SIZE / 512
|
|
64
|
-
|
|
65
|
-
// Create local index arrays
|
|
66
|
-
const flags = new Int8Array(PSYMODEL_HALF_FFT_SIZE + 1)
|
|
67
|
-
const tonalIdx = new Int32Array(PSYMODEL_HALF_FFT_SIZE)
|
|
68
|
-
const nonTonalIdx = new Int32Array(PSYMODEL_CRITICAL_BANDS.length)
|
|
69
|
-
const maskIsTonal = new Int8Array(PSYMODEL_HALF_FFT_SIZE)
|
|
70
|
-
|
|
71
|
-
// Calculate PSD (dB), store raw linear power and normalization scale
|
|
72
|
-
const { offsetDb } = calculatePSDFromMDCT(
|
|
73
|
-
mdctCoeffs,
|
|
74
|
-
normalizationDb,
|
|
75
|
-
psychoBuffers
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
// Find tonal and non-tonal maskers
|
|
79
|
-
const { tonalCount, nonTonalCount } = findAllMaskers(
|
|
80
|
-
psychoBuffers,
|
|
81
|
-
flags,
|
|
82
|
-
tonalIdx,
|
|
83
|
-
nonTonalIdx,
|
|
84
|
-
offsetDb,
|
|
85
|
-
fftScale
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
// Calculate critical band masking thresholds
|
|
89
|
-
calculateCriticalBandThresholds(
|
|
90
|
-
psychoBuffers,
|
|
91
|
-
tonalIdx,
|
|
92
|
-
nonTonalIdx,
|
|
93
|
-
maskIsTonal,
|
|
94
|
-
tonalCount,
|
|
95
|
-
nonTonalCount
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
// Materialize objects only at the boundary (keep internal typed arrays)
|
|
99
|
-
const tonalMaskers = new Array(tonalCount)
|
|
100
|
-
for (let i = 0; i < tonalCount; i++) {
|
|
101
|
-
tonalMaskers[i] = { index: tonalIdx[i], spl: psychoBuffers.tonalSPL[i] }
|
|
102
|
-
}
|
|
103
|
-
const nonTonalMaskers = new Array(nonTonalCount)
|
|
104
|
-
for (let i = 0; i < nonTonalCount; i++) {
|
|
105
|
-
nonTonalMaskers[i] = {
|
|
106
|
-
index: nonTonalIdx[i],
|
|
107
|
-
spl: psychoBuffers.nonTonalSPL[i],
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
psd: psychoBuffers.psdDb,
|
|
113
|
-
tonalMaskers,
|
|
114
|
-
tonalMaskersCount: tonalCount,
|
|
115
|
-
nonTonalMaskers,
|
|
116
|
-
nonTonalMaskersCount: nonTonalCount,
|
|
117
|
-
criticalBandThresholds: psychoBuffers.criticalBandThresholds,
|
|
118
|
-
flags,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Calculate Power Spectral Density from MDCT coefficients
|
|
124
|
-
*
|
|
125
|
-
* Transforms MDCT coefficients to power spectral density using interpolation
|
|
126
|
-
* between adjacent MDCT bins to match the psychoacoustic model's FFT resolution.
|
|
127
|
-
* The output is normalized so the maximum power reaches the target level.
|
|
128
|
-
*
|
|
129
|
-
* @param {Float32Array} mdctCoeffs - Input MDCT coefficients (512 samples)
|
|
130
|
-
* @param {number} normalizationDb - Target maximum power level in dB
|
|
131
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
132
|
-
* @returns {Object} Normalization parameters {offsetDb, linScale}
|
|
133
|
-
*/
|
|
134
|
-
function calculatePSDFromMDCT(mdctCoeffs, normalizationDb, psychoBuffers) {
|
|
135
|
-
const linPower = psychoBuffers.linPower
|
|
136
|
-
|
|
137
|
-
let maxLinearPower = 0
|
|
138
|
-
for (let i = 0; i <= PSYMODEL_HALF_FFT_SIZE; i++) {
|
|
139
|
-
const a = mdctCoeffs[PSYMODEL_PSD_SOURCE_IDX0[i]]
|
|
140
|
-
const b = mdctCoeffs[PSYMODEL_PSD_SOURCE_IDX1[i]]
|
|
141
|
-
const w = PSYMODEL_PSD_INTERP_WEIGHT[i]
|
|
142
|
-
|
|
143
|
-
// Calculate interpolated power between adjacent MDCT coefficients
|
|
144
|
-
const interpolatedPower = a * a * (1 - w) + b * b * w
|
|
145
|
-
linPower[i] = interpolatedPower
|
|
146
|
-
if (interpolatedPower > maxLinearPower) maxLinearPower = interpolatedPower
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// dB offset ensures max reaches normalizationDb
|
|
150
|
-
const offsetDb =
|
|
151
|
-
normalizationDb -
|
|
152
|
-
(maxLinearPower > 0 ? PSYMODEL_LOG10_FACTOR * Math.log(maxLinearPower) : 0)
|
|
153
|
-
|
|
154
|
-
// Calculate linear scale for later fast conversions
|
|
155
|
-
const linScale = Math.exp(offsetDb * PSYMODEL_POW10_FACTOR)
|
|
156
|
-
|
|
157
|
-
// Fill PSD in dB (normalized)
|
|
158
|
-
const psdDb = psychoBuffers.psdDb
|
|
159
|
-
if (maxLinearPower > 0) {
|
|
160
|
-
for (let i = 0; i <= PSYMODEL_HALF_FFT_SIZE; i++) {
|
|
161
|
-
const p = linPower[i]
|
|
162
|
-
psdDb[i] =
|
|
163
|
-
p > 0
|
|
164
|
-
? PSYMODEL_LOG10_FACTOR * Math.log(p) + offsetDb
|
|
165
|
-
: PSYMODEL_MIN_POWER_DB
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
psdDb.fill(PSYMODEL_MIN_POWER_DB, 0, PSYMODEL_NUM_BINS)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return { offsetDb, linScale }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Orchestrate the complete masker detection process
|
|
176
|
-
*
|
|
177
|
-
* Coordinates the detection of both tonal and non-tonal maskers, then filters
|
|
178
|
-
* out maskers that fall below the absolute hearing threshold.
|
|
179
|
-
*
|
|
180
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
181
|
-
* @param {Int8Array} flags - Classification flags for each frequency bin
|
|
182
|
-
* @param {Int32Array} tonalIdx - Output array for tonal masker indices
|
|
183
|
-
* @param {Int32Array} nonTonalIdx - Output array for non-tonal masker indices
|
|
184
|
-
* @param {number} offsetDb - dB offset for power normalization
|
|
185
|
-
* @param {number} fftScale - Scaling factor for FFT bin mapping
|
|
186
|
-
* @returns {Object} Count of detected maskers after filtering
|
|
187
|
-
*/
|
|
188
|
-
function findAllMaskers(
|
|
189
|
-
psychoBuffers,
|
|
190
|
-
flags,
|
|
191
|
-
tonalIdx,
|
|
192
|
-
nonTonalIdx,
|
|
193
|
-
offsetDb,
|
|
194
|
-
fftScale
|
|
195
|
-
) {
|
|
196
|
-
flags.fill(PSYMODEL_NOT_EXAMINED)
|
|
197
|
-
|
|
198
|
-
const tonalCount = findTonalMaskers(
|
|
199
|
-
psychoBuffers,
|
|
200
|
-
flags,
|
|
201
|
-
tonalIdx,
|
|
202
|
-
offsetDb,
|
|
203
|
-
fftScale
|
|
204
|
-
)
|
|
205
|
-
const nonTonalCount = findNonTonalMaskers(
|
|
206
|
-
psychoBuffers,
|
|
207
|
-
flags,
|
|
208
|
-
nonTonalIdx,
|
|
209
|
-
offsetDb
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
const { tonal: keptTonal, nonTonal: keptNonTonal } = decimateMaskers(
|
|
213
|
-
psychoBuffers,
|
|
214
|
-
flags,
|
|
215
|
-
tonalIdx,
|
|
216
|
-
nonTonalIdx,
|
|
217
|
-
tonalCount,
|
|
218
|
-
nonTonalCount
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
tonalCount: keptTonal,
|
|
223
|
-
nonTonalCount: keptNonTonal,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Detect tonal maskers using linear power sums and a global dB offset
|
|
229
|
-
*
|
|
230
|
-
* Identifies frequency bins that represent tonal components by finding local peaks
|
|
231
|
-
* that exceed neighboring bins by a sufficient margin. Uses different neighbor
|
|
232
|
-
* patterns based on frequency range for optimal detection.
|
|
233
|
-
*
|
|
234
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
235
|
-
* @param {Int8Array} flags - Classification flags for each frequency bin
|
|
236
|
-
* @param {Int32Array} tonalIdx - Output array for tonal masker indices
|
|
237
|
-
* @param {number} offsetDb - dB offset for power normalization
|
|
238
|
-
* @param {number} fftScale - Scaling factor for FFT bin mapping
|
|
239
|
-
* @returns {number} Number of tonal maskers detected
|
|
240
|
-
*/
|
|
241
|
-
function findTonalMaskers(psychoBuffers, flags, tonalIdx, offsetDb, fftScale) {
|
|
242
|
-
const psdDb = psychoBuffers.psdDb
|
|
243
|
-
const linPower = psychoBuffers.linPower
|
|
244
|
-
|
|
245
|
-
let count = 0
|
|
246
|
-
const maxTonalBin = Math.floor(249 * fftScale)
|
|
247
|
-
|
|
248
|
-
const RANGE_LOW = Math.floor(63 * fftScale)
|
|
249
|
-
const RANGE_MID = Math.floor(127 * fftScale)
|
|
250
|
-
const RANGE_HIGH = Math.floor(150 * fftScale)
|
|
251
|
-
|
|
252
|
-
for (
|
|
253
|
-
let binIndex = 1, len = psdDb.length - 1;
|
|
254
|
-
binIndex < len && binIndex <= maxTonalBin;
|
|
255
|
-
binIndex++
|
|
256
|
-
) {
|
|
257
|
-
// Check for local peak in power spectral density
|
|
258
|
-
const binPower = psdDb[binIndex]
|
|
259
|
-
if (binPower <= psdDb[binIndex - 1] || binPower < psdDb[binIndex + 1])
|
|
260
|
-
continue
|
|
261
|
-
|
|
262
|
-
// Select neighbor pattern based on frequency range
|
|
263
|
-
let neighborOffsets = null
|
|
264
|
-
if (binIndex > 2 && binIndex < RANGE_LOW)
|
|
265
|
-
neighborOffsets = PSYMODEL_TONAL_OFFSETS_LOW_FREQ
|
|
266
|
-
else if (binIndex >= RANGE_LOW && binIndex < RANGE_MID)
|
|
267
|
-
neighborOffsets = PSYMODEL_TONAL_OFFSETS_MID_FREQ
|
|
268
|
-
else if (binIndex >= RANGE_MID && binIndex < RANGE_HIGH)
|
|
269
|
-
neighborOffsets = PSYMODEL_TONAL_OFFSETS_HIGH_FREQ
|
|
270
|
-
else continue
|
|
271
|
-
|
|
272
|
-
if (isTonalWithOffsets(psdDb, binIndex, neighborOffsets)) {
|
|
273
|
-
// Calculate SPL using adjacent bins: offsetDb + 10*log10(sum of linear powers)
|
|
274
|
-
const adjacentPowerSum =
|
|
275
|
-
linPower[binIndex - 1] + linPower[binIndex] + linPower[binIndex + 1]
|
|
276
|
-
const soundPressureLevel =
|
|
277
|
-
adjacentPowerSum > 0
|
|
278
|
-
? offsetDb + PSYMODEL_LOG10_FACTOR * Math.log(adjacentPowerSum)
|
|
279
|
-
: PSYMODEL_MIN_POWER_DB
|
|
280
|
-
|
|
281
|
-
tonalIdx[count] = binIndex
|
|
282
|
-
psychoBuffers.tonalSPL[count] = soundPressureLevel
|
|
283
|
-
count++
|
|
284
|
-
|
|
285
|
-
// Mark this bin as tonal and mark neighbors as irrelevant
|
|
286
|
-
flags[binIndex] = PSYMODEL_TONAL
|
|
287
|
-
|
|
288
|
-
// Mark pattern-specific neighbors as irrelevant
|
|
289
|
-
for (let i = 0; i < neighborOffsets.length; i++) {
|
|
290
|
-
const neighborIdx = binIndex + neighborOffsets[i]
|
|
291
|
-
if (
|
|
292
|
-
neighborIdx >= 0 &&
|
|
293
|
-
neighborIdx < flags.length &&
|
|
294
|
-
neighborIdx !== binIndex
|
|
295
|
-
) {
|
|
296
|
-
flags[neighborIdx] = PSYMODEL_IRRELEVANT
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Mark immediate neighbors as irrelevant
|
|
301
|
-
if (binIndex - 1 >= 0) flags[binIndex - 1] = PSYMODEL_IRRELEVANT
|
|
302
|
-
if (binIndex + 1 < flags.length) flags[binIndex + 1] = PSYMODEL_IRRELEVANT
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return count
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Check if a frequency bin qualifies as tonal based on neighbor comparison
|
|
311
|
-
*
|
|
312
|
-
* A bin is considered tonal if it exceeds all specified neighbor bins
|
|
313
|
-
* by at least the tonal threshold (7 dB).
|
|
314
|
-
*
|
|
315
|
-
* @param {Float32Array} psdDb - Power spectral density in dB
|
|
316
|
-
* @param {number} binIndex - Index of the bin to test
|
|
317
|
-
* @param {Int8Array} offsets - Neighbor offset pattern to check
|
|
318
|
-
* @returns {boolean} True if the bin qualifies as tonal
|
|
319
|
-
*/
|
|
320
|
-
function isTonalWithOffsets(psdDb, binIndex, offsets) {
|
|
321
|
-
const TONAL_THRESHOLD = 7.0
|
|
322
|
-
const binPower = psdDb[binIndex]
|
|
323
|
-
const arrayLength = psdDb.length
|
|
324
|
-
|
|
325
|
-
for (let i = 0; i < offsets.length; i++) {
|
|
326
|
-
const neighborIndex = binIndex + offsets[i]
|
|
327
|
-
if (
|
|
328
|
-
neighborIndex >= 0 &&
|
|
329
|
-
neighborIndex < arrayLength &&
|
|
330
|
-
binPower - psdDb[neighborIndex] < TONAL_THRESHOLD
|
|
331
|
-
) {
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return true
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Detect non-tonal maskers per critical band using linear power aggregation
|
|
340
|
-
*
|
|
341
|
-
* Groups unexamined frequency bins by critical band and calculates the energy
|
|
342
|
-
* centroid for each band. Creates non-tonal maskers at the centroid positions
|
|
343
|
-
* to represent the noise-like spectral content.
|
|
344
|
-
*
|
|
345
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
346
|
-
* @param {Int8Array} flags - Classification flags for each frequency bin
|
|
347
|
-
* @param {Int32Array} nonTonalIdx - Output array for non-tonal masker indices
|
|
348
|
-
* @param {number} offsetDb - dB offset for power normalization
|
|
349
|
-
* @returns {number} Number of non-tonal maskers detected
|
|
350
|
-
*/
|
|
351
|
-
function findNonTonalMaskers(psychoBuffers, flags, nonTonalIdx, offsetDb) {
|
|
352
|
-
const linPower = psychoBuffers.linPower
|
|
353
|
-
|
|
354
|
-
let count = 0
|
|
355
|
-
const cbList = PSYMODEL_CRITICAL_BANDS
|
|
356
|
-
const mapLen = PSYMODEL_FREQ_TO_CB_MAP.length
|
|
357
|
-
const flagsLen = flags.length
|
|
358
|
-
|
|
359
|
-
for (let bandIndex = 0; bandIndex < cbList.length - 1; bandIndex++) {
|
|
360
|
-
const currentBandIdx = cbList[bandIndex]
|
|
361
|
-
const bandStartBin = PSYMODEL_THRESHOLD_TABLE[currentBandIdx][0] - 1
|
|
362
|
-
const bandEndBin = PSYMODEL_THRESHOLD_TABLE[cbList[bandIndex + 1]][0] - 1
|
|
363
|
-
|
|
364
|
-
// Analyze critical band in linear power domain
|
|
365
|
-
let totalLinearPower = 0
|
|
366
|
-
let weightedLinearPower = 0
|
|
367
|
-
const baseBarkValue = PSYMODEL_THRESHOLD_TABLE[currentBandIdx][1]
|
|
368
|
-
|
|
369
|
-
const endBin = Math.min(bandEndBin, flagsLen)
|
|
370
|
-
for (let binIndex = bandStartBin; binIndex < endBin; binIndex++) {
|
|
371
|
-
if (flags[binIndex] === PSYMODEL_NOT_EXAMINED) {
|
|
372
|
-
const binPower = linPower[binIndex]
|
|
373
|
-
totalLinearPower += binPower
|
|
374
|
-
if (binIndex < mapLen) {
|
|
375
|
-
const barkValue =
|
|
376
|
-
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[binIndex]][1]
|
|
377
|
-
weightedLinearPower += binPower * (barkValue - baseBarkValue)
|
|
378
|
-
}
|
|
379
|
-
flags[binIndex] = PSYMODEL_IRRELEVANT
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (totalLinearPower > 0) {
|
|
384
|
-
// Calculate energy centroid offset within the critical band
|
|
385
|
-
const centroidOffset =
|
|
386
|
-
(weightedLinearPower / totalLinearPower) * (bandEndBin - bandStartBin)
|
|
387
|
-
|
|
388
|
-
// Convert total power to dB: offsetDb + 10*log10(totalLinearPower)
|
|
389
|
-
const maskerPowerDb =
|
|
390
|
-
offsetDb + PSYMODEL_LOG10_FACTOR * Math.log(totalLinearPower)
|
|
391
|
-
|
|
392
|
-
// Place non-tonal masker near energy centroid (avoid tonal slots)
|
|
393
|
-
let maskerIndex = Math.round(bandStartBin + centroidOffset)
|
|
394
|
-
maskerIndex =
|
|
395
|
-
maskerIndex < 0
|
|
396
|
-
? 0
|
|
397
|
-
: maskerIndex >= flagsLen
|
|
398
|
-
? flagsLen - 1
|
|
399
|
-
: maskerIndex
|
|
400
|
-
if (flags[maskerIndex] === PSYMODEL_TONAL && maskerIndex + 1 < flagsLen)
|
|
401
|
-
maskerIndex++
|
|
402
|
-
|
|
403
|
-
nonTonalIdx[count] = maskerIndex
|
|
404
|
-
psychoBuffers.nonTonalSPL[count] = maskerPowerDb
|
|
405
|
-
flags[maskerIndex] = PSYMODEL_NON_TONAL
|
|
406
|
-
count++
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return count
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Remove weak maskers below absolute hearing threshold
|
|
415
|
-
*
|
|
416
|
-
* Filters out maskers that fall below the absolute threshold of hearing
|
|
417
|
-
* using the psychoacoustic model's threshold table. Compacts the arrays
|
|
418
|
-
* in-place to maintain efficiency.
|
|
419
|
-
*
|
|
420
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
421
|
-
* @param {Int8Array} flags - Classification flags for each frequency bin
|
|
422
|
-
* @param {Int32Array} tonalIdx - Array of tonal masker indices
|
|
423
|
-
* @param {Int32Array} nonTonalIdx - Array of non-tonal masker indices
|
|
424
|
-
* @param {number} tonalCount - Number of detected tonal maskers
|
|
425
|
-
* @param {number} nonTonalCount - Number of detected non-tonal maskers
|
|
426
|
-
* @returns {Object} Counts of audible maskers after filtering
|
|
427
|
-
*/
|
|
428
|
-
function decimateMaskers(
|
|
429
|
-
psychoBuffers,
|
|
430
|
-
flags,
|
|
431
|
-
tonalIdx,
|
|
432
|
-
nonTonalIdx,
|
|
433
|
-
tonalCount,
|
|
434
|
-
nonTonalCount
|
|
435
|
-
) {
|
|
436
|
-
const isAudible = (idx, spl) => {
|
|
437
|
-
if (idx >= PSYMODEL_FREQ_TO_CB_MAP.length) return false
|
|
438
|
-
const cb = PSYMODEL_FREQ_TO_CB_MAP[idx]
|
|
439
|
-
return spl >= PSYMODEL_THRESHOLD_TABLE[cb][2]
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let keptTonal = 0
|
|
443
|
-
for (let i = 0; i < tonalCount; i++) {
|
|
444
|
-
const idx = tonalIdx[i]
|
|
445
|
-
const spl = psychoBuffers.tonalSPL[i]
|
|
446
|
-
if (isAudible(idx, spl)) {
|
|
447
|
-
// compact in-place
|
|
448
|
-
if (keptTonal !== i) {
|
|
449
|
-
tonalIdx[keptTonal] = idx
|
|
450
|
-
psychoBuffers.tonalSPL[keptTonal] = spl
|
|
451
|
-
}
|
|
452
|
-
keptTonal++
|
|
453
|
-
} else if (idx < flags.length) {
|
|
454
|
-
flags[idx] = PSYMODEL_IRRELEVANT
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
let keptNonTonal = 0
|
|
459
|
-
for (let i = 0; i < nonTonalCount; i++) {
|
|
460
|
-
const idx = nonTonalIdx[i]
|
|
461
|
-
const spl = psychoBuffers.nonTonalSPL[i]
|
|
462
|
-
if (isAudible(idx, spl)) {
|
|
463
|
-
if (keptNonTonal !== i) {
|
|
464
|
-
nonTonalIdx[keptNonTonal] = idx
|
|
465
|
-
psychoBuffers.nonTonalSPL[keptNonTonal] = spl
|
|
466
|
-
}
|
|
467
|
-
keptNonTonal++
|
|
468
|
-
} else if (idx < flags.length) {
|
|
469
|
-
flags[idx] = PSYMODEL_IRRELEVANT
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return { tonal: keptTonal, nonTonal: keptNonTonal }
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Precompute masker features used in threshold summation
|
|
478
|
-
*
|
|
479
|
-
* Collects all audible maskers and precomputes their bark scale values,
|
|
480
|
-
* tonal offsets, and masking function coefficients to optimize the
|
|
481
|
-
* critical band threshold calculation.
|
|
482
|
-
*
|
|
483
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
484
|
-
* @param {Int32Array} tonalIdx - Array of tonal masker indices
|
|
485
|
-
* @param {Int32Array} nonTonalIdx - Array of non-tonal masker indices
|
|
486
|
-
* @param {Int8Array} maskIsTonal - Array indicating masker type (1=tonal, 0=non-tonal)
|
|
487
|
-
* @param {number} tonalCount - Number of audible tonal maskers
|
|
488
|
-
* @param {number} nonTonalCount - Number of audible non-tonal maskers
|
|
489
|
-
* @returns {number} Total number of maskers collected
|
|
490
|
-
*/
|
|
491
|
-
function collectMaskers(
|
|
492
|
-
psychoBuffers,
|
|
493
|
-
tonalIdx,
|
|
494
|
-
nonTonalIdx,
|
|
495
|
-
maskIsTonal,
|
|
496
|
-
tonalCount,
|
|
497
|
-
nonTonalCount
|
|
498
|
-
) {
|
|
499
|
-
const psd = psychoBuffers.psdDb
|
|
500
|
-
const maskSpl = psychoBuffers.maskSpl
|
|
501
|
-
const maskPsd = psychoBuffers.maskPsd
|
|
502
|
-
const maskBark = psychoBuffers.maskBark
|
|
503
|
-
const maskTonalOffset = psychoBuffers.maskTonalOffset
|
|
504
|
-
const maskPsdF1 = psychoBuffers.maskPsdF1
|
|
505
|
-
const maskPsdF2 = psychoBuffers.maskPsdF2
|
|
506
|
-
|
|
507
|
-
let maskerIndex = 0
|
|
508
|
-
|
|
509
|
-
// Process tonal maskers
|
|
510
|
-
for (let i = 0; i < tonalCount; i++) {
|
|
511
|
-
const binIndex = tonalIdx[i]
|
|
512
|
-
if (binIndex < PSYMODEL_FREQ_TO_CB_MAP.length) {
|
|
513
|
-
const barkValue =
|
|
514
|
-
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[binIndex]][1]
|
|
515
|
-
const soundPressureLevel = psychoBuffers.tonalSPL[i]
|
|
516
|
-
const psdValue = psd[binIndex]
|
|
517
|
-
|
|
518
|
-
maskSpl[maskerIndex] = soundPressureLevel
|
|
519
|
-
maskPsd[maskerIndex] = psdValue
|
|
520
|
-
maskBark[maskerIndex] = barkValue
|
|
521
|
-
maskIsTonal[maskerIndex] = 1
|
|
522
|
-
maskTonalOffset[maskerIndex] = -1.525 - 0.275 * barkValue - 4.5 // Tonal offset formula
|
|
523
|
-
maskPsdF1[maskerIndex] = 0.4 * psdValue + 6.0
|
|
524
|
-
maskPsdF2[maskerIndex] = 0.15 * psdValue
|
|
525
|
-
maskerIndex++
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Process non-tonal maskers
|
|
530
|
-
for (let i = 0; i < nonTonalCount; i++) {
|
|
531
|
-
const binIndex = nonTonalIdx[i]
|
|
532
|
-
if (binIndex < PSYMODEL_FREQ_TO_CB_MAP.length) {
|
|
533
|
-
const barkValue =
|
|
534
|
-
PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[binIndex]][1]
|
|
535
|
-
const soundPressureLevel = psychoBuffers.nonTonalSPL[i]
|
|
536
|
-
const psdValue = psd[binIndex]
|
|
537
|
-
|
|
538
|
-
maskSpl[maskerIndex] = soundPressureLevel
|
|
539
|
-
maskPsd[maskerIndex] = psdValue
|
|
540
|
-
maskBark[maskerIndex] = barkValue
|
|
541
|
-
maskIsTonal[maskerIndex] = 0
|
|
542
|
-
maskTonalOffset[maskerIndex] = -1.525 - 0.175 * barkValue - 0.5 // Non-tonal offset formula
|
|
543
|
-
maskPsdF1[maskerIndex] = 0.4 * psdValue + 6.0
|
|
544
|
-
maskPsdF2[maskerIndex] = 0.15 * psdValue
|
|
545
|
-
maskerIndex++
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return maskerIndex
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Calculate critical band masking thresholds using precomputed masker features
|
|
554
|
-
*
|
|
555
|
-
* Computes the masking threshold for each critical band by summing the
|
|
556
|
-
* contributions from all maskers, taking into account the masking function
|
|
557
|
-
* shape and bark scale distances.
|
|
558
|
-
*
|
|
559
|
-
* @param {Object} psychoBuffers - Pre-allocated working buffers
|
|
560
|
-
* @param {Int32Array} tonalIdx - Array of tonal masker indices
|
|
561
|
-
* @param {Int32Array} nonTonalIdx - Array of non-tonal masker indices
|
|
562
|
-
* @param {Int8Array} maskIsTonal - Array indicating masker type
|
|
563
|
-
* @param {number} tonalCount - Number of audible tonal maskers
|
|
564
|
-
* @param {number} nonTonalCount - Number of audible non-tonal maskers
|
|
565
|
-
*/
|
|
566
|
-
function calculateCriticalBandThresholds(
|
|
567
|
-
psychoBuffers,
|
|
568
|
-
tonalIdx,
|
|
569
|
-
nonTonalIdx,
|
|
570
|
-
maskIsTonal,
|
|
571
|
-
tonalCount,
|
|
572
|
-
nonTonalCount
|
|
573
|
-
) {
|
|
574
|
-
const cbList = PSYMODEL_CRITICAL_BANDS
|
|
575
|
-
const thresholds = psychoBuffers.criticalBandThresholds
|
|
576
|
-
|
|
577
|
-
const maskersCount = collectMaskers(
|
|
578
|
-
psychoBuffers,
|
|
579
|
-
tonalIdx,
|
|
580
|
-
nonTonalIdx,
|
|
581
|
-
maskIsTonal,
|
|
582
|
-
tonalCount,
|
|
583
|
-
nonTonalCount
|
|
584
|
-
)
|
|
585
|
-
const maskSpl = psychoBuffers.maskSpl
|
|
586
|
-
const maskBark = psychoBuffers.maskBark
|
|
587
|
-
const maskTonalOffset = psychoBuffers.maskTonalOffset
|
|
588
|
-
const maskPsdF1 = psychoBuffers.maskPsdF1
|
|
589
|
-
const maskPsdF2 = psychoBuffers.maskPsdF2
|
|
590
|
-
|
|
591
|
-
// Calculate masking threshold for each critical band
|
|
592
|
-
for (let bandIndex = 0; bandIndex < cbList.length; bandIndex++) {
|
|
593
|
-
const bandData = PSYMODEL_THRESHOLD_TABLE[cbList[bandIndex]]
|
|
594
|
-
const maskedBarkValue = bandData[1]
|
|
595
|
-
const absoluteThreshold = bandData[2]
|
|
596
|
-
|
|
597
|
-
// Start with absolute threshold of hearing (quiet threshold)
|
|
598
|
-
let totalLinearEnergy = fromDb(absoluteThreshold)
|
|
599
|
-
|
|
600
|
-
// Sum contributions from all maskers
|
|
601
|
-
for (let maskerIdx = 0; maskerIdx < maskersCount; maskerIdx++) {
|
|
602
|
-
const barkDistance = maskedBarkValue - maskBark[maskerIdx]
|
|
603
|
-
|
|
604
|
-
// Check if masker is within effective masking range
|
|
605
|
-
if (barkDistance >= -3 && barkDistance < 8) {
|
|
606
|
-
// Calculate piecewise masking function using precomputed terms
|
|
607
|
-
const f1 = maskPsdF1[maskerIdx]
|
|
608
|
-
const f2 = maskPsdF2[maskerIdx]
|
|
609
|
-
let maskingFunction
|
|
610
|
-
|
|
611
|
-
if (barkDistance < -1.0) {
|
|
612
|
-
maskingFunction = 17.0 * (barkDistance + 1.0) - f1
|
|
613
|
-
} else if (barkDistance < 0.0) {
|
|
614
|
-
maskingFunction = f1 * barkDistance
|
|
615
|
-
} else if (barkDistance < 1.0) {
|
|
616
|
-
maskingFunction = -17.0 * barkDistance
|
|
617
|
-
} else {
|
|
618
|
-
maskingFunction = -(barkDistance - 1.0) * (17.0 - f2) - 17.0
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Calculate final masked threshold and add to total
|
|
622
|
-
const maskedThresholdDb =
|
|
623
|
-
maskSpl[maskerIdx] + maskTonalOffset[maskerIdx] + maskingFunction
|
|
624
|
-
totalLinearEnergy += fromDb(maskedThresholdDb)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Convert back to dB and store final threshold
|
|
629
|
-
thresholds[bandIndex] = toDb(totalLinearEnergy)
|
|
630
|
-
}
|
|
631
|
-
}
|