carta1 1.0.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/LICENSE +15 -0
- package/README.md +129 -0
- package/bin/cli.js +679 -0
- package/codec/analysis/psychoacoustics.js +589 -0
- package/codec/analysis/transient.js +226 -0
- package/codec/browser/interface.js +188 -0
- package/codec/browser/worker.js +140 -0
- package/codec/coding/bitallocation.js +407 -0
- package/codec/coding/quantization.js +150 -0
- package/codec/core/bitstream.js +82 -0
- package/codec/core/buffers.js +99 -0
- package/codec/core/constants.js +321 -0
- package/codec/core/options.js +151 -0
- package/codec/index.js +27 -0
- package/codec/io/processor.js +566 -0
- package/codec/io/readers.js +56 -0
- package/codec/io/serialization.js +254 -0
- package/codec/pipeline/decoder.js +278 -0
- package/codec/pipeline/encoder.js +369 -0
- package/codec/transforms/fft.js +69 -0
- package/codec/transforms/mdct.js +225 -0
- package/codec/transforms/qmf.js +127 -0
- package/codec/utils.js +151 -0
- package/dist/carta1-worker-interface.min.js +2 -0
- package/dist/carta1-worker-interface.min.js.map +1 -0
- package/dist/carta1-worker.min.js +2 -0
- package/dist/carta1-worker.min.js.map +1 -0
- package/dist/carta1.min.js +2 -0
- package/dist/carta1.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Encoding Pipeline
|
|
3
|
+
*
|
|
4
|
+
* The encoding pipeline transforms PCM audio samples into ATRAC1 compressed format
|
|
5
|
+
* through a series of processing stages:
|
|
6
|
+
*
|
|
7
|
+
* 1. QMF Analysis: Splits audio into frequency bands using quadrature mirror filters
|
|
8
|
+
* 2. Block Selection: Detects transients to choose appropriate transform block sizes
|
|
9
|
+
* 3. MDCT Transform: Applies Modified Discrete Cosine Transform to frequency bands
|
|
10
|
+
* 4. Quantization: Performs psychoacoustic analysis, bit allocation, and quantization
|
|
11
|
+
*
|
|
12
|
+
* Each stage is implemented as a functional pipeline component that can be composed
|
|
13
|
+
* using the pipe utility. The pipeline maintains state through a shared context
|
|
14
|
+
* containing buffer pools and encoding options.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { pipe, throwError } from '../utils.js'
|
|
18
|
+
import { qmfAnalysis } from '../transforms/qmf.js'
|
|
19
|
+
import { mdct64, mdct256, mdct512 } from '../transforms/mdct.js'
|
|
20
|
+
import {
|
|
21
|
+
groupIntoBFUs,
|
|
22
|
+
findScaleFactor,
|
|
23
|
+
quantize,
|
|
24
|
+
} from '../coding/quantization.js'
|
|
25
|
+
import { allocateBits } from '../coding/bitallocation.js'
|
|
26
|
+
import { BufferPool } from '../core/buffers.js'
|
|
27
|
+
import { EncoderOptions } from '../core/options.js'
|
|
28
|
+
import {
|
|
29
|
+
WORD_LENGTH_BITS,
|
|
30
|
+
WINDOW_SHORT,
|
|
31
|
+
FFT_SIZE_LOW,
|
|
32
|
+
FFT_SIZE_MID,
|
|
33
|
+
FFT_SIZE_HIGH,
|
|
34
|
+
PSYMODEL_FFT_SIZE,
|
|
35
|
+
MDCT_SIZE_SHORT,
|
|
36
|
+
MDCT_SIZE_MID,
|
|
37
|
+
MDCT_SIZE_LONG,
|
|
38
|
+
} from '../core/constants.js'
|
|
39
|
+
import { performFFT, detectTransient } from '../analysis/transient.js'
|
|
40
|
+
import { psychoAnalysis } from '../analysis/psychoacoustics.js'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* QMF Analysis Stage - Splits audio into frequency bands
|
|
44
|
+
*
|
|
45
|
+
* Performs two-stage QMF analysis to decompose the input signal into three bands:
|
|
46
|
+
* - Low band (0-5.5kHz): processed at full resolution
|
|
47
|
+
* - Mid band (5.5-11kHz): processed at half resolution
|
|
48
|
+
* - High band (11-22kHz): processed at quarter resolution with delay compensation
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} context - Pipeline context containing bufferPool
|
|
51
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
52
|
+
* @returns {Function} Stage function that processes PCM samples
|
|
53
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
54
|
+
*/
|
|
55
|
+
function qmfAnalysisStage(context) {
|
|
56
|
+
const bufferPool =
|
|
57
|
+
context?.bufferPool ??
|
|
58
|
+
throwError('qmfAnalysisStage: bufferPool is required')
|
|
59
|
+
const delays = bufferPool.qmfDelays
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Process PCM samples through QMF analysis
|
|
63
|
+
* @param {Float32Array} pcmSamples - Input PCM samples
|
|
64
|
+
* @returns {Object} Analysis result containing frequency bands
|
|
65
|
+
* @returns {Array<Float32Array>} returns.bands - Three frequency bands [low, mid, high]
|
|
66
|
+
*/
|
|
67
|
+
return (pcmSamples) => {
|
|
68
|
+
const stage1 = qmfAnalysis(
|
|
69
|
+
pcmSamples,
|
|
70
|
+
delays.lowBand,
|
|
71
|
+
bufferPool.qmfWorkBuffers
|
|
72
|
+
)
|
|
73
|
+
delays.lowBand = stage1.newDelay
|
|
74
|
+
|
|
75
|
+
const stage2 = qmfAnalysis(
|
|
76
|
+
stage1.lowBand,
|
|
77
|
+
delays.midBand,
|
|
78
|
+
bufferPool.qmfWorkBuffers
|
|
79
|
+
)
|
|
80
|
+
delays.midBand = stage2.newDelay
|
|
81
|
+
|
|
82
|
+
const delayedHigh =
|
|
83
|
+
bufferPool.qmfWorkBuffers.highBandDelay[stage1.highBand.length]
|
|
84
|
+
delayedHigh.set(delays.highBand)
|
|
85
|
+
delayedHigh.set(stage1.highBand, delays.highBand.length)
|
|
86
|
+
|
|
87
|
+
const highBand = delayedHigh.slice(0, stage1.highBand.length)
|
|
88
|
+
delays.highBand = delayedHigh.slice(stage1.highBand.length)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
bands: [stage2.lowBand, stage2.highBand, highBand],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Block Selection Stage - Detects transients to choose transform block sizes
|
|
98
|
+
*
|
|
99
|
+
* Analyzes frequency bands using FFT and transient detection to determine
|
|
100
|
+
* appropriate MDCT block sizes. Short blocks are used for transient signals
|
|
101
|
+
* to prevent pre-echo artifacts, while long blocks provide better frequency
|
|
102
|
+
* resolution for steady-state signals.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} context - Pipeline context containing bufferPool
|
|
105
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
106
|
+
* @returns {Function} Stage function that processes QMF analysis results
|
|
107
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
108
|
+
*/
|
|
109
|
+
function blockSelectorStage(context) {
|
|
110
|
+
const bufferPool =
|
|
111
|
+
context?.bufferPool ??
|
|
112
|
+
throwError('blockSelectorStage: bufferPool is required')
|
|
113
|
+
const options =
|
|
114
|
+
context?.options ?? throwError('blockSelectorStage: options is required')
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Analyze frequency bands and determine block modes
|
|
118
|
+
* @param {Object} input - QMF analysis results
|
|
119
|
+
* @param {Array<Float32Array>} input.bands - Three frequency bands [low, mid, high]
|
|
120
|
+
* @returns {Object} Block selection results
|
|
121
|
+
* @returns {Array<Float32Array>} returns.bands - Original frequency bands
|
|
122
|
+
* @returns {Array<number>} returns.blockModes - Block mode indices for each band
|
|
123
|
+
*/
|
|
124
|
+
return (input) => {
|
|
125
|
+
const { bands } = input
|
|
126
|
+
const [lowBand, midBand, highBand] = bands
|
|
127
|
+
|
|
128
|
+
const lowCoeffs = performFFT(lowBand, FFT_SIZE_LOW)
|
|
129
|
+
const midCoeffs = performFFT(midBand, FFT_SIZE_MID)
|
|
130
|
+
const highCoeffs = performFFT(highBand, FFT_SIZE_HIGH)
|
|
131
|
+
|
|
132
|
+
const lowTransient = detectTransient(
|
|
133
|
+
lowCoeffs,
|
|
134
|
+
bufferPool.transientDetection.prevLowCoeffs,
|
|
135
|
+
options.transientThresholdLow
|
|
136
|
+
)
|
|
137
|
+
const midTransient = detectTransient(
|
|
138
|
+
midCoeffs,
|
|
139
|
+
bufferPool.transientDetection.prevMidCoeffs,
|
|
140
|
+
options.transientThresholdMid
|
|
141
|
+
)
|
|
142
|
+
const highTransient = detectTransient(
|
|
143
|
+
highCoeffs,
|
|
144
|
+
bufferPool.transientDetection.prevHighCoeffs,
|
|
145
|
+
options.transientThresholdHigh
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
bufferPool.transientDetection.prevLowCoeffs = lowCoeffs
|
|
149
|
+
bufferPool.transientDetection.prevMidCoeffs = midCoeffs
|
|
150
|
+
bufferPool.transientDetection.prevHighCoeffs = highCoeffs
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
bands,
|
|
154
|
+
blockModes: [
|
|
155
|
+
lowTransient ? 2 : 0,
|
|
156
|
+
midTransient ? 2 : 0,
|
|
157
|
+
highTransient ? 3 : 0,
|
|
158
|
+
],
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* MDCT Transform Stage - Applies Modified Discrete Cosine Transform
|
|
165
|
+
*
|
|
166
|
+
* Transforms time-domain samples to frequency-domain coefficients using MDCT.
|
|
167
|
+
* The transform size depends on the block mode determined by transient detection:
|
|
168
|
+
* - Long blocks: 512/256 samples for better frequency resolution
|
|
169
|
+
* - Short blocks: 64 samples for better time resolution
|
|
170
|
+
*
|
|
171
|
+
* Applies windowing and overlap-add processing to prevent blocking artifacts.
|
|
172
|
+
* Mid and high bands are spectrally reversed to match ATRAC1 format.
|
|
173
|
+
*
|
|
174
|
+
* @param {Object} context - Pipeline context containing bufferPool
|
|
175
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
176
|
+
* @returns {Function} Stage function that processes block selection results
|
|
177
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
178
|
+
*/
|
|
179
|
+
function mdctStage(context) {
|
|
180
|
+
const bufferPool =
|
|
181
|
+
context?.bufferPool ?? throwError('mdctStage: bufferPool is required')
|
|
182
|
+
const overlapBuffers = bufferPool.mdctOverlap
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Transform frequency bands using MDCT
|
|
186
|
+
* @param {Object} input - Block selection results
|
|
187
|
+
* @param {Array<Float32Array>} input.bands - Three frequency bands
|
|
188
|
+
* @param {Array<number>} input.blockModes - Block mode indices for each band
|
|
189
|
+
* @param {any} input.originalFrame - Original frame data (passed through)
|
|
190
|
+
* @returns {Object} MDCT transform results
|
|
191
|
+
* @returns {Array<Float32Array>} returns.bands - Original frequency bands
|
|
192
|
+
* @returns {Float32Array} returns.coefficients - MDCT coefficients (512 samples)
|
|
193
|
+
* @returns {Array<number>} returns.blockModes - Block mode indices
|
|
194
|
+
* @returns {any} returns.originalFrame - Original frame data
|
|
195
|
+
*/
|
|
196
|
+
return (input) => {
|
|
197
|
+
const { bands, blockModes, originalFrame } = input
|
|
198
|
+
|
|
199
|
+
const coefficients = new Float32Array(512)
|
|
200
|
+
|
|
201
|
+
for (let band = 0; band < 3; band++) {
|
|
202
|
+
const samples = bands[band]
|
|
203
|
+
const bandSize = band === 2 ? 256 : 128
|
|
204
|
+
const numBlocks = 1 << blockModes[band]
|
|
205
|
+
const blockSize = numBlocks === 1 ? bandSize : 32
|
|
206
|
+
|
|
207
|
+
const mdctFunc =
|
|
208
|
+
numBlocks === 1 ? (band === 2 ? mdct512 : mdct256) : mdct64
|
|
209
|
+
const mdctSize =
|
|
210
|
+
numBlocks === 1
|
|
211
|
+
? band === 2
|
|
212
|
+
? MDCT_SIZE_LONG
|
|
213
|
+
: MDCT_SIZE_MID
|
|
214
|
+
: MDCT_SIZE_SHORT
|
|
215
|
+
const windowStart = numBlocks === 1 ? (band === 2 ? 112 : 48) : 0
|
|
216
|
+
|
|
217
|
+
const offset = band === 0 ? 0 : band === 1 ? 128 : 256
|
|
218
|
+
|
|
219
|
+
for (let block = 0; block < numBlocks; block++) {
|
|
220
|
+
const blockStart = block * blockSize
|
|
221
|
+
const mdctInput = bufferPool.transformBuffers[mdctSize]
|
|
222
|
+
mdctInput.fill(0)
|
|
223
|
+
|
|
224
|
+
mdctInput.set(overlapBuffers[band], windowStart)
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < 32; i++) {
|
|
227
|
+
const tailValue = samples[blockStart + blockSize - 32 + i]
|
|
228
|
+
overlapBuffers[band][i] = WINDOW_SHORT[i] * tailValue
|
|
229
|
+
samples[blockStart + blockSize - 32 + i] =
|
|
230
|
+
tailValue * WINDOW_SHORT[31 - i]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
mdctInput.set(
|
|
234
|
+
samples.subarray(blockStart, blockStart + blockSize),
|
|
235
|
+
windowStart + 32
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
let spectrum = mdctFunc.transform(mdctInput, bufferPool.mdctBuffers)
|
|
239
|
+
|
|
240
|
+
if (band > 0) {
|
|
241
|
+
const reversed = bufferPool.reversalBuffers[spectrum.length]
|
|
242
|
+
reversed.fill(0)
|
|
243
|
+
for (let i = 0; i < spectrum.length; i++) {
|
|
244
|
+
reversed[i] = spectrum[spectrum.length - 1 - i]
|
|
245
|
+
}
|
|
246
|
+
spectrum = reversed
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
coefficients.set(spectrum, offset + blockStart)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { bands, coefficients, blockModes, originalFrame }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Quantization Stage - Performs psychoacoustic analysis and quantization
|
|
259
|
+
*
|
|
260
|
+
* The final encoding stage that:
|
|
261
|
+
* 1. Groups coefficients into Block Floating Units (BFUs)
|
|
262
|
+
* 2. Performs psychoacoustic analysis to determine masking thresholds
|
|
263
|
+
* 3. Allocates bits based on perceptual importance
|
|
264
|
+
* 4. Quantizes coefficients using scale factors and word lengths
|
|
265
|
+
*
|
|
266
|
+
* Uses a perceptual model to ensure quantization noise remains below
|
|
267
|
+
* the masking threshold, maximizing quality within the bit budget.
|
|
268
|
+
*
|
|
269
|
+
* @param {Object} context - Pipeline context containing bufferPool and options
|
|
270
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
271
|
+
* @param {EncoderOptions} context.options - Encoding configuration options
|
|
272
|
+
* @returns {Function} Stage function that processes MDCT results
|
|
273
|
+
* @throws {Error} If bufferPool or options are not provided in context
|
|
274
|
+
*/
|
|
275
|
+
function quantizationStage(context) {
|
|
276
|
+
const bufferPool =
|
|
277
|
+
context?.bufferPool ??
|
|
278
|
+
throwError('quantizationStage: bufferPool is required')
|
|
279
|
+
const options =
|
|
280
|
+
context?.options ?? throwError('quantizationStage: options is required')
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Perform psychoacoustic analysis and quantization
|
|
284
|
+
* @param {Object} input - MDCT transform results
|
|
285
|
+
* @param {Float32Array} input.coefficients - MDCT coefficients
|
|
286
|
+
* @param {Array<number>} input.blockModes - Block mode indices
|
|
287
|
+
* @returns {Object} Quantization results ready for bitstream encoding
|
|
288
|
+
* @returns {number} returns.nBfu - Number of active BFUs
|
|
289
|
+
* @returns {Int32Array} returns.scaleFactorIndices - Scale factor indices for each BFU
|
|
290
|
+
* @returns {Int32Array} returns.wordLengthIndices - Word length indices for each BFU
|
|
291
|
+
* @returns {Array<Int32Array>} returns.quantizedCoefficients - Quantized coefficient data
|
|
292
|
+
* @returns {Array<number>} returns.blockSizeMode - Block size mode for each band
|
|
293
|
+
*/
|
|
294
|
+
return (input) => {
|
|
295
|
+
const { coefficients, blockModes } = input
|
|
296
|
+
|
|
297
|
+
const { bfuData, bfuSizes, bfuCount } = groupIntoBFUs(
|
|
298
|
+
coefficients,
|
|
299
|
+
blockModes,
|
|
300
|
+
bufferPool.bfuData
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
const psychoResults = psychoAnalysis(
|
|
304
|
+
coefficients,
|
|
305
|
+
PSYMODEL_FFT_SIZE,
|
|
306
|
+
bufferPool.psychoAnalysis,
|
|
307
|
+
options.normalizationDb
|
|
308
|
+
)
|
|
309
|
+
const { bfuCount: selectedBfuCount, allocation } = allocateBits(
|
|
310
|
+
psychoResults,
|
|
311
|
+
bfuData,
|
|
312
|
+
bfuSizes,
|
|
313
|
+
bfuCount
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const scaleFactorIndices = new Int32Array(selectedBfuCount)
|
|
317
|
+
const quantizedCoefficients = []
|
|
318
|
+
|
|
319
|
+
for (let bfu = 0; bfu < selectedBfuCount; bfu++) {
|
|
320
|
+
const data = bfuData[bfu].subarray(0, bfuSizes[bfu])
|
|
321
|
+
const wordLength = allocation[bfu]
|
|
322
|
+
const bitsPerSample = WORD_LENGTH_BITS[wordLength]
|
|
323
|
+
|
|
324
|
+
scaleFactorIndices[bfu] = findScaleFactor(data)
|
|
325
|
+
const quantized = quantize(data, scaleFactorIndices[bfu], bitsPerSample)
|
|
326
|
+
quantizedCoefficients.push(quantized)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
nBfu: selectedBfuCount,
|
|
331
|
+
scaleFactorIndices,
|
|
332
|
+
wordLengthIndices: allocation.slice(0, selectedBfuCount),
|
|
333
|
+
quantizedCoefficients,
|
|
334
|
+
blockSizeMode: blockModes,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create ATRAC1 encoding pipeline
|
|
341
|
+
*
|
|
342
|
+
* Constructs a complete encoding pipeline that transforms PCM audio samples
|
|
343
|
+
* into ATRAC1 compressed format. The pipeline processes audio through QMF
|
|
344
|
+
* analysis, transient detection, MDCT transform, and psychoacoustic quantization.
|
|
345
|
+
*
|
|
346
|
+
* The returned function can be called repeatedly to encode audio frames,
|
|
347
|
+
* maintaining state through the shared buffer pool for efficient processing.
|
|
348
|
+
*
|
|
349
|
+
* @param {EncoderOptions} [options=new EncoderOptions()] - Encoding configuration
|
|
350
|
+
* @param {BufferPool} [bufferPool=new BufferPool()] - Shared buffer pool for state
|
|
351
|
+
* @returns {Function} Encoding pipeline function that processes PCM samples
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* const encoder = encode(new EncoderOptions(), new BufferPool())
|
|
355
|
+
* const result = encoder(pcmSamples) // Returns quantized ATRAC1 frame data
|
|
356
|
+
*/
|
|
357
|
+
export function encode(
|
|
358
|
+
options = new EncoderOptions(),
|
|
359
|
+
bufferPool = new BufferPool()
|
|
360
|
+
) {
|
|
361
|
+
const context = { options, bufferPool }
|
|
362
|
+
return pipe(
|
|
363
|
+
context,
|
|
364
|
+
qmfAnalysisStage,
|
|
365
|
+
blockSelectorStage,
|
|
366
|
+
mdctStage,
|
|
367
|
+
quantizationStage
|
|
368
|
+
)
|
|
369
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Fast Fourier Transform
|
|
3
|
+
*
|
|
4
|
+
* This module implements a radix-2 Cooley-Tukey FFT algorithm for frequency domain
|
|
5
|
+
* analysis used in psychoacoustic modeling and transient detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class FFT {
|
|
9
|
+
/**
|
|
10
|
+
* Perform in-place FFT on complex data
|
|
11
|
+
* @param {Float32Array} real - Real part of input/output data
|
|
12
|
+
* @param {Float32Array} imag - Imaginary part of input/output data
|
|
13
|
+
*/
|
|
14
|
+
static fft(real, imag) {
|
|
15
|
+
const size = real.length
|
|
16
|
+
if (size === 1) return
|
|
17
|
+
|
|
18
|
+
const bits = Math.log2(size)
|
|
19
|
+
|
|
20
|
+
// Bit reversal
|
|
21
|
+
for (let i = 0; i < size; i++) {
|
|
22
|
+
let reversed = 0
|
|
23
|
+
let temp = i
|
|
24
|
+
for (let b = 0; b < bits; b++) {
|
|
25
|
+
reversed = (reversed << 1) | (temp & 1)
|
|
26
|
+
temp >>= 1
|
|
27
|
+
}
|
|
28
|
+
if (reversed > i) {
|
|
29
|
+
;[real[i], real[reversed]] = [real[reversed], real[i]]
|
|
30
|
+
;[imag[i], imag[reversed]] = [imag[reversed], imag[i]]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cooley-Tukey decimation-in-time
|
|
35
|
+
for (let stride = 2; stride <= size; stride <<= 1) {
|
|
36
|
+
const halfStride = stride >> 1
|
|
37
|
+
const angle = (-2 * Math.PI) / stride
|
|
38
|
+
const wReal = Math.cos(angle)
|
|
39
|
+
const wImag = Math.sin(angle)
|
|
40
|
+
|
|
41
|
+
for (let start = 0; start < size; start += stride) {
|
|
42
|
+
let twiddleReal = 1
|
|
43
|
+
let twiddleImag = 0
|
|
44
|
+
|
|
45
|
+
for (let k = 0; k < halfStride; k++) {
|
|
46
|
+
const evenIndex = start + k
|
|
47
|
+
const oddIndex = evenIndex + halfStride
|
|
48
|
+
|
|
49
|
+
const evenReal = real[evenIndex]
|
|
50
|
+
const evenImag = imag[evenIndex]
|
|
51
|
+
const oddReal = real[oddIndex]
|
|
52
|
+
const oddImag = imag[oddIndex]
|
|
53
|
+
|
|
54
|
+
const tReal = oddReal * twiddleReal - oddImag * twiddleImag
|
|
55
|
+
const tImag = oddReal * twiddleImag + oddImag * twiddleReal
|
|
56
|
+
|
|
57
|
+
real[evenIndex] = evenReal + tReal
|
|
58
|
+
imag[evenIndex] = evenImag + tImag
|
|
59
|
+
real[oddIndex] = evenReal - tReal
|
|
60
|
+
imag[oddIndex] = evenImag - tImag
|
|
61
|
+
|
|
62
|
+
const nextReal = twiddleReal * wReal - twiddleImag * wImag
|
|
63
|
+
twiddleImag = twiddleReal * wImag + twiddleImag * wReal
|
|
64
|
+
twiddleReal = nextReal
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Modified Discrete Cosine Transform
|
|
3
|
+
*
|
|
4
|
+
* This module implements forward and inverse MDCT transforms for the ATRAC1 codec.
|
|
5
|
+
* The MDCT provides time-frequency analysis with perfect reconstruction and
|
|
6
|
+
* 50% overlap between adjacent blocks to avoid blocking artifacts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { FFT } from './fft.js'
|
|
10
|
+
import {
|
|
11
|
+
MDCT_SIZE_SHORT,
|
|
12
|
+
MDCT_SIZE_MID,
|
|
13
|
+
MDCT_SIZE_LONG,
|
|
14
|
+
} from '../core/constants.js'
|
|
15
|
+
import { throwError } from '../utils.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base class for MDCT and IMDCT transforms
|
|
19
|
+
*/
|
|
20
|
+
class MDCTBase {
|
|
21
|
+
constructor(size, scale) {
|
|
22
|
+
this.size = size
|
|
23
|
+
this.halfSize = size >> 1
|
|
24
|
+
this.quarterSize = size >> 2
|
|
25
|
+
this.fftSize = this.halfSize >> 1
|
|
26
|
+
|
|
27
|
+
const alpha = (2.0 * Math.PI) / (8.0 * size)
|
|
28
|
+
const omega = (2.0 * Math.PI) / size
|
|
29
|
+
const scaleRoot = Math.sqrt(scale / size)
|
|
30
|
+
|
|
31
|
+
this.sinCosTable = new Float32Array(this.halfSize)
|
|
32
|
+
for (let i = 0; i < this.quarterSize; i++) {
|
|
33
|
+
const angle = omega * i + alpha
|
|
34
|
+
this.sinCosTable[i * 2] = scaleRoot * Math.cos(angle)
|
|
35
|
+
this.sinCosTable[i * 2 + 1] = scaleRoot * Math.sin(angle)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Forward MDCT transform
|
|
42
|
+
*/
|
|
43
|
+
export class MDCT extends MDCTBase {
|
|
44
|
+
constructor(size, scale) {
|
|
45
|
+
super(size, scale)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Perform forward MDCT transform
|
|
50
|
+
* @param {Float32Array} input - Time-domain input samples
|
|
51
|
+
* @param {Object} mdctBuffers - Optional work buffers for FFT
|
|
52
|
+
* @returns {Float32Array} Frequency-domain MDCT coefficients
|
|
53
|
+
*/
|
|
54
|
+
transform(input, mdctBuffers = null) {
|
|
55
|
+
const buffers = mdctBuffers?.[this.fftSize]
|
|
56
|
+
const real = buffers?.real ?? throwError('mdct: real is required')
|
|
57
|
+
const imag = buffers?.imag ?? throwError('mdct: imag is required')
|
|
58
|
+
|
|
59
|
+
const n4 = this.quarterSize
|
|
60
|
+
const n34 = 3 * n4
|
|
61
|
+
|
|
62
|
+
real.fill(0)
|
|
63
|
+
imag.fill(0)
|
|
64
|
+
|
|
65
|
+
// Pre-FFT butterfly
|
|
66
|
+
for (let i = 0; i < n4; i += 2) {
|
|
67
|
+
const i1 = n34 - 1 - i
|
|
68
|
+
const i2 = n34 + i
|
|
69
|
+
const i3 = n4 + i
|
|
70
|
+
const i4 = n4 - 1 - i
|
|
71
|
+
|
|
72
|
+
const r = input[i1] + input[i2]
|
|
73
|
+
const im = input[i3] - input[i4]
|
|
74
|
+
const c = this.sinCosTable[i]
|
|
75
|
+
const s = this.sinCosTable[i + 1]
|
|
76
|
+
|
|
77
|
+
real[i >> 1] = r * c + im * s
|
|
78
|
+
imag[i >> 1] = im * c - r * s
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let i = n4; i < this.halfSize; i += 2) {
|
|
82
|
+
const idx = i >> 1
|
|
83
|
+
const i1 = n34 - 1 - i
|
|
84
|
+
const i2 = i - n4
|
|
85
|
+
const i3 = n4 + i
|
|
86
|
+
const i4 = 5 * n4 - 1 - i
|
|
87
|
+
|
|
88
|
+
const r = input[i1] - input[i2]
|
|
89
|
+
const im = input[i3] + input[i4]
|
|
90
|
+
const c = this.sinCosTable[i]
|
|
91
|
+
const s = this.sinCosTable[i + 1]
|
|
92
|
+
|
|
93
|
+
real[idx] = r * c + im * s
|
|
94
|
+
imag[idx] = im * c - r * s
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
FFT.fft(real, imag)
|
|
98
|
+
|
|
99
|
+
// Post-FFT processing
|
|
100
|
+
const output = new Float32Array(this.halfSize)
|
|
101
|
+
for (let i = 0; i < this.fftSize; i++) {
|
|
102
|
+
const c = this.sinCosTable[i * 2]
|
|
103
|
+
const s = this.sinCosTable[i * 2 + 1]
|
|
104
|
+
const re = real[i]
|
|
105
|
+
const im = imag[i]
|
|
106
|
+
|
|
107
|
+
output[i * 2] = -re * c - im * s
|
|
108
|
+
output[this.halfSize - 1 - i * 2] = -re * s + im * c
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return output
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Inverse MDCT transform
|
|
117
|
+
*/
|
|
118
|
+
export class IMDCT extends MDCTBase {
|
|
119
|
+
constructor(size, scale = null) {
|
|
120
|
+
super(size, scale || size)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Perform inverse MDCT transform
|
|
125
|
+
* @param {Float32Array} input - Frequency-domain MDCT coefficients
|
|
126
|
+
* @param {Object} mdctBuffers - Optional work buffers for FFT
|
|
127
|
+
* @returns {Float32Array} Time-domain output samples
|
|
128
|
+
*/
|
|
129
|
+
transform(input, mdctBuffers = null) {
|
|
130
|
+
const buffers = mdctBuffers?.[this.fftSize]
|
|
131
|
+
const real = buffers?.real ?? throwError('imdct: real is required')
|
|
132
|
+
const imag = buffers?.imag ?? throwError('imdct: imag is required')
|
|
133
|
+
|
|
134
|
+
const n4 = this.quarterSize
|
|
135
|
+
const n34 = 3 * n4
|
|
136
|
+
|
|
137
|
+
real.fill(0)
|
|
138
|
+
imag.fill(0)
|
|
139
|
+
|
|
140
|
+
// Pre-FFT processing
|
|
141
|
+
for (let i = 0; i < this.fftSize; i++) {
|
|
142
|
+
const i2 = i * 2
|
|
143
|
+
const r = input[i2]
|
|
144
|
+
const im = input[this.halfSize - 1 - i2]
|
|
145
|
+
const c = this.sinCosTable[i2]
|
|
146
|
+
const s = this.sinCosTable[i2 + 1]
|
|
147
|
+
|
|
148
|
+
real[i] = -2.0 * (im * s + r * c)
|
|
149
|
+
imag[i] = -2.0 * (im * c - r * s)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
FFT.fft(real, imag)
|
|
153
|
+
|
|
154
|
+
// Post-FFT butterfly
|
|
155
|
+
const output = new Float32Array(this.size)
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < this.fftSize / 2; i++) {
|
|
158
|
+
const i2 = i * 2
|
|
159
|
+
const c = this.sinCosTable[i2]
|
|
160
|
+
const s = this.sinCosTable[i2 + 1]
|
|
161
|
+
const re = real[i]
|
|
162
|
+
const im = imag[i]
|
|
163
|
+
|
|
164
|
+
const r1 = re * c + im * s
|
|
165
|
+
const i1 = re * s - im * c
|
|
166
|
+
|
|
167
|
+
output[n34 - 1 - i2] = r1
|
|
168
|
+
output[n34 + i2] = r1
|
|
169
|
+
output[n4 + i2] = i1
|
|
170
|
+
output[n4 - 1 - i2] = -i1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (let i = this.fftSize / 2; i < this.fftSize; i++) {
|
|
174
|
+
const idx = (i - this.fftSize / 2) * 2 + n4
|
|
175
|
+
const i2 = i * 2
|
|
176
|
+
const c = this.sinCosTable[i2]
|
|
177
|
+
const s = this.sinCosTable[i2 + 1]
|
|
178
|
+
const re = real[i]
|
|
179
|
+
const im = imag[i]
|
|
180
|
+
|
|
181
|
+
const r1 = re * c + im * s
|
|
182
|
+
const i1 = re * s - im * c
|
|
183
|
+
|
|
184
|
+
output[n34 - 1 - idx] = r1
|
|
185
|
+
output[idx - n4] = -r1
|
|
186
|
+
output[n4 + idx] = i1
|
|
187
|
+
output[5 * n4 - 1 - idx] = i1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return output
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Pre-instantiated transforms
|
|
195
|
+
export const mdct64 = new MDCT(MDCT_SIZE_SHORT, 0.5)
|
|
196
|
+
export const mdct256 = new MDCT(MDCT_SIZE_MID, 0.5)
|
|
197
|
+
export const mdct512 = new MDCT(MDCT_SIZE_LONG, 1.0)
|
|
198
|
+
|
|
199
|
+
export const imdct64 = new IMDCT(MDCT_SIZE_SHORT, MDCT_SIZE_SHORT * 4)
|
|
200
|
+
export const imdct256 = new IMDCT(MDCT_SIZE_MID, MDCT_SIZE_MID * 4)
|
|
201
|
+
export const imdct512 = new IMDCT(MDCT_SIZE_LONG, MDCT_SIZE_LONG * 2)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Perform overlap-add operation for MDCT reconstruction
|
|
205
|
+
* @param {Float32Array} prev - Previous block samples
|
|
206
|
+
* @param {Float32Array} curr - Current block samples
|
|
207
|
+
* @param {Float32Array} window - Window function coefficients
|
|
208
|
+
* @returns {Float32Array} Overlap-added output samples
|
|
209
|
+
*/
|
|
210
|
+
export function overlapAdd(prev, curr, window) {
|
|
211
|
+
const size = prev.length
|
|
212
|
+
const output = new Float32Array(size * 2)
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < size; i++) {
|
|
215
|
+
const w1 = window[i]
|
|
216
|
+
const w2 = window[2 * size - 1 - i]
|
|
217
|
+
const p = prev[i]
|
|
218
|
+
const c = curr[size - 1 - i]
|
|
219
|
+
|
|
220
|
+
output[i] = p * w2 - c * w1
|
|
221
|
+
output[2 * size - 1 - i] = p * w1 + c * w2
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return output
|
|
225
|
+
}
|