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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carta1",
3
- "version": "1.0.22",
3
+ "version": "1.1.0",
4
4
  "description": "An ATRAC1 audio codec implementation in JS",
5
5
  "type": "module",
6
6
  "main": "codec/index.js",
@@ -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
- }