carta1 1.1.10 → 1.3.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/bin/cli.js +8 -4
- package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
- package/codec/allocation/source.js +69 -0
- package/codec/allocation/stage.js +38 -0
- package/codec/analysis/bands.js +68 -0
- package/codec/analysis/{transient.js → blocks.js} +90 -47
- package/codec/analysis/spectrum.js +160 -0
- package/codec/analysis/stage.js +32 -0
- package/codec/{browser/interface.js → boundary/browser.js} +1 -1
- package/codec/boundary/container.js +96 -0
- package/codec/boundary/pcm.js +193 -0
- package/codec/boundary/processor.js +208 -0
- package/codec/{io/readers.js → boundary/reader.js} +2 -1
- package/codec/boundary/stream.js +361 -0
- package/codec/{browser → boundary}/worker.js +45 -42
- package/codec/core/constants.js +19 -132
- package/codec/core/geometry.js +25 -0
- package/codec/core/tables.js +215 -0
- package/codec/dequantization.js +54 -0
- package/codec/index.js +12 -13
- package/codec/pipeline/decoder.js +10 -399
- package/codec/pipeline/encoder.js +12 -434
- package/codec/quantization/spectrum.js +67 -0
- package/codec/quantization/stage.js +49 -0
- package/codec/signal/fft.js +68 -0
- package/codec/{transforms → signal}/mdct.js +37 -65
- package/codec/signal/order.js +16 -0
- package/codec/signal/qmf.js +539 -0
- package/codec/state.js +112 -0
- package/codec/{io → syntax}/bitstream.js +27 -8
- package/codec/{io/serialization.js → syntax/frame.js} +5 -101
- package/codec/synthesis/bands.js +228 -0
- package/codec/synthesis/pcm.js +64 -0
- package/codec/synthesis/stage.js +17 -0
- package/codec/utils.js +9 -52
- 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/coding/quantization.js +0 -149
- package/codec/core/buffers.js +0 -81
- package/codec/io/processor.js +0 -671
- package/codec/transforms/fft.js +0 -69
- package/codec/transforms/qmf.js +0 -105
package/bin/cli.js
CHANGED
|
@@ -19,9 +19,10 @@ import { performance } from 'perf_hooks'
|
|
|
19
19
|
import cliProgress from 'cli-progress'
|
|
20
20
|
import wav from 'wav'
|
|
21
21
|
|
|
22
|
-
import { AudioProcessor } from '../codec/
|
|
23
|
-
import { AeaReader } from '../codec/
|
|
24
|
-
import {
|
|
22
|
+
import { AudioProcessor } from '../codec/boundary/processor.js'
|
|
23
|
+
import { AeaReader } from '../codec/boundary/reader.js'
|
|
24
|
+
import { AeaFile } from '../codec/boundary/container.js'
|
|
25
|
+
import { deserializeFrame } from '../codec/syntax/frame.js'
|
|
25
26
|
import { EncoderOptions } from '../codec/core/options.js'
|
|
26
27
|
import {
|
|
27
28
|
SAMPLE_RATE,
|
|
@@ -483,6 +484,7 @@ async function encodeFile(inputFile, outputFile, options) {
|
|
|
483
484
|
const blob = await AudioProcessor.createAeaBlob(encodedFrames, {
|
|
484
485
|
title,
|
|
485
486
|
channelCount: reader.channels,
|
|
487
|
+
sampleCount: reader.totalSamples,
|
|
486
488
|
})
|
|
487
489
|
|
|
488
490
|
const buffer = await blob.arrayBuffer()
|
|
@@ -533,11 +535,13 @@ async function decodeFile(inputFile, outputFile, options) {
|
|
|
533
535
|
|
|
534
536
|
const decodedFrames = AudioProcessor.decodeStream(reader, {
|
|
535
537
|
channelCount: metadata.channelCount,
|
|
538
|
+
primingSampleCount: metadata.primingSampleCount,
|
|
539
|
+
sampleCount: metadata.sampleCount,
|
|
536
540
|
onProgress,
|
|
537
541
|
})
|
|
538
542
|
|
|
539
543
|
const pcmFrames = await AudioProcessor.collectFrames(decodedFrames)
|
|
540
|
-
const wavBlob = AudioProcessor.
|
|
544
|
+
const wavBlob = AudioProcessor.createPcmWaveBlob(
|
|
541
545
|
pcmFrames,
|
|
542
546
|
metadata.channelCount,
|
|
543
547
|
SAMPLE_RATE
|
|
@@ -27,13 +27,15 @@ import {
|
|
|
27
27
|
FRAME_OVERHEAD_BITS,
|
|
28
28
|
BITS_PER_BFU_METADATA,
|
|
29
29
|
MAX_WORD_LENGTH_INDEX,
|
|
30
|
+
} from '../core/constants.js'
|
|
31
|
+
import {
|
|
30
32
|
WORD_LENGTH_BITS,
|
|
31
33
|
INV_POWER_OF_TWO,
|
|
32
34
|
SCALE_FACTORS,
|
|
33
35
|
BFU_AMOUNTS,
|
|
34
36
|
WORD_LENGTH_DELTA_BITS,
|
|
35
37
|
DISTORTION_DELTA_FACTORS,
|
|
36
|
-
} from '../core/
|
|
38
|
+
} from '../core/tables.js'
|
|
37
39
|
|
|
38
40
|
const biasedScaleFactorCache = new Map()
|
|
39
41
|
|
|
@@ -61,84 +63,43 @@ function buildBiasedScaleFactorTable(bias) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
|
-
*
|
|
65
|
-
* This function searches through the valid BFU counts to find the one that
|
|
66
|
-
* minimizes the total perceptual distortion.
|
|
66
|
+
* Evaluate one legal active-BFU count.
|
|
67
67
|
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @param {Int32Array} bfuSizes
|
|
68
|
+
* @param {number} activeBfuCount
|
|
70
69
|
* @param {number} maxBfuCount
|
|
71
|
-
* @param {
|
|
72
|
-
* @
|
|
70
|
+
* @param {Int32Array} bfuSizes
|
|
71
|
+
* @param {number} remainingBits
|
|
72
|
+
* @param {Float64Array} biasedScaleFactors
|
|
73
|
+
* @param {Int32Array} allScaleFactorIndices
|
|
74
|
+
* @param {Float32Array} zeroBitDistortions
|
|
75
|
+
* @returns {object} Allocation result.
|
|
73
76
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
candidateBfuCount * BITS_PER_BFU_METADATA
|
|
101
|
-
|
|
102
|
-
if (availableBits < 0) continue
|
|
103
|
-
|
|
104
|
-
const rdoResult = distributeBitsRDO(
|
|
105
|
-
candidateBfuCount,
|
|
106
|
-
bfuSizes,
|
|
107
|
-
availableBits,
|
|
108
|
-
biasedScaleFactors,
|
|
109
|
-
allScaleFactorIndices
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
const totalDistortion = calculateTotalDistortion(
|
|
113
|
-
candidateBfuCount,
|
|
114
|
-
maxBfuCount,
|
|
115
|
-
bfuSizes,
|
|
116
|
-
rdoResult.wordLengths,
|
|
117
|
-
rdoResult.scaleFactorIndices,
|
|
118
|
-
biasedScaleFactors,
|
|
119
|
-
zeroBitDistortions
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
if (totalDistortion < minTotalDistortion) {
|
|
123
|
-
minTotalDistortion = totalDistortion
|
|
124
|
-
bestResult = {
|
|
125
|
-
bfuCount: candidateBfuCount,
|
|
126
|
-
allocation: rdoResult.wordLengths,
|
|
127
|
-
scaleFactorIndices: rdoResult.scaleFactorIndices,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!bestResult) {
|
|
133
|
-
const fallbackBfuCount = BFU_AMOUNTS[0]
|
|
134
|
-
return {
|
|
135
|
-
bfuCount: fallbackBfuCount,
|
|
136
|
-
allocation: new Int32Array(fallbackBfuCount),
|
|
137
|
-
scaleFactorIndices: new Int32Array(NUM_BFUS),
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return bestResult
|
|
77
|
+
function search(
|
|
78
|
+
activeBfuCount,
|
|
79
|
+
maxBfuCount,
|
|
80
|
+
bfuSizes,
|
|
81
|
+
remainingBits,
|
|
82
|
+
biasedScaleFactors,
|
|
83
|
+
allScaleFactorIndices,
|
|
84
|
+
zeroBitDistortions
|
|
85
|
+
) {
|
|
86
|
+
const allocation = distributeBits(
|
|
87
|
+
activeBfuCount,
|
|
88
|
+
bfuSizes,
|
|
89
|
+
remainingBits,
|
|
90
|
+
biasedScaleFactors,
|
|
91
|
+
allScaleFactorIndices
|
|
92
|
+
)
|
|
93
|
+
const distortion = measureDistortion(
|
|
94
|
+
activeBfuCount,
|
|
95
|
+
maxBfuCount,
|
|
96
|
+
bfuSizes,
|
|
97
|
+
allocation.wordLengths,
|
|
98
|
+
allocation.scaleFactorIndices,
|
|
99
|
+
biasedScaleFactors,
|
|
100
|
+
zeroBitDistortions
|
|
101
|
+
)
|
|
102
|
+
return { distortion, ...allocation }
|
|
142
103
|
}
|
|
143
104
|
|
|
144
105
|
/**
|
|
@@ -154,7 +115,7 @@ export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
|
154
115
|
* @param {Float32Array} zeroBitDistortions
|
|
155
116
|
* @returns {number}
|
|
156
117
|
*/
|
|
157
|
-
function
|
|
118
|
+
function measureDistortion(
|
|
158
119
|
activeBfuCount,
|
|
159
120
|
maxBfuCount,
|
|
160
121
|
bfuSizes,
|
|
@@ -198,9 +159,9 @@ function calculateTotalDistortion(
|
|
|
198
159
|
* @param {number} remainingBits
|
|
199
160
|
* @param {Float64Array} biasedScaleFactors
|
|
200
161
|
* @param {Int32Array} allScaleFactorIndices
|
|
201
|
-
* @returns {
|
|
162
|
+
* @returns {object} Retained allocation decisions.
|
|
202
163
|
*/
|
|
203
|
-
function
|
|
164
|
+
function distributeBits(
|
|
204
165
|
activeBfuCount,
|
|
205
166
|
bfuSizes,
|
|
206
167
|
remainingBits,
|
|
@@ -280,24 +241,6 @@ function distributeBitsRDO(
|
|
|
280
241
|
return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
|
|
281
242
|
}
|
|
282
243
|
|
|
283
|
-
/**
|
|
284
|
-
* Find the optimal scale factor index for a set of coefficients.
|
|
285
|
-
* Same semantics as the original.
|
|
286
|
-
* @param {Float32Array} coefficients
|
|
287
|
-
* @param {number} length
|
|
288
|
-
* @returns {number}
|
|
289
|
-
*/
|
|
290
|
-
export function findScaleFactor(coefficients, length) {
|
|
291
|
-
let maxAmplitude = 0.0
|
|
292
|
-
for (let i = 0; i < length; i++) {
|
|
293
|
-
const a = Math.abs(coefficients[i])
|
|
294
|
-
if (a > maxAmplitude) maxAmplitude = a
|
|
295
|
-
}
|
|
296
|
-
if (maxAmplitude === 0) return 0
|
|
297
|
-
const index = Math.ceil(3 * (Math.log2(maxAmplitude) + 21))
|
|
298
|
-
return Math.max(0, Math.min(63, index))
|
|
299
|
-
}
|
|
300
|
-
|
|
301
244
|
/**
|
|
302
245
|
* Restores max-heap property by sifting an element down from a given position.
|
|
303
246
|
*
|
|
@@ -339,3 +282,94 @@ function siftDown(heapIndices, heapPriorities, startIndex, heapSize) {
|
|
|
339
282
|
heapIndices[i] = idxVal
|
|
340
283
|
heapPriorities[i] = prVal
|
|
341
284
|
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Allocate bits by finding the optimal BFU count and distribution using RDO.
|
|
288
|
+
* This function searches through the valid BFU counts to find the one that
|
|
289
|
+
* minimizes the total perceptual distortion.
|
|
290
|
+
*
|
|
291
|
+
* @param {Array<Float32Array>} bfuData
|
|
292
|
+
* @param {Int32Array} bfuSizes
|
|
293
|
+
* @param {number} maxBfuCount
|
|
294
|
+
* @param {number} allocationBias
|
|
295
|
+
* @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
|
|
296
|
+
*/
|
|
297
|
+
export function solve(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
298
|
+
const allScaleFactorIndices = new Int32Array(maxBfuCount)
|
|
299
|
+
const zeroBitDistortions = new Float32Array(maxBfuCount)
|
|
300
|
+
const biasedScaleFactors = buildBiasedScaleFactorTable(allocationBias)
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < maxBfuCount; i++) {
|
|
303
|
+
const sz = bfuSizes[i] | 0
|
|
304
|
+
if (sz === 0) continue
|
|
305
|
+
|
|
306
|
+
const sfi = findScaleFactor(bfuData[i], sz)
|
|
307
|
+
allScaleFactorIndices[i] = sfi
|
|
308
|
+
if (sfi > 0) {
|
|
309
|
+
const effectiveScaleFactor = biasedScaleFactors[sfi]
|
|
310
|
+
zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let bestResult = null
|
|
315
|
+
let minTotalDistortion = Infinity
|
|
316
|
+
|
|
317
|
+
for (const candidateBfuCount of BFU_AMOUNTS) {
|
|
318
|
+
if (candidateBfuCount > maxBfuCount) continue
|
|
319
|
+
|
|
320
|
+
const availableBits =
|
|
321
|
+
FRAME_BITS -
|
|
322
|
+
FRAME_OVERHEAD_BITS -
|
|
323
|
+
candidateBfuCount * BITS_PER_BFU_METADATA
|
|
324
|
+
|
|
325
|
+
if (availableBits < 0) continue
|
|
326
|
+
|
|
327
|
+
const result = search(
|
|
328
|
+
candidateBfuCount,
|
|
329
|
+
maxBfuCount,
|
|
330
|
+
bfuSizes,
|
|
331
|
+
availableBits,
|
|
332
|
+
biasedScaleFactors,
|
|
333
|
+
allScaleFactorIndices,
|
|
334
|
+
zeroBitDistortions
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if (result.distortion < minTotalDistortion) {
|
|
338
|
+
minTotalDistortion = result.distortion
|
|
339
|
+
bestResult = {
|
|
340
|
+
bfuCount: candidateBfuCount,
|
|
341
|
+
allocation: result.wordLengths,
|
|
342
|
+
scaleFactorIndices: result.scaleFactorIndices,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!bestResult) {
|
|
348
|
+
const fallbackBfuCount = BFU_AMOUNTS[0]
|
|
349
|
+
return {
|
|
350
|
+
bfuCount: fallbackBfuCount,
|
|
351
|
+
allocation: new Int32Array(fallbackBfuCount),
|
|
352
|
+
scaleFactorIndices: new Int32Array(NUM_BFUS),
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return bestResult
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Find the optimal scale factor index for a set of coefficients.
|
|
361
|
+
* Same semantics as the original.
|
|
362
|
+
* @param {Float32Array} coefficients
|
|
363
|
+
* @param {number} length
|
|
364
|
+
* @returns {number}
|
|
365
|
+
*/
|
|
366
|
+
export function findScaleFactor(coefficients, length) {
|
|
367
|
+
let maxAmplitude = 0.0
|
|
368
|
+
for (let i = 0; i < length; i++) {
|
|
369
|
+
const a = Math.abs(coefficients[i])
|
|
370
|
+
if (a > maxAmplitude) maxAmplitude = a
|
|
371
|
+
}
|
|
372
|
+
if (maxAmplitude === 0) return 0
|
|
373
|
+
const index = Math.ceil(3 * (Math.log2(maxAmplitude) + 21))
|
|
374
|
+
return Math.max(0, Math.min(63, index))
|
|
375
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 allocation-source preparation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BFU_BAND_BOUNDARIES,
|
|
7
|
+
BFU_START_LONG,
|
|
8
|
+
BFU_START_SHORT,
|
|
9
|
+
SPECS_PER_BFU,
|
|
10
|
+
} from '../core/tables.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Group spectral coefficients into block floating units.
|
|
14
|
+
*
|
|
15
|
+
* @param {Float32Array} coefficients
|
|
16
|
+
* @param {Array<number>} blockModes
|
|
17
|
+
* @returns {object} Grouped bit-frequency units.
|
|
18
|
+
*/
|
|
19
|
+
export function groupIntoBFUs(coefficients, blockModes) {
|
|
20
|
+
const bfuData = []
|
|
21
|
+
const bfuSizes = []
|
|
22
|
+
let coeffIndex = 0
|
|
23
|
+
let bfuIndex = 0
|
|
24
|
+
|
|
25
|
+
for (let band = 0; band < 3; band++) {
|
|
26
|
+
const bandStart = coeffIndex
|
|
27
|
+
const bandSize = band === 2 ? 256 : 128
|
|
28
|
+
const bandEnd = band < 2 ? BFU_BAND_BOUNDARIES[band] : SPECS_PER_BFU.length
|
|
29
|
+
const startPositions =
|
|
30
|
+
blockModes[band] === 0 ? BFU_START_LONG : BFU_START_SHORT
|
|
31
|
+
|
|
32
|
+
while (bfuIndex < bandEnd) {
|
|
33
|
+
const size = SPECS_PER_BFU[bfuIndex]
|
|
34
|
+
const startPos = startPositions[bfuIndex] - bandStart
|
|
35
|
+
const endPos = startPos + size
|
|
36
|
+
const bfu = new Float32Array(size)
|
|
37
|
+
|
|
38
|
+
if (startPos >= 0 && endPos <= bandSize) {
|
|
39
|
+
bfu.set(coefficients.subarray(bandStart + startPos, bandStart + endPos))
|
|
40
|
+
} else if (startPos < bandSize && endPos > 0) {
|
|
41
|
+
const srcStart = Math.max(0, startPos)
|
|
42
|
+
const srcEnd = Math.min(bandSize, endPos)
|
|
43
|
+
bfu.set(
|
|
44
|
+
coefficients.subarray(bandStart + srcStart, bandStart + srcEnd),
|
|
45
|
+
Math.max(0, -startPos)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bfuData.push(bfu)
|
|
50
|
+
bfuSizes.push(size)
|
|
51
|
+
bfuIndex++
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
coeffIndex += bandSize
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { bfuData, bfuSizes, bfuCount: bfuIndex }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Prepare the spectrum as an allocation source.
|
|
62
|
+
*
|
|
63
|
+
* @param {Float32Array} coefficients
|
|
64
|
+
* @param {Array<number>} blockModes
|
|
65
|
+
* @returns {object} Allocation source.
|
|
66
|
+
*/
|
|
67
|
+
export function prepare(coefficients, blockModes) {
|
|
68
|
+
return groupIntoBFUs(coefficients, blockModes)
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 allocation stage.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { throwError } from '../utils.js'
|
|
6
|
+
import { prepare } from './source.js'
|
|
7
|
+
import { solve } from './solve.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Allocate the frame budget across block floating units.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} context
|
|
13
|
+
* @returns {Function}
|
|
14
|
+
*/
|
|
15
|
+
export function allocate(context) {
|
|
16
|
+
const options =
|
|
17
|
+
context?.options ?? throwError('allocate: options is required')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} input
|
|
21
|
+
* @returns {object}
|
|
22
|
+
*/
|
|
23
|
+
return (input) => {
|
|
24
|
+
const { coefficients, blockModes } = input
|
|
25
|
+
const { bfuData, bfuSizes, bfuCount } = prepare(coefficients, blockModes)
|
|
26
|
+
const result = solve(bfuData, bfuSizes, bfuCount, options.allocationBias)
|
|
27
|
+
const nBfu = result.bfuCount
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
bfuData,
|
|
31
|
+
bfuSizes,
|
|
32
|
+
nBfu,
|
|
33
|
+
scaleFactorIndices: result.scaleFactorIndices.slice(0, nBfu),
|
|
34
|
+
wordLengthIndices: result.allocation.slice(0, nBfu),
|
|
35
|
+
blockModes,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 QMF analysis application.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { QMF_ANALYSIS_DELAY_SAMPLES } from '../core/constants.js'
|
|
6
|
+
import { analyzeQmf } from '../signal/qmf.js'
|
|
7
|
+
import { throwError } from '../utils.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Split one sample row through a two-band configurable QMF stage.
|
|
11
|
+
*
|
|
12
|
+
* @param {Float32Array} input
|
|
13
|
+
* @param {Float32Array} delay
|
|
14
|
+
* @param {object} scratch
|
|
15
|
+
* @returns {Float32Array[]}
|
|
16
|
+
*/
|
|
17
|
+
function analyzePair(input, delay, scratch) {
|
|
18
|
+
const window =
|
|
19
|
+
scratch.analysisWindows[input.length] ??
|
|
20
|
+
throwError(`QMF analysis window for ${input.length} samples is required`)
|
|
21
|
+
window.set(delay)
|
|
22
|
+
window.set(input, delay.length)
|
|
23
|
+
|
|
24
|
+
const sampleCount = input.length / 2
|
|
25
|
+
const bands = [new Float32Array(sampleCount), new Float32Array(sampleCount)]
|
|
26
|
+
analyzeQmf(window, bands, scratch)
|
|
27
|
+
delay.set(
|
|
28
|
+
window.subarray(input.length, input.length + QMF_ANALYSIS_DELAY_SAMPLES)
|
|
29
|
+
)
|
|
30
|
+
return bands
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Split PCM into the three ATRAC1 frequency bands.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} context
|
|
37
|
+
* @returns {Function}
|
|
38
|
+
* @throws {Error} If the buffer pool is missing
|
|
39
|
+
*/
|
|
40
|
+
export function analyzeBands(context) {
|
|
41
|
+
const bufferPool =
|
|
42
|
+
context?.bufferPool ?? throwError('analyzeBands: bufferPool is required')
|
|
43
|
+
const delays = bufferPool.qmfAnalysisDelays
|
|
44
|
+
const scratch = bufferPool.qmfScratch
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Float32Array} pcmSamples
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
return (pcmSamples) => {
|
|
51
|
+
const stage1 = analyzePair(pcmSamples, delays.lowBand, scratch)
|
|
52
|
+
const stage2 = analyzePair(stage1[0], delays.midBand, scratch)
|
|
53
|
+
|
|
54
|
+
const delayedHigh = scratch.highBandWindows[stage1[1].length]
|
|
55
|
+
delayedHigh.set(bufferPool.qmfHighBandDelay)
|
|
56
|
+
delayedHigh.set(stage1[1], bufferPool.qmfHighBandDelay.length)
|
|
57
|
+
|
|
58
|
+
const highBand = delayedHigh.slice(0, stage1[1].length)
|
|
59
|
+
bufferPool.qmfHighBandDelay.set(
|
|
60
|
+
delayedHigh.subarray(
|
|
61
|
+
stage1[1].length,
|
|
62
|
+
stage1[1].length + bufferPool.qmfHighBandDelay.length
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return { bands: [stage2[0], stage2[1], highBand] }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -6,53 +6,9 @@
|
|
|
6
6
|
* characteristics that require special handling during encoding.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Perform FFT on time-domain samples and return magnitude spectrum
|
|
13
|
-
* @param {Float32Array} samples - Time-domain samples
|
|
14
|
-
* @param {number} fftSize - FFT size (must be power of 2)
|
|
15
|
-
* @returns {Float32Array} Magnitude spectrum (positive frequencies only)
|
|
16
|
-
*/
|
|
17
|
-
export function performFFT(samples, fftSize) {
|
|
18
|
-
const real = new Float32Array(fftSize)
|
|
19
|
-
const imag = new Float32Array(fftSize)
|
|
20
|
-
|
|
21
|
-
// Copy samples with zero-padding if necessary
|
|
22
|
-
const copyLen = Math.min(samples.length, fftSize)
|
|
23
|
-
real.set(samples.subarray(0, copyLen))
|
|
24
|
-
|
|
25
|
-
// Perform FFT
|
|
26
|
-
FFT.fft(real, imag)
|
|
27
|
-
|
|
28
|
-
// Calculate magnitude spectrum for positive frequencies
|
|
29
|
-
const magnitude = new Float32Array(fftSize / 2)
|
|
30
|
-
for (let i = 0; i < fftSize / 2; i++) {
|
|
31
|
-
magnitude[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i])
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return magnitude
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Detect transients by analyzing spectral changes between frames
|
|
39
|
-
* @param {Float32Array} currentCoeffs - Current frame coefficients
|
|
40
|
-
* @param {Float32Array} prevCoeffs - Previous frame coefficients
|
|
41
|
-
* @param {number} threshold - Detection threshold
|
|
42
|
-
* @returns {boolean} True if transient detected
|
|
43
|
-
*/
|
|
44
|
-
export function detectTransient(currentCoeffs, prevCoeffs, threshold) {
|
|
45
|
-
// Can't detect transients without previous frame
|
|
46
|
-
if (!prevCoeffs) return false
|
|
47
|
-
|
|
48
|
-
// Calculate all spectral features
|
|
49
|
-
const features = calculateSpectralFeatures(currentCoeffs, prevCoeffs)
|
|
50
|
-
|
|
51
|
-
// Combine features using perceptually-motivated weighting
|
|
52
|
-
const transientScore = calculateTransientScore(features)
|
|
53
|
-
|
|
54
|
-
return transientScore > threshold
|
|
55
|
-
}
|
|
9
|
+
import { FFT_SIZE_HIGH, FFT_SIZE_LOW, FFT_SIZE_MID } from '../core/constants.js'
|
|
10
|
+
import { FFT } from '../signal/fft.js'
|
|
11
|
+
import { throwError } from '../utils.js'
|
|
56
12
|
|
|
57
13
|
/**
|
|
58
14
|
* Calculate all spectral features for transient detection
|
|
@@ -224,3 +180,90 @@ function calculateTransientScore(features) {
|
|
|
224
180
|
4
|
|
225
181
|
)
|
|
226
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Perform FFT on time-domain samples and return magnitude spectrum
|
|
186
|
+
* @param {Float32Array} samples - Time-domain samples
|
|
187
|
+
* @param {number} fftSize - FFT size (must be power of 2)
|
|
188
|
+
* @returns {Float32Array} Magnitude spectrum (positive frequencies only)
|
|
189
|
+
*/
|
|
190
|
+
export function performFFT(samples, fftSize) {
|
|
191
|
+
const real = new Float32Array(fftSize)
|
|
192
|
+
const imag = new Float32Array(fftSize)
|
|
193
|
+
|
|
194
|
+
// Copy samples with zero-padding if necessary
|
|
195
|
+
const copyLen = Math.min(samples.length, fftSize)
|
|
196
|
+
real.set(samples.subarray(0, copyLen))
|
|
197
|
+
|
|
198
|
+
// Perform FFT
|
|
199
|
+
FFT.transform(real, imag)
|
|
200
|
+
|
|
201
|
+
// Calculate magnitude spectrum for positive frequencies
|
|
202
|
+
const magnitude = new Float32Array(fftSize / 2)
|
|
203
|
+
for (let i = 0; i < fftSize / 2; i++) {
|
|
204
|
+
magnitude[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return magnitude
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Detect transients by analyzing spectral changes between frames
|
|
212
|
+
* @param {Float32Array} currentCoeffs - Current frame coefficients
|
|
213
|
+
* @param {Float32Array} prevCoeffs - Previous frame coefficients
|
|
214
|
+
* @param {number} threshold - Detection threshold
|
|
215
|
+
* @returns {boolean} True if transient detected
|
|
216
|
+
*/
|
|
217
|
+
export function detectTransient(currentCoeffs, prevCoeffs, threshold) {
|
|
218
|
+
// Can't detect transients without previous frame
|
|
219
|
+
if (!prevCoeffs) return false
|
|
220
|
+
|
|
221
|
+
// Calculate all spectral features
|
|
222
|
+
const features = calculateSpectralFeatures(currentCoeffs, prevCoeffs)
|
|
223
|
+
|
|
224
|
+
// Combine features using perceptually-motivated weighting
|
|
225
|
+
const transientScore = calculateTransientScore(features)
|
|
226
|
+
|
|
227
|
+
return transientScore > threshold
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Select transform block sizes from transient analysis.
|
|
232
|
+
*
|
|
233
|
+
* @param {object} context
|
|
234
|
+
* @returns {Function}
|
|
235
|
+
* @throws {Error} If the required context is missing
|
|
236
|
+
*/
|
|
237
|
+
export function selectBlocks(context) {
|
|
238
|
+
const bufferPool =
|
|
239
|
+
context?.bufferPool ?? throwError('selectBlocks: bufferPool is required')
|
|
240
|
+
const options =
|
|
241
|
+
context?.options ?? throwError('selectBlocks: options is required')
|
|
242
|
+
const fftSizes = [FFT_SIZE_LOW, FFT_SIZE_MID, FFT_SIZE_HIGH]
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {object} input
|
|
246
|
+
* @returns {object}
|
|
247
|
+
*/
|
|
248
|
+
return (input) => {
|
|
249
|
+
const { bands } = input
|
|
250
|
+
let blockModes
|
|
251
|
+
|
|
252
|
+
if (options.fixedBlockModes) {
|
|
253
|
+
blockModes = options.fixedBlockModes
|
|
254
|
+
} else {
|
|
255
|
+
blockModes = bands.map((bandSamples, bandIndex) => {
|
|
256
|
+
const coeffs = performFFT(bandSamples, fftSizes[bandIndex])
|
|
257
|
+
const transient = detectTransient(
|
|
258
|
+
coeffs,
|
|
259
|
+
bufferPool.transientDetection[bandIndex],
|
|
260
|
+
options.transientThresholdLow
|
|
261
|
+
)
|
|
262
|
+
bufferPool.transientDetection[bandIndex] = coeffs
|
|
263
|
+
return transient * Math.max(bandIndex + 1, 2)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { bands, blockModes }
|
|
268
|
+
}
|
|
269
|
+
}
|