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,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Serialization Module
|
|
3
|
+
*
|
|
4
|
+
* Handles serialization and deserialization of ATRAC1 audio frames and AEA file format.
|
|
5
|
+
* Provides functions for packing encoded frame data into binary format and unpacking
|
|
6
|
+
* it back into structured data for processing.
|
|
7
|
+
*
|
|
8
|
+
* The module manages:
|
|
9
|
+
* - Frame data serialization/deserialization with proper bit packing
|
|
10
|
+
* - AEA file header creation and parsing
|
|
11
|
+
* - Binary format compliance for ATRAC1 specification
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
SOUND_UNIT_SIZE,
|
|
16
|
+
BFU_AMOUNTS,
|
|
17
|
+
SPECS_PER_BFU,
|
|
18
|
+
AEA_HEADER_SIZE,
|
|
19
|
+
AEA_MAGIC,
|
|
20
|
+
AEA_TITLE_OFFSET,
|
|
21
|
+
AEA_TITLE_SIZE,
|
|
22
|
+
AEA_FRAME_COUNT_OFFSET,
|
|
23
|
+
AEA_CHANNEL_COUNT_OFFSET,
|
|
24
|
+
WORD_LENGTH_BITS,
|
|
25
|
+
FRAME_HEADER_BITS,
|
|
26
|
+
FRAME_WORD_LENGTH_BITS,
|
|
27
|
+
FRAME_SCALE_FACTOR_BITS,
|
|
28
|
+
} from '../core/constants.js'
|
|
29
|
+
import { packBits, unpackBits, unpackSignedBits } from '../core/bitstream.js'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serializes encoded frame data into binary format
|
|
33
|
+
* @param {Object} frameData - The encoded frame data to serialize
|
|
34
|
+
* @param {number} frameData.nBfu - Number of block floating units
|
|
35
|
+
* @param {number[]} frameData.blockSizeMode - Block size mode for each band [0-2]
|
|
36
|
+
* @param {Int32Array} frameData.scaleFactorIndices - Scale factor indices for each BFU
|
|
37
|
+
* @param {Int32Array} frameData.wordLengthIndices - Word length indices for each BFU
|
|
38
|
+
* @param {Int32Array[]} frameData.quantizedCoefficients - Quantized spectral coefficients
|
|
39
|
+
* @returns {Uint8Array} Serialized frame data buffer
|
|
40
|
+
*/
|
|
41
|
+
export function serializeFrame(frameData) {
|
|
42
|
+
const buffer = new Uint8Array(SOUND_UNIT_SIZE)
|
|
43
|
+
const view = new DataView(buffer.buffer)
|
|
44
|
+
|
|
45
|
+
// Pack header (16 bits)
|
|
46
|
+
const bfuIndex = BFU_AMOUNTS.indexOf(frameData.nBfu)
|
|
47
|
+
const header =
|
|
48
|
+
((2 - frameData.blockSizeMode[0]) << 14) |
|
|
49
|
+
((2 - frameData.blockSizeMode[1]) << 12) |
|
|
50
|
+
((3 - frameData.blockSizeMode[2]) << 10) |
|
|
51
|
+
(bfuIndex << 5)
|
|
52
|
+
|
|
53
|
+
view.setUint16(0, header, false)
|
|
54
|
+
let bitPosition = FRAME_HEADER_BITS
|
|
55
|
+
|
|
56
|
+
// Pack word lengths (4 bits each)
|
|
57
|
+
for (let i = 0; i < frameData.nBfu; i++) {
|
|
58
|
+
packBits(
|
|
59
|
+
buffer,
|
|
60
|
+
bitPosition,
|
|
61
|
+
frameData.wordLengthIndices[i],
|
|
62
|
+
FRAME_WORD_LENGTH_BITS
|
|
63
|
+
)
|
|
64
|
+
bitPosition += FRAME_WORD_LENGTH_BITS
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Pack scale factors (6 bits each)
|
|
68
|
+
for (let i = 0; i < frameData.nBfu; i++) {
|
|
69
|
+
packBits(
|
|
70
|
+
buffer,
|
|
71
|
+
bitPosition,
|
|
72
|
+
frameData.scaleFactorIndices[i],
|
|
73
|
+
FRAME_SCALE_FACTOR_BITS
|
|
74
|
+
)
|
|
75
|
+
bitPosition += FRAME_SCALE_FACTOR_BITS
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pack quantized coefficients
|
|
79
|
+
for (let i = 0; i < frameData.nBfu; i++) {
|
|
80
|
+
const bitsPerSample = WORD_LENGTH_BITS[frameData.wordLengthIndices[i]]
|
|
81
|
+
if (bitsPerSample > 0) {
|
|
82
|
+
const coefficients = frameData.quantizedCoefficients[i]
|
|
83
|
+
for (const coefficient of coefficients) {
|
|
84
|
+
const value =
|
|
85
|
+
coefficient < 0 ? coefficient + (1 << bitsPerSample) : coefficient
|
|
86
|
+
packBits(buffer, bitPosition, value, bitsPerSample)
|
|
87
|
+
bitPosition += bitsPerSample
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Zero padding at end
|
|
93
|
+
buffer[SOUND_UNIT_SIZE - 3] = 0
|
|
94
|
+
buffer[SOUND_UNIT_SIZE - 2] = 0
|
|
95
|
+
buffer[SOUND_UNIT_SIZE - 1] = 0
|
|
96
|
+
|
|
97
|
+
return buffer
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Deserializes binary frame data back into structured format
|
|
102
|
+
* @param {Uint8Array} buffer - Binary frame data buffer
|
|
103
|
+
* @returns {Object} Deserialized frame data
|
|
104
|
+
* @returns {number} returns.nBfu - Number of block floating units
|
|
105
|
+
* @returns {number[]} returns.blockSizeMode - Block size mode for each band
|
|
106
|
+
* @returns {Int32Array} returns.scaleFactorIndices - Scale factor indices for each BFU
|
|
107
|
+
* @returns {Int32Array} returns.wordLengthIndices - Word length indices for each BFU
|
|
108
|
+
* @returns {Int32Array[]} returns.quantizedCoefficients - Quantized spectral coefficients
|
|
109
|
+
* @throws {Error} If buffer size is invalid
|
|
110
|
+
*/
|
|
111
|
+
export function deserializeFrame(buffer) {
|
|
112
|
+
if (buffer.length !== SOUND_UNIT_SIZE) {
|
|
113
|
+
throw new Error(`Frame must be ${SOUND_UNIT_SIZE} bytes`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
|
117
|
+
|
|
118
|
+
// Unpack header
|
|
119
|
+
const header = view.getUint16(0, false)
|
|
120
|
+
const blockSizeMode = [
|
|
121
|
+
2 - ((header >> 14) & 3),
|
|
122
|
+
2 - ((header >> 12) & 3),
|
|
123
|
+
3 - ((header >> 10) & 3),
|
|
124
|
+
]
|
|
125
|
+
const bfuIndex = (header >> 5) & 7
|
|
126
|
+
const nBfu = BFU_AMOUNTS[bfuIndex]
|
|
127
|
+
|
|
128
|
+
let bitPosition = FRAME_HEADER_BITS
|
|
129
|
+
|
|
130
|
+
// Unpack word lengths
|
|
131
|
+
const wordLengthIndices = new Int32Array(nBfu)
|
|
132
|
+
for (let i = 0; i < nBfu; i++) {
|
|
133
|
+
wordLengthIndices[i] = unpackBits(
|
|
134
|
+
buffer,
|
|
135
|
+
bitPosition,
|
|
136
|
+
FRAME_WORD_LENGTH_BITS
|
|
137
|
+
)
|
|
138
|
+
bitPosition += FRAME_WORD_LENGTH_BITS
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Unpack scale factors
|
|
142
|
+
const scaleFactorIndices = new Int32Array(nBfu)
|
|
143
|
+
for (let i = 0; i < nBfu; i++) {
|
|
144
|
+
scaleFactorIndices[i] = unpackBits(
|
|
145
|
+
buffer,
|
|
146
|
+
bitPosition,
|
|
147
|
+
FRAME_SCALE_FACTOR_BITS
|
|
148
|
+
)
|
|
149
|
+
bitPosition += FRAME_SCALE_FACTOR_BITS
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Unpack coefficients
|
|
153
|
+
const quantizedCoefficients = []
|
|
154
|
+
for (let i = 0; i < nBfu; i++) {
|
|
155
|
+
const bitsPerSample = WORD_LENGTH_BITS[wordLengthIndices[i]]
|
|
156
|
+
const bfuSize = SPECS_PER_BFU[i]
|
|
157
|
+
const coefficients = new Int32Array(bfuSize)
|
|
158
|
+
|
|
159
|
+
if (bitsPerSample > 0) {
|
|
160
|
+
for (let j = 0; j < bfuSize; j++) {
|
|
161
|
+
coefficients[j] = unpackSignedBits(buffer, bitPosition, bitsPerSample)
|
|
162
|
+
bitPosition += bitsPerSample
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
quantizedCoefficients.push(coefficients)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
nBfu,
|
|
171
|
+
scaleFactorIndices,
|
|
172
|
+
wordLengthIndices,
|
|
173
|
+
quantizedCoefficients,
|
|
174
|
+
blockSizeMode,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* AEA file format handler for ATRAC1 audio files
|
|
180
|
+
* Manages creation and parsing of AEA file headers
|
|
181
|
+
*/
|
|
182
|
+
export class AeaFile {
|
|
183
|
+
/**
|
|
184
|
+
* Creates an AEA file header with the specified metadata
|
|
185
|
+
* @param {string} [title=''] - Audio file title (max 255 characters)
|
|
186
|
+
* @param {number} [frameCount=0] - Number of audio frames in the file
|
|
187
|
+
* @param {number} [channelCount=1] - Number of audio channels (1 or 2)
|
|
188
|
+
* @returns {Uint8Array} AEA file header buffer
|
|
189
|
+
*/
|
|
190
|
+
static createHeader(title = '', frameCount = 0, channelCount = 1) {
|
|
191
|
+
const header = new Uint8Array(AEA_HEADER_SIZE)
|
|
192
|
+
const view = new DataView(header.buffer)
|
|
193
|
+
|
|
194
|
+
// Magic number
|
|
195
|
+
header.set(AEA_MAGIC, 0)
|
|
196
|
+
|
|
197
|
+
// Title (null-terminated string)
|
|
198
|
+
const titleBytes = new TextEncoder().encode(title)
|
|
199
|
+
header.set(
|
|
200
|
+
titleBytes.subarray(0, Math.min(titleBytes.length, AEA_TITLE_SIZE - 1)),
|
|
201
|
+
AEA_TITLE_OFFSET
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// Frame count
|
|
205
|
+
view.setUint32(AEA_FRAME_COUNT_OFFSET, frameCount, true)
|
|
206
|
+
|
|
207
|
+
// Channel count
|
|
208
|
+
header[AEA_CHANNEL_COUNT_OFFSET] = channelCount
|
|
209
|
+
|
|
210
|
+
return header
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parses an AEA file header to extract metadata
|
|
215
|
+
* @param {Uint8Array} header - AEA file header buffer
|
|
216
|
+
* @returns {Object} Parsed header information
|
|
217
|
+
* @returns {string} returns.title - Audio file title
|
|
218
|
+
* @returns {number} returns.frameCount - Number of audio frames
|
|
219
|
+
* @returns {number} returns.channelCount - Number of audio channels
|
|
220
|
+
* @throws {Error} If header size is invalid or magic number doesn't match
|
|
221
|
+
*/
|
|
222
|
+
static parseHeader(header) {
|
|
223
|
+
if (header.length !== AEA_HEADER_SIZE) {
|
|
224
|
+
throw new Error(`Header must be ${AEA_HEADER_SIZE} bytes`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check magic number
|
|
228
|
+
for (let i = 0; i < AEA_MAGIC.length; i++) {
|
|
229
|
+
if (header[i] !== AEA_MAGIC[i]) {
|
|
230
|
+
throw new Error('Invalid AEA file')
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const view = new DataView(
|
|
235
|
+
header.buffer,
|
|
236
|
+
header.byteOffset,
|
|
237
|
+
header.byteLength
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Extract title
|
|
241
|
+
const titleEnd = header.indexOf(0, AEA_TITLE_OFFSET)
|
|
242
|
+
const titleLength =
|
|
243
|
+
titleEnd === -1 ? AEA_TITLE_SIZE : titleEnd - AEA_TITLE_OFFSET
|
|
244
|
+
const title = new TextDecoder().decode(
|
|
245
|
+
header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + titleLength)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Extract metadata
|
|
249
|
+
const frameCount = view.getUint32(AEA_FRAME_COUNT_OFFSET, true)
|
|
250
|
+
const channelCount = header[AEA_CHANNEL_COUNT_OFFSET]
|
|
251
|
+
|
|
252
|
+
return { title, frameCount, channelCount }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Decoding Pipeline
|
|
3
|
+
*
|
|
4
|
+
* The decoding pipeline transforms ATRAC1 compressed format back into PCM audio samples
|
|
5
|
+
* through a series of processing stages:
|
|
6
|
+
*
|
|
7
|
+
* 1. Dequantization: Reconstructs MDCT coefficients from quantized data using scale factors
|
|
8
|
+
* 2. Inverse MDCT: Applies Inverse Modified Discrete Cosine Transform to frequency bands
|
|
9
|
+
* 3. QMF Synthesis: Reconstructs full-spectrum audio using quadrature mirror filters
|
|
10
|
+
*
|
|
11
|
+
* Each stage is implemented as a functional pipeline component that can be composed
|
|
12
|
+
* using the pipe utility. The pipeline maintains state through a shared context
|
|
13
|
+
* containing buffer pools and decoding options.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { pipe, throwError } from '../utils.js'
|
|
17
|
+
import { qmfSynthesis } from '../transforms/qmf.js'
|
|
18
|
+
import { imdct64, imdct256, imdct512, overlapAdd } from '../transforms/mdct.js'
|
|
19
|
+
import { dequantize } from '../coding/quantization.js'
|
|
20
|
+
import { BufferPool } from '../core/buffers.js'
|
|
21
|
+
import {
|
|
22
|
+
BFU_BAND_BOUNDARIES,
|
|
23
|
+
BFU_START_LONG,
|
|
24
|
+
BFU_START_SHORT,
|
|
25
|
+
WORD_LENGTH_BITS,
|
|
26
|
+
WINDOW_SHORT,
|
|
27
|
+
MDCT_SIZE_LONG,
|
|
28
|
+
} from '../core/constants.js'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Dequantization Stage - Reconstructs MDCT coefficients from quantized data
|
|
32
|
+
*
|
|
33
|
+
* Reverses the quantization process by:
|
|
34
|
+
* - Reconstructing coefficient values using scale factors and word lengths
|
|
35
|
+
* - Organizing coefficients back into Block Floating Units (BFUs)
|
|
36
|
+
* - Preparing coefficient array for inverse MDCT transform
|
|
37
|
+
*
|
|
38
|
+
* Uses the same BFU organization as the encoder but reconstructs the original
|
|
39
|
+
* coefficient values from the quantized representation.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Function} Stage function that processes quantized frame data
|
|
42
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
43
|
+
*/
|
|
44
|
+
function dequantizationStage() {
|
|
45
|
+
/**
|
|
46
|
+
* Reconstruct MDCT coefficients from quantized data
|
|
47
|
+
* @param {Object} frameData - Quantized frame data
|
|
48
|
+
* @param {number} frameData.nBfu - Number of active BFUs
|
|
49
|
+
* @param {Int32Array} frameData.scaleFactorIndices - Scale factor indices for each BFU
|
|
50
|
+
* @param {Int32Array} frameData.wordLengthIndices - Word length indices for each BFU
|
|
51
|
+
* @param {Array<Int32Array>} frameData.quantizedCoefficients - Quantized coefficient data
|
|
52
|
+
* @param {Array<number>} frameData.blockSizeMode - Block size mode for each band
|
|
53
|
+
* @returns {Array} Dequantization results [coefficients, blockModes]
|
|
54
|
+
* @returns {Float32Array} returns[0] - Reconstructed MDCT coefficients (512 samples)
|
|
55
|
+
* @returns {Array<number>} returns[1] - Block mode indices for each band
|
|
56
|
+
*/
|
|
57
|
+
return (frameData) => {
|
|
58
|
+
const coefficients = new Float32Array(512)
|
|
59
|
+
const {
|
|
60
|
+
nBfu,
|
|
61
|
+
scaleFactorIndices,
|
|
62
|
+
wordLengthIndices,
|
|
63
|
+
quantizedCoefficients,
|
|
64
|
+
blockSizeMode,
|
|
65
|
+
} = frameData
|
|
66
|
+
|
|
67
|
+
for (let bfu = 0; bfu < nBfu; bfu++) {
|
|
68
|
+
const bitsPerSample = WORD_LENGTH_BITS[wordLengthIndices[bfu]]
|
|
69
|
+
|
|
70
|
+
let band = 0
|
|
71
|
+
if (bfu >= BFU_BAND_BOUNDARIES[0]) band = 1
|
|
72
|
+
if (bfu >= BFU_BAND_BOUNDARIES[1]) band = 2
|
|
73
|
+
|
|
74
|
+
const isLongBlock = blockSizeMode[band] === 0
|
|
75
|
+
const position = isLongBlock ? BFU_START_LONG[bfu] : BFU_START_SHORT[bfu]
|
|
76
|
+
|
|
77
|
+
if (bitsPerSample > 0) {
|
|
78
|
+
const quantized = quantizedCoefficients[bfu]
|
|
79
|
+
const dequantized = dequantize(
|
|
80
|
+
quantized,
|
|
81
|
+
scaleFactorIndices[bfu],
|
|
82
|
+
bitsPerSample
|
|
83
|
+
)
|
|
84
|
+
coefficients.set(dequantized, position)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [coefficients, blockSizeMode]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Inverse MDCT Stage - Transforms frequency coefficients back to time domain
|
|
94
|
+
*
|
|
95
|
+
* Applies Inverse Modified Discrete Cosine Transform to reconstruct time-domain
|
|
96
|
+
* samples from frequency coefficients. The transform size depends on block modes:
|
|
97
|
+
* - Long blocks: 512/256 samples for steady-state signals
|
|
98
|
+
* - Short blocks: 64 samples for transient signals
|
|
99
|
+
*
|
|
100
|
+
* Applies windowing and overlap-add processing to prevent blocking artifacts.
|
|
101
|
+
* Mid and high bands are spectrally unreversed from ATRAC1 format.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} context - Pipeline context containing bufferPool
|
|
104
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
105
|
+
* @returns {Function} Stage function that processes dequantization results
|
|
106
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
107
|
+
*/
|
|
108
|
+
function imdctStage(context) {
|
|
109
|
+
const bufferPool =
|
|
110
|
+
context?.bufferPool ?? throwError('imdctStage: bufferPool is required')
|
|
111
|
+
const overlapBuffers = bufferPool.imdctOverlap
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Transform coefficients back to time domain using inverse MDCT
|
|
115
|
+
* @param {Array} input - Dequantization results
|
|
116
|
+
* @param {Float32Array} input[0] - MDCT coefficients (512 samples)
|
|
117
|
+
* @param {Array<number>} input[1] - Block mode indices for each band
|
|
118
|
+
* @returns {Array<Float32Array>} Three reconstructed frequency bands [low, mid, high]
|
|
119
|
+
*/
|
|
120
|
+
return (input) => {
|
|
121
|
+
const [coefficients, blockModes] = input
|
|
122
|
+
const bands = []
|
|
123
|
+
|
|
124
|
+
const bandCoeffs = [
|
|
125
|
+
coefficients.subarray(0, 128),
|
|
126
|
+
coefficients.subarray(128, 256),
|
|
127
|
+
coefficients.subarray(256, 512),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for (let band = 0; band < 3; band++) {
|
|
131
|
+
const bandSize = band === 2 ? 256 : 128
|
|
132
|
+
const numBlocks = 1 << blockModes[band]
|
|
133
|
+
const blockSize = numBlocks === 1 ? bandSize : 32
|
|
134
|
+
|
|
135
|
+
const imdctFunc =
|
|
136
|
+
numBlocks === 1 ? (band === 2 ? imdct512 : imdct256) : imdct64
|
|
137
|
+
|
|
138
|
+
const invBuf = bufferPool.transformBuffers[MDCT_SIZE_LONG]
|
|
139
|
+
invBuf.fill(0)
|
|
140
|
+
const dstBuf = overlapBuffers[band]
|
|
141
|
+
const prevBuf = dstBuf.slice(bandSize * 2 - 16, bandSize * 2)
|
|
142
|
+
|
|
143
|
+
let start = 0
|
|
144
|
+
let pos = 0
|
|
145
|
+
|
|
146
|
+
for (let block = 0; block < numBlocks; block++) {
|
|
147
|
+
let blockSpecs = bandCoeffs[band].slice(pos, pos + blockSize)
|
|
148
|
+
|
|
149
|
+
// Reverse spectral reversal for mid and high bands
|
|
150
|
+
if (band > 0) {
|
|
151
|
+
const reversed = bufferPool.reversalBuffers[blockSpecs.length]
|
|
152
|
+
for (let i = 0; i < blockSpecs.length; i++) {
|
|
153
|
+
reversed[i] = blockSpecs[blockSpecs.length - 1 - i]
|
|
154
|
+
}
|
|
155
|
+
blockSpecs = reversed
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const inv = imdctFunc.transform(blockSpecs, bufferPool.mdctBuffers)
|
|
159
|
+
const invStart = inv.length / 4
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < blockSize; i++) {
|
|
162
|
+
invBuf[start + i] = inv[invStart + i]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const dstPart = overlapAdd(
|
|
166
|
+
prevBuf.slice(0, 16),
|
|
167
|
+
invBuf.slice(start, start + 16),
|
|
168
|
+
WINDOW_SHORT
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
dstBuf.set(dstPart, start)
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < 16; i++) {
|
|
174
|
+
prevBuf[i] = invBuf[start + 16 + i]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
start += blockSize
|
|
178
|
+
pos += blockSize
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (numBlocks === 1) {
|
|
182
|
+
const copyLen = band === 2 ? 240 : 112
|
|
183
|
+
for (let i = 0; i < copyLen; i++) {
|
|
184
|
+
dstBuf[32 + i] = invBuf[16 + i]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < 16; i++) {
|
|
189
|
+
dstBuf[bandSize * 2 - 16 + i] = invBuf[bandSize - 16 + i]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
bands.push(dstBuf.slice(0, bandSize))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return bands
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* QMF Synthesis Stage - Reconstructs full-spectrum audio from frequency bands
|
|
201
|
+
*
|
|
202
|
+
* Performs two-stage QMF synthesis to reconstruct the full-spectrum signal
|
|
203
|
+
* from three frequency bands:
|
|
204
|
+
* - Low band (0-5.5kHz): processed at full resolution
|
|
205
|
+
* - Mid band (5.5-11kHz): processed at half resolution
|
|
206
|
+
* - High band (11-22kHz): processed at quarter resolution with delay compensation
|
|
207
|
+
*
|
|
208
|
+
* This is the inverse operation of the QMF analysis performed during encoding,
|
|
209
|
+
* combining the separate frequency bands back into a single wideband signal.
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} context - Pipeline context containing bufferPool
|
|
212
|
+
* @param {BufferPool} context.bufferPool - Shared buffer pool for efficient memory management
|
|
213
|
+
* @returns {Function} Stage function that processes inverse MDCT results
|
|
214
|
+
* @throws {Error} If bufferPool is not provided in context
|
|
215
|
+
*/
|
|
216
|
+
function qmfSynthesisStage(context) {
|
|
217
|
+
const bufferPool =
|
|
218
|
+
context?.bufferPool ??
|
|
219
|
+
throwError('qmfSynthesisStage: bufferPool is required')
|
|
220
|
+
const delays = bufferPool.qmfDelays
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reconstruct full-spectrum PCM samples from frequency bands
|
|
224
|
+
* @param {Array<Float32Array>} bands - Three frequency bands [low, mid, high]
|
|
225
|
+
* @returns {Float32Array} Reconstructed PCM samples
|
|
226
|
+
*/
|
|
227
|
+
return (bands) => {
|
|
228
|
+
// Apply high band delay compensation
|
|
229
|
+
const delayedHigh = bufferPool.qmfWorkBuffers.highBandDelay[bands[2].length]
|
|
230
|
+
delayedHigh.set(delays.highBand)
|
|
231
|
+
delayedHigh.set(bands[2])
|
|
232
|
+
|
|
233
|
+
const highBand = delayedHigh.slice(0, bands[0].length * 2)
|
|
234
|
+
delays.highBand = delayedHigh.slice(bands[0].length * 2)
|
|
235
|
+
|
|
236
|
+
// Second stage synthesis: combine low and mid bands
|
|
237
|
+
const stage2 = qmfSynthesis(
|
|
238
|
+
bands[0],
|
|
239
|
+
bands[1],
|
|
240
|
+
delays.midBand,
|
|
241
|
+
bufferPool.qmfWorkBuffers
|
|
242
|
+
)
|
|
243
|
+
delays.midBand = stage2.newDelay
|
|
244
|
+
|
|
245
|
+
// First stage synthesis: combine result with high band
|
|
246
|
+
const stage1 = qmfSynthesis(
|
|
247
|
+
stage2.output,
|
|
248
|
+
highBand,
|
|
249
|
+
delays.lowBand,
|
|
250
|
+
bufferPool.qmfWorkBuffers
|
|
251
|
+
)
|
|
252
|
+
delays.lowBand = stage1.newDelay
|
|
253
|
+
|
|
254
|
+
return stage1.output
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create ATRAC1 decoding pipeline
|
|
260
|
+
*
|
|
261
|
+
* Constructs a complete decoding pipeline that transforms ATRAC1 compressed format
|
|
262
|
+
* back into PCM audio samples. The pipeline processes quantized data through
|
|
263
|
+
* dequantization, inverse MDCT transform, and QMF synthesis.
|
|
264
|
+
*
|
|
265
|
+
* The returned function can be called repeatedly to decode audio frames,
|
|
266
|
+
* maintaining state through the shared buffer pool for efficient processing.
|
|
267
|
+
*
|
|
268
|
+
* @param {BufferPool} [bufferPool=new BufferPool()] - Shared buffer pool for state
|
|
269
|
+
* @returns {Function} Decoding pipeline function that processes quantized frame data
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* const decoder = decode(new BufferPool())
|
|
273
|
+
* const result = decoder(quantizedFrame) // Returns reconstructed PCM samples
|
|
274
|
+
*/
|
|
275
|
+
export function decode(bufferPool = new BufferPool()) {
|
|
276
|
+
const context = { bufferPool }
|
|
277
|
+
return pipe(context, dequantizationStage, imdctStage, qmfSynthesisStage)
|
|
278
|
+
}
|