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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carta1",
3
- "version": "1.0.21",
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,453 +0,0 @@
1
- /**
2
- * Carta1 Audio Codec - MDCT-based Psychoacoustic Analysis
3
- *
4
- * This module implements the psychoacoustic model for ATRAC1 compression,
5
- * analyzing audio signals to determine masking thresholds for perceptual coding.
6
- */
7
-
8
- import {
9
- PSYMODEL_MIN_POWER_DB,
10
- PSYMODEL_NOT_EXAMINED,
11
- PSYMODEL_TONAL,
12
- PSYMODEL_FFT_SIZE,
13
- PSYMODEL_NON_TONAL,
14
- PSYMODEL_IRRELEVANT,
15
- PSYMODEL_LOG10_FACTOR,
16
- PSYMODEL_POW10_FACTOR,
17
- PSYMODEL_CRITICAL_BANDS,
18
- PSYMODEL_THRESHOLD_TABLE,
19
- PSYMODEL_FREQ_TO_CB_MAP,
20
- PSYMODEL_PSD_SOURCE_IDX0,
21
- PSYMODEL_PSD_INTERP_WEIGHT,
22
- PSYMODEL_PSD_SOURCE_IDX1,
23
- } from '../core/constants.js'
24
-
25
- /**
26
- * Convert linear power value to decibels
27
- * @param {number} value - Linear power value
28
- * @returns {number} Power in decibels
29
- */
30
- export function toDb(value) {
31
- return value > 0
32
- ? PSYMODEL_LOG10_FACTOR * Math.log(value)
33
- : PSYMODEL_MIN_POWER_DB
34
- }
35
-
36
- /**
37
- * Convert decibels to linear power value
38
- * @param {number} db - Power in decibels
39
- * @returns {number} Linear power value
40
- */
41
- export function fromDb(db) {
42
- return Math.exp(db * PSYMODEL_POW10_FACTOR)
43
- }
44
-
45
- /**
46
- * Perform complete psychoacoustic analysis on MDCT coefficients
47
- * @param {Float32Array} mdctCoeffs - MDCT coefficients (512 samples)
48
- * @param {number} normalizationDb - Target maximum power level in dB
49
- * @returns {Object} Analysis results including PSD, maskers, and thresholds
50
- */
51
- export function psychoAnalysis(mdctCoeffs, normalizationDb) {
52
- const fftScale = PSYMODEL_FFT_SIZE / 512
53
-
54
- // Calculate power spectral density
55
- const psd = calculatePSDFromMDCT(mdctCoeffs, normalizationDb)
56
-
57
- // Find tonal and non-tonal maskers
58
- const { flags, tonalMaskers, nonTonalMaskers } = findAllMaskers(psd, fftScale)
59
-
60
- // Calculate critical band masking thresholds
61
- const criticalBandThresholds = calculateCriticalBandThresholds(
62
- psd,
63
- tonalMaskers,
64
- nonTonalMaskers
65
- )
66
-
67
- return {
68
- psd,
69
- tonalMaskers,
70
- tonalMaskersCount: tonalMaskers.length,
71
- nonTonalMaskers,
72
- nonTonalMaskersCount: nonTonalMaskers.length,
73
- criticalBandThresholds,
74
- flags,
75
- }
76
- }
77
-
78
- /**
79
- * Calculate Power Spectral Density from MDCT coefficients
80
- * @param {Float32Array} mdctCoeffs - MDCT coefficients
81
- * @returns {Float32Array} Power spectral density in dB
82
- */
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
100
- }
101
- }
102
-
103
- const offset =
104
- normalizationDb - PSYMODEL_LOG10_FACTOR * Math.log(maxLinearPower || 1)
105
-
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
112
- }
113
- }
114
-
115
- return psd
116
- }
117
-
118
- /**
119
- * Find all maskers (tonal and non-tonal) in the spectrum
120
- * @param {Float32Array} psd - Power spectral density
121
- * @param {number} fftScale - FFT scaling factor
122
- * @returns {Object} Flags array and lists of maskers
123
- */
124
- function findAllMaskers(psd, fftScale) {
125
- // Initialize flags
126
- const flags = new Uint8Array(PSYMODEL_FFT_SIZE / 2)
127
- flags.fill(PSYMODEL_NOT_EXAMINED)
128
-
129
- // Find tonal maskers first
130
- const tonalList = findTonalMaskers(psd, flags, fftScale)
131
-
132
- // Find non-tonal maskers
133
- const nonTonalList = findNonTonalMaskers(flags, psd)
134
-
135
- // Apply decimation to remove weak maskers
136
- const decimated = decimateMaskers(flags, tonalList, nonTonalList)
137
-
138
- return {
139
- flags,
140
- tonalMaskers: decimated.tonalList,
141
- nonTonalMaskers: decimated.nonTonalList,
142
- }
143
- }
144
-
145
- /**
146
- * Detect tonal maskers (peaks in spectrum)
147
- * @param {Float32Array} psd - Power spectral density
148
- * @param {Uint8Array} flags - Component classification flags
149
- * @param {number} fftScale - FFT scaling factor
150
- * @returns {Object} Updated flags and tonal masker list
151
- */
152
- function findTonalMaskers(psd, flags, fftScale) {
153
- const tonalList = []
154
- const maxTonalBin = Math.floor(249 * fftScale)
155
-
156
- // Frequency range boundaries
157
- const RANGE_LOW = Math.floor(63 * fftScale)
158
- const RANGE_MID = Math.floor(127 * fftScale)
159
- const RANGE_HIGH = Math.floor(150 * fftScale)
160
-
161
- for (let k = 1; k < psd.length - 1 && k <= maxTonalBin; k++) {
162
- // Check for local maximum
163
- if (psd[k] <= psd[k - 1] || psd[k] < psd[k + 1]) continue
164
-
165
- // Determine search range based on frequency
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
- }
176
-
177
- // Check tonality criteria
178
- if (isTonal(psd, k, searchOffsets)) {
179
- // Calculate combined SPL
180
- const spl = toDb(fromDb(psd[k - 1]) + fromDb(psd[k]) + fromDb(psd[k + 1]))
181
- tonalList.push({ index: k, spl })
182
- markTonalComponent(flags, k, searchOffsets)
183
- }
184
- }
185
-
186
- return tonalList
187
- }
188
-
189
- /**
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
195
- */
196
- function isTonal(psd, k, offsets) {
197
- const TONAL_THRESHOLD = 7.0
198
- for (const offset of offsets) {
199
- const idx = k + offset
200
- if (idx >= 0 && idx < psd.length && psd[k] - psd[idx] < TONAL_THRESHOLD) {
201
- return false
202
- }
203
- }
204
- return true
205
- }
206
-
207
- /**
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
212
- */
213
- function markTonalComponent(flags, k, searchOffsets) {
214
- flags[k] = PSYMODEL_TONAL
215
- for (const offset of [...searchOffsets, -1, 1]) {
216
- const idx = k + offset
217
- if (idx >= 0 && idx < flags.length && idx !== k) {
218
- flags[idx] = PSYMODEL_IRRELEVANT
219
- }
220
- }
221
- }
222
-
223
- /**
224
- * Detect non-tonal maskers (noise-like components)
225
- * @param {Uint8Array} flags - Component classification flags
226
- * @param {Float32Array} psd - Power spectral density
227
- * @returns {Object} Updated flags and non-tonal masker list
228
- */
229
- function findNonTonalMaskers(flags, psd) {
230
- const nonTonalList = []
231
-
232
- for (let i = 0; i < PSYMODEL_CRITICAL_BANDS.length - 1; i++) {
233
- const cbStart = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i]][0] - 1
234
- const cbEnd =
235
- PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i + 1]][0] - 1
236
-
237
- const bandAnalysis = analyzeCriticalBand(
238
- psd,
239
- flags,
240
- cbStart,
241
- cbEnd,
242
- PSYMODEL_CRITICAL_BANDS[i]
243
- )
244
-
245
- if (bandAnalysis.powerDb > PSYMODEL_MIN_POWER_DB) {
246
- // Place masker at energy centroid
247
- let index = Math.round(cbStart + bandAnalysis.centroidOffset)
248
- index = Math.max(0, Math.min(index, flags.length - 1))
249
-
250
- // Avoid conflict with tonal maskers
251
- if (flags[index] === PSYMODEL_TONAL) {
252
- index = Math.min(index + 1, flags.length - 1)
253
- }
254
-
255
- nonTonalList.push({ index, spl: bandAnalysis.powerDb })
256
- flags[index] = PSYMODEL_NON_TONAL
257
- }
258
- }
259
-
260
- return nonTonalList
261
- }
262
-
263
- /**
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
271
- */
272
- function analyzeCriticalBand(psd, flags, startBin, endBin, bandIndex) {
273
- let totalPower = 0
274
- let weightedPower = 0
275
- const baseBark = PSYMODEL_THRESHOLD_TABLE[bandIndex][1]
276
-
277
- for (let k = startBin; k < endBin && k < flags.length; k++) {
278
- if (flags[k] === PSYMODEL_NOT_EXAMINED) {
279
- const power = fromDb(psd[k])
280
- totalPower += power
281
-
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
- }
287
-
288
- flags[k] = PSYMODEL_IRRELEVANT
289
- }
290
- }
291
-
292
- return {
293
- powerDb: toDb(totalPower),
294
- centroidOffset:
295
- totalPower > 0 ? (weightedPower / totalPower) * (endBin - startBin) : 0,
296
- }
297
- }
298
-
299
- /**
300
- * Remove weak maskers below absolute threshold
301
- * @param {Uint8Array} flags - Component classification flags
302
- * @param {Array} tonalList - List of tonal maskers
303
- * @param {Array} nonTonalList - List of non-tonal maskers
304
- * @returns {Object} Filtered masker lists
305
- */
306
- function decimateMaskers(flags, tonalList, nonTonalList) {
307
- const isAudible = (masker) => {
308
- const k = masker.index
309
- return (
310
- k < PSYMODEL_FREQ_TO_CB_MAP.length &&
311
- masker.spl >= PSYMODEL_THRESHOLD_TABLE[PSYMODEL_FREQ_TO_CB_MAP[k]][2]
312
- )
313
- }
314
-
315
- const markInaudible = (masker) => {
316
- if (masker.index < flags.length) {
317
- flags[masker.index] = PSYMODEL_IRRELEVANT
318
- }
319
- }
320
-
321
- // Filter tonal maskers
322
- const audibleTonal = []
323
- for (const tonal of tonalList) {
324
- if (isAudible(tonal)) {
325
- audibleTonal.push(tonal)
326
- } else {
327
- markInaudible(tonal)
328
- }
329
- }
330
-
331
- // Filter non-tonal maskers
332
- const audibleNonTonal = []
333
- for (const nonTonal of nonTonalList) {
334
- if (isAudible(nonTonal)) {
335
- audibleNonTonal.push(nonTonal)
336
- } else {
337
- markInaudible(nonTonal)
338
- }
339
- }
340
-
341
- return {
342
- tonalList: audibleTonal,
343
- nonTonalList: audibleNonTonal,
344
- }
345
- }
346
-
347
- /**
348
- * Calculate critical band masking thresholds
349
- * @param {Float32Array} psd - Power spectral density
350
- * @param {Array} tonalMaskers - List of tonal maskers
351
- * @param {Array} nonTonalMaskers - List of non-tonal maskers
352
- * @returns {Float32Array} Critical band thresholds
353
- */
354
- function calculateCriticalBandThresholds(psd, tonalMaskers, nonTonalMaskers) {
355
- const criticalBandThresholds = new Float32Array(
356
- PSYMODEL_CRITICAL_BANDS.length
357
- )
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
-
386
- // Calculate threshold for each critical band
387
- for (let i = 0; i < PSYMODEL_CRITICAL_BANDS.length; i++) {
388
- const bandData = PSYMODEL_THRESHOLD_TABLE[PSYMODEL_CRITICAL_BANDS[i]]
389
- const maskedBark = bandData[1]
390
- const quietThreshold = bandData[2]
391
-
392
- // Start with threshold in quiet
393
- let summedEnergy = fromDb(quietThreshold)
394
-
395
- // Add contributions from all maskers
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
-
412
- criticalBandThresholds[i] = toDb(summedEnergy)
413
- }
414
-
415
- return criticalBandThresholds
416
- }
417
-
418
- /**
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
426
- */
427
- function calculateMaskingLevel(
428
- maskerSpl,
429
- maskerPsd,
430
- maskerBark,
431
- barkDistance,
432
- isTonal
433
- ) {
434
- const tonalOffset = isTonal
435
- ? -1.525 - 0.275 * maskerBark - 4.5
436
- : -1.525 - 0.175 * maskerBark - 0.5
437
-
438
- const psdFactor1 = 0.4 * maskerPsd + 6.0
439
- const psdFactor2 = 0.15 * maskerPsd
440
-
441
- let maskingFunction
442
- if (barkDistance < -1.0) {
443
- maskingFunction = 17.0 * (barkDistance + 1.0) - psdFactor1
444
- } else if (barkDistance < 0.0) {
445
- maskingFunction = psdFactor1 * barkDistance
446
- } else if (barkDistance < 1.0) {
447
- maskingFunction = -17.0 * barkDistance
448
- } else {
449
- maskingFunction = -(barkDistance - 1.0) * (17.0 - psdFactor2) - 17.0
450
- }
451
-
452
- return maskerSpl + tonalOffset + maskingFunction
453
- }