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.
@@ -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
+ }