carta1 1.1.9 → 1.2.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.
Files changed (44) hide show
  1. package/README.md +149 -75
  2. package/bin/cli.js +8 -4
  3. package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
  4. package/codec/allocation/source.js +69 -0
  5. package/codec/allocation/stage.js +38 -0
  6. package/codec/analysis/bands.js +49 -0
  7. package/codec/analysis/{transient.js → blocks.js} +90 -47
  8. package/codec/analysis/spectrum.js +160 -0
  9. package/codec/analysis/stage.js +32 -0
  10. package/codec/{browser/interface.js → boundary/browser.js} +1 -1
  11. package/codec/boundary/container.js +96 -0
  12. package/codec/boundary/pcm.js +193 -0
  13. package/codec/boundary/processor.js +208 -0
  14. package/codec/{io/readers.js → boundary/reader.js} +2 -1
  15. package/codec/boundary/stream.js +361 -0
  16. package/codec/{browser → boundary}/worker.js +45 -42
  17. package/codec/core/constants.js +9 -131
  18. package/codec/core/geometry.js +25 -0
  19. package/codec/core/tables.js +117 -0
  20. package/codec/dequantization.js +54 -0
  21. package/codec/index.js +12 -11
  22. package/codec/pipeline/decoder.js +10 -399
  23. package/codec/pipeline/encoder.js +12 -434
  24. package/codec/quantization/spectrum.js +67 -0
  25. package/codec/quantization/stage.js +45 -0
  26. package/codec/{transforms/qmf.js → signal/bands.js} +8 -5
  27. package/codec/{transforms → signal}/fft.js +1 -1
  28. package/codec/{transforms/mdct.js → signal/spectrum.js} +16 -1
  29. package/codec/{core/buffers.js → state.js} +46 -35
  30. package/codec/{io → syntax}/bitstream.js +28 -9
  31. package/codec/{io/serialization.js → syntax/frame.js} +5 -101
  32. package/codec/synthesis/bands.js +201 -0
  33. package/codec/synthesis/pcm.js +50 -0
  34. package/codec/synthesis/stage.js +17 -0
  35. package/codec/utils.js +9 -52
  36. package/dist/carta1-worker-interface.min.js +1 -1
  37. package/dist/carta1-worker-interface.min.js.map +1 -1
  38. package/dist/carta1-worker.min.js +1 -1
  39. package/dist/carta1-worker.min.js.map +1 -1
  40. package/dist/carta1.min.js +1 -1
  41. package/dist/carta1.min.js.map +1 -1
  42. package/package.json +16 -16
  43. package/codec/coding/quantization.js +0 -149
  44. package/codec/io/processor.js +0 -558
@@ -1,12 +1,45 @@
1
1
  /**
2
- * Carta1 Audio Codec - Buffer Management
2
+ * Carta1 stream state and reusable scratch.
3
3
  */
4
4
 
5
- import { QMF_DELAY, QMF_HIGH_BAND_DELAY } from './constants.js'
5
+ import { QMF_DELAY, QMF_HIGH_BAND_DELAY } from './core/constants.js'
6
6
 
7
- export class BufferPool {
7
+ /**
8
+ * Values with meaning across chronological frames.
9
+ */
10
+ class History {
11
+ constructor() {
12
+ this.qmfDelays = {
13
+ lowBand: new Float32Array(QMF_DELAY),
14
+ midBand: new Float32Array(QMF_DELAY),
15
+ highBand: new Float32Array(QMF_HIGH_BAND_DELAY),
16
+ }
17
+
18
+ this.transientDetection = [
19
+ new Float32Array(64),
20
+ new Float32Array(64),
21
+ new Float32Array(128),
22
+ ]
23
+
24
+ this.mdctOverlap = [
25
+ new Float32Array(32),
26
+ new Float32Array(32),
27
+ new Float32Array(32),
28
+ ]
29
+
30
+ this.imdctOverlap = [
31
+ new Float32Array(256),
32
+ new Float32Array(256),
33
+ new Float32Array(512),
34
+ ]
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Reusable storage with no meaning between operations.
40
+ */
41
+ class Scratch {
8
42
  constructor() {
9
- // Transform buffers
10
43
  this.transformBuffers = {
11
44
  64: new Float32Array(64),
12
45
  128: new Float32Array(128),
@@ -14,7 +47,6 @@ export class BufferPool {
14
47
  512: new Float32Array(512),
15
48
  }
16
49
 
17
- // QMF work buffers
18
50
  this.qmfWorkBuffers = {
19
51
  delay: {
20
52
  128: new Float32Array(QMF_DELAY + 128),
@@ -27,21 +59,6 @@ export class BufferPool {
27
59
  },
28
60
  }
29
61
 
30
- // QMF delay lines
31
- this.qmfDelays = {
32
- lowBand: new Float32Array(QMF_DELAY),
33
- midBand: new Float32Array(QMF_DELAY),
34
- highBand: new Float32Array(QMF_HIGH_BAND_DELAY),
35
- }
36
-
37
- // Transient detector
38
- this.transientDetection = [
39
- new Float32Array(64),
40
- new Float32Array(64),
41
- new Float32Array(128),
42
- ]
43
-
44
- // MDCT/IMDCT work buffers
45
62
  this.mdctBuffers = {
46
63
  16: {
47
64
  real: new Float32Array(16),
@@ -57,21 +74,6 @@ export class BufferPool {
57
74
  },
58
75
  }
59
76
 
60
- // MDCT overlap buffers (encoder)
61
- this.mdctOverlap = [
62
- new Float32Array(32),
63
- new Float32Array(32),
64
- new Float32Array(32),
65
- ]
66
-
67
- // IMDCT overlap buffers (decoder)
68
- this.imdctOverlap = [
69
- new Float32Array(256),
70
- new Float32Array(256),
71
- new Float32Array(512),
72
- ]
73
-
74
- // Spectrum reversal buffers
75
77
  this.reversalBuffers = {
76
78
  32: new Float32Array(32),
77
79
  128: new Float32Array(128),
@@ -79,3 +81,12 @@ export class BufferPool {
79
81
  }
80
82
  }
81
83
  }
84
+
85
+ /**
86
+ * Own one chronological stream's history and reusable storage.
87
+ */
88
+ export class BufferPool {
89
+ constructor() {
90
+ Object.assign(this, new History(), new Scratch())
91
+ }
92
+ }
@@ -1,24 +1,40 @@
1
1
  /**
2
- * Carta1 Audio Codec - Bitstream Operations
2
+ * Carta1 bitstream syntax operators.
3
3
  *
4
4
  * This module provides low-level bitstream packing and unpacking operations
5
5
  * for reading and writing ATRAC1 frame data with bit-level precision.
6
6
  */
7
7
 
8
+ /**
9
+ * Reject field widths outside the shared bitstream primitive's safe range.
10
+ *
11
+ * @param {number} bitCount
12
+ * @returns {void}
13
+ */
14
+ function assertBitCount(bitCount) {
15
+ if (!Number.isInteger(bitCount) || bitCount < 0 || bitCount > 32) {
16
+ throw new RangeError(
17
+ `Bit count must be an integer in 0..32, got ${bitCount}`
18
+ )
19
+ }
20
+ }
21
+
8
22
  /**
9
23
  * Pack bits into a buffer at a specific bit position
10
24
  * @param {Uint8Array} buffer - Destination buffer
11
25
  * @param {number} bitPosition - Bit position to start writing at
12
26
  * @param {number} value - Value to pack
13
27
  * @param {number} bitCount - Number of bits to pack
28
+ * @returns {void}
14
29
  */
15
30
  export function packBits(buffer, bitPosition, value, bitCount) {
31
+ assertBitCount(bitCount)
16
32
  if (bitCount === 0) return
17
33
 
18
34
  let byteIndex = Math.floor(bitPosition / 8)
19
35
  let bitOffset = bitPosition % 8
20
36
 
21
- value &= (1 << bitCount) - 1
37
+ const unsignedValue = Number(value) >>> 0
22
38
 
23
39
  let bitsWritten = 0
24
40
  while (bitsWritten < bitCount && byteIndex < buffer.length) {
@@ -26,9 +42,9 @@ export function packBits(buffer, bitPosition, value, bitCount) {
26
42
  const bitsToWrite = Math.min(bitCount - bitsWritten, bitsAvailable)
27
43
 
28
44
  const shift = bitCount - bitsWritten - bitsToWrite
29
- const valueBits = (value >> shift) & ((1 << bitsToWrite) - 1)
45
+ const valueBits = (unsignedValue >>> shift) & (2 ** bitsToWrite - 1)
30
46
 
31
- const mask = ((1 << bitsToWrite) - 1) << (bitsAvailable - bitsToWrite)
47
+ const mask = (2 ** bitsToWrite - 1) << (bitsAvailable - bitsToWrite)
32
48
  buffer[byteIndex] =
33
49
  (buffer[byteIndex] & ~mask) | (valueBits << (bitsAvailable - bitsToWrite))
34
50
 
@@ -46,20 +62,21 @@ export function packBits(buffer, bitPosition, value, bitCount) {
46
62
  * @returns {number} Unpacked unsigned value
47
63
  */
48
64
  export function unpackBits(buffer, bitPosition, bitCount) {
65
+ assertBitCount(bitCount)
49
66
  if (bitCount === 0) return 0
50
67
 
51
68
  let byteIndex = Math.floor(bitPosition / 8)
52
69
  let bitOffset = bitPosition % 8
53
70
  let value = 0
54
71
 
55
- for (let bitsRead = 0; bitsRead < bitCount && byteIndex < buffer.length; ) {
72
+ for (let bitsRead = 0; bitsRead < bitCount && byteIndex < buffer.length;) {
56
73
  const bitsAvailable = 8 - bitOffset
57
74
  const bitsToRead = Math.min(bitCount - bitsRead, bitsAvailable)
58
75
 
59
- const mask = (1 << bitsToRead) - 1
76
+ const mask = 2 ** bitsToRead - 1
60
77
  const bits = (buffer[byteIndex] >> (bitsAvailable - bitsToRead)) & mask
61
78
 
62
- value = (value << bitsToRead) | bits
79
+ value = (value * 2 ** bitsToRead + bits) >>> 0
63
80
  bitsRead += bitsToRead
64
81
  byteIndex++
65
82
  bitOffset = 0
@@ -76,7 +93,9 @@ export function unpackBits(buffer, bitPosition, bitCount) {
76
93
  * @returns {number} Unpacked signed value (two's complement)
77
94
  */
78
95
  export function unpackSignedBits(buffer, bitPosition, bitCount) {
96
+ assertBitCount(bitCount)
97
+ if (bitCount === 0) return 0
79
98
  const value = unpackBits(buffer, bitPosition, bitCount)
80
- const signBit = 1 << (bitCount - 1)
81
- return value >= signBit ? value - (1 << bitCount) : value
99
+ const signBit = 2 ** (bitCount - 1)
100
+ return value >= signBit ? value - 2 ** bitCount : value
82
101
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Carta1 Audio Codec - Serialization Module
2
+ * Carta1 structured-frame syntax.
3
3
  *
4
- * Handles serialization and deserialization of ATRAC1 audio frames and AEA file format.
4
+ * Handles serialization and deserialization of ATRAC1 audio frames.
5
5
  * Provides functions for packing encoded frame data into binary format and unpacking
6
6
  * it back into structured data for processing.
7
7
  *
@@ -13,29 +13,16 @@
13
13
 
14
14
  import {
15
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
16
  FRAME_HEADER_BITS,
26
17
  FRAME_WORD_LENGTH_BITS,
27
18
  FRAME_SCALE_FACTOR_BITS,
28
19
  } from '../core/constants.js'
20
+ import { BFU_AMOUNTS, SPECS_PER_BFU, WORD_LENGTH_BITS } from '../core/tables.js'
29
21
  import { packBits, unpackBits, unpackSignedBits } from './bitstream.js'
30
22
 
31
23
  /**
32
24
  * 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.blockModes - Block modes 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
25
+ * @param {object} frameData - The encoded frame data to serialize
39
26
  * @returns {Uint8Array} Serialized frame data buffer
40
27
  */
41
28
  export function serializeFrame(frameData) {
@@ -100,12 +87,7 @@ export function serializeFrame(frameData) {
100
87
  /**
101
88
  * Deserializes binary frame data back into structured format
102
89
  * @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.blockModes - Block modes 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
90
+ * @returns {object} Deserialized frame data
109
91
  * @throws {Error} If buffer size is invalid
110
92
  */
111
93
  export function deserializeFrame(buffer) {
@@ -174,81 +156,3 @@ export function deserializeFrame(buffer) {
174
156
  blockModes,
175
157
  }
176
158
  }
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,201 @@
1
+ /**
2
+ * Carta1 spectral synthesis application.
3
+ */
4
+
5
+ import {
6
+ MDCT_SHORT_BLOCK_SIZE,
7
+ MDCT_SIZE_LONG,
8
+ MDCT_TAIL_WINDOW_SIZE,
9
+ } from '../core/constants.js'
10
+ import { extractBandCoefficients } from '../core/geometry.js'
11
+ import { MDCT_BAND_CONFIGS, WINDOW_SHORT } from '../core/tables.js'
12
+ import {
13
+ imdct64,
14
+ imdct256,
15
+ imdct512,
16
+ overlapAdd,
17
+ reverseSpectrum,
18
+ } from '../signal/spectrum.js'
19
+ import { throwError } from '../utils.js'
20
+
21
+ /**
22
+ * Synthesize time-domain bands from the ATRAC1 spectrum.
23
+ *
24
+ * @param {object} context
25
+ * @returns {Function}
26
+ * @throws {Error} If the buffer pool is missing
27
+ */
28
+ export function synthesizeSpectrum(context) {
29
+ const bufferPool =
30
+ context?.bufferPool ??
31
+ throwError('synthesizeSpectrum: bufferPool is required')
32
+ const overlapBuffers = bufferPool.imdctOverlap
33
+ const transformFunctions = [imdct256, imdct256, imdct512]
34
+
35
+ /**
36
+ * @param {Float32Array} coefficients
37
+ * @param {number} bandIndex
38
+ * @param {number} blockMode
39
+ * @param {Float32Array} overlapBuffer
40
+ * @param {Object} config
41
+ * @returns {Float32Array}
42
+ */
43
+ function transformBand(
44
+ coefficients,
45
+ bandIndex,
46
+ blockMode,
47
+ overlapBuffer,
48
+ config
49
+ ) {
50
+ if (blockMode === 0) {
51
+ return inverseLongBlock(
52
+ coefficients,
53
+ bandIndex,
54
+ overlapBuffer,
55
+ config,
56
+ transformFunctions[bandIndex]
57
+ )
58
+ }
59
+
60
+ return inverseShortBlocks(coefficients, bandIndex, overlapBuffer, config)
61
+ }
62
+
63
+ /**
64
+ * @param {Float32Array} coefficients
65
+ * @param {number} bandIndex
66
+ * @param {Float32Array} overlapBuffer
67
+ * @param {Object} config
68
+ * @param {Object} transformFunction
69
+ * @returns {Float32Array}
70
+ */
71
+ function inverseLongBlock(
72
+ coefficients,
73
+ bandIndex,
74
+ overlapBuffer,
75
+ config,
76
+ transformFunction
77
+ ) {
78
+ let blockSpectrum = coefficients
79
+ if (bandIndex > 0) {
80
+ blockSpectrum = reverseSpectrum(coefficients, bufferPool.reversalBuffers)
81
+ }
82
+
83
+ const inverse = transformFunction.transform(
84
+ blockSpectrum,
85
+ bufferPool.mdctBuffers
86
+ )
87
+ const inverseStart = inverse.length / 4
88
+ const inverseBuffer = bufferPool.transformBuffers[MDCT_SIZE_LONG]
89
+ inverseBuffer.fill(0)
90
+
91
+ for (let i = 0; i < config.size; i++) {
92
+ inverseBuffer[i] = inverse[inverseStart + i]
93
+ }
94
+
95
+ const previous = overlapBuffer.slice(
96
+ config.size * 2 - MDCT_TAIL_WINDOW_SIZE,
97
+ config.size * 2
98
+ )
99
+ const overlap = overlapAdd(
100
+ previous.slice(0, MDCT_TAIL_WINDOW_SIZE),
101
+ inverseBuffer.slice(0, MDCT_TAIL_WINDOW_SIZE),
102
+ WINDOW_SHORT
103
+ )
104
+ overlapBuffer.set(overlap, 0)
105
+
106
+ for (let i = 0; i < MDCT_TAIL_WINDOW_SIZE; i++) {
107
+ previous[i] = inverseBuffer[MDCT_TAIL_WINDOW_SIZE + i]
108
+ }
109
+
110
+ const copyLength = bandIndex === 2 ? 240 : 112
111
+ for (let i = 0; i < copyLength; i++) {
112
+ overlapBuffer[32 + i] = inverseBuffer[MDCT_TAIL_WINDOW_SIZE + i]
113
+ }
114
+
115
+ for (let i = 0; i < MDCT_TAIL_WINDOW_SIZE; i++) {
116
+ overlapBuffer[config.size * 2 - MDCT_TAIL_WINDOW_SIZE + i] =
117
+ inverseBuffer[config.size - MDCT_TAIL_WINDOW_SIZE + i]
118
+ }
119
+
120
+ return overlapBuffer.slice(0, config.size)
121
+ }
122
+
123
+ /**
124
+ * @param {Float32Array} coefficients
125
+ * @param {number} bandIndex
126
+ * @param {Float32Array} overlapBuffer
127
+ * @param {Object} config
128
+ * @returns {Float32Array}
129
+ */
130
+ function inverseShortBlocks(coefficients, bandIndex, overlapBuffer, config) {
131
+ const numBlocks = 1 << (config.size === 256 ? 3 : 2)
132
+ const inverseBuffer = bufferPool.transformBuffers[MDCT_SIZE_LONG]
133
+ inverseBuffer.fill(0)
134
+
135
+ const previous = overlapBuffer.slice(
136
+ config.size * 2 - MDCT_TAIL_WINDOW_SIZE,
137
+ config.size * 2
138
+ )
139
+ let start = 0
140
+ let position = 0
141
+
142
+ for (let block = 0; block < numBlocks; block++) {
143
+ let blockSpectrum = coefficients.slice(
144
+ position,
145
+ position + MDCT_SHORT_BLOCK_SIZE
146
+ )
147
+ if (bandIndex > 0) {
148
+ blockSpectrum = reverseSpectrum(
149
+ blockSpectrum,
150
+ bufferPool.reversalBuffers
151
+ )
152
+ }
153
+
154
+ const inverse = imdct64.transform(blockSpectrum, bufferPool.mdctBuffers)
155
+ const inverseStart = inverse.length / 4
156
+ for (let i = 0; i < MDCT_SHORT_BLOCK_SIZE; i++) {
157
+ inverseBuffer[start + i] = inverse[inverseStart + i]
158
+ }
159
+
160
+ const overlap = overlapAdd(
161
+ previous.slice(0, MDCT_TAIL_WINDOW_SIZE),
162
+ inverseBuffer.slice(start, start + MDCT_TAIL_WINDOW_SIZE),
163
+ WINDOW_SHORT
164
+ )
165
+ overlapBuffer.set(overlap, start)
166
+
167
+ for (let i = 0; i < MDCT_TAIL_WINDOW_SIZE; i++) {
168
+ previous[i] = inverseBuffer[start + MDCT_TAIL_WINDOW_SIZE + i]
169
+ }
170
+
171
+ start += MDCT_SHORT_BLOCK_SIZE
172
+ position += MDCT_SHORT_BLOCK_SIZE
173
+ }
174
+
175
+ for (let i = 0; i < MDCT_TAIL_WINDOW_SIZE; i++) {
176
+ overlapBuffer[config.size * 2 - MDCT_TAIL_WINDOW_SIZE + i] =
177
+ inverseBuffer[config.size - MDCT_TAIL_WINDOW_SIZE + i]
178
+ }
179
+
180
+ return overlapBuffer.slice(0, config.size)
181
+ }
182
+
183
+ /**
184
+ * @param {object} input
185
+ * @returns {Array<Float32Array>}
186
+ */
187
+ return (input) => {
188
+ const { coefficients, blockModes } = input
189
+
190
+ return MDCT_BAND_CONFIGS.map((config, bandIndex) => {
191
+ const bandCoefficients = extractBandCoefficients(coefficients, bandIndex)
192
+ return transformBand(
193
+ bandCoefficients,
194
+ bandIndex,
195
+ blockModes[bandIndex],
196
+ overlapBuffers[bandIndex],
197
+ config
198
+ )
199
+ })
200
+ }
201
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Carta1 PCM synthesis application.
3
+ */
4
+
5
+ import { synthesizeQmfPair } from '../signal/bands.js'
6
+ import { throwError } from '../utils.js'
7
+
8
+ /**
9
+ * Synthesize full-spectrum PCM from frequency bands.
10
+ *
11
+ * @param {object} context
12
+ * @returns {Function}
13
+ * @throws {Error} If the buffer pool is missing
14
+ */
15
+ export function synthesizePcm(context) {
16
+ const bufferPool =
17
+ context?.bufferPool ?? throwError('synthesizePcm: bufferPool is required')
18
+ const delays = bufferPool.qmfDelays
19
+
20
+ /**
21
+ * @param {Array<Float32Array>} bands
22
+ * @returns {Float32Array}
23
+ */
24
+ return (bands) => {
25
+ const delayedHigh = bufferPool.qmfWorkBuffers.highBandDelay[bands[2].length]
26
+ delayedHigh.set(delays.highBand)
27
+ delayedHigh.set(bands[2], delays.highBand.length)
28
+
29
+ const highBand = delayedHigh.slice(0, bands[0].length * 2)
30
+ delays.highBand = delayedHigh.slice(bands[0].length * 2)
31
+
32
+ const stage2 = synthesizeQmfPair(
33
+ bands[0],
34
+ bands[1],
35
+ delays.midBand,
36
+ bufferPool.qmfWorkBuffers
37
+ )
38
+ delays.midBand = stage2.newDelay
39
+
40
+ const stage1 = synthesizeQmfPair(
41
+ stage2.output,
42
+ highBand,
43
+ delays.lowBand,
44
+ bufferPool.qmfWorkBuffers
45
+ )
46
+ delays.lowBand = stage1.newDelay
47
+
48
+ return stage1.output
49
+ }
50
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Carta1 synthesis stage.
3
+ */
4
+
5
+ import { pipe } from '../utils.js'
6
+ import { synthesizeSpectrum } from './bands.js'
7
+ import { synthesizePcm } from './pcm.js'
8
+
9
+ /**
10
+ * Synthesize PCM from the dequantized ATRAC1 spectrum.
11
+ *
12
+ * @param {object} context
13
+ * @returns {Function}
14
+ */
15
+ export function synthesize(context) {
16
+ return pipe(context, synthesizeSpectrum, synthesizePcm)
17
+ }
package/codec/utils.js CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * Carta1 Audio Codec - Utilities
3
3
  *
4
- * This module provides common utility functions for the ATRAC1 codec including
5
- * pipeline composition, error handling, and audio stream processing utilities
6
- * for delay compensation and frame padding.
4
+ * This module provides common utility functions for the ATRAC1 codec.
7
5
  */
8
6
 
9
7
  /**
@@ -16,55 +14,14 @@ export function throwError(msg) {
16
14
  }
17
15
 
18
16
  /**
19
- * Creates a pipeline by composing multiple processing stages
20
- * @param {Object} context - Shared context passed to all stages
21
- * @param {...Function} stages - Stage functions to compose
22
- * @returns {Function} Composed pipeline function
23
- */
24
- export function pipe(context, ...stages) {
25
- const functions = stages.map((stage) => stage(context))
26
-
27
- return (input) => {
28
- return functions.reduce((value, fn) => fn(value), input)
29
- }
30
- }
31
-
32
- /**
33
- * Reverses the spectral order of coefficients for mid/high band processing
34
- *
35
- * Used in both MDCT and IMDCT stages to handle spectral reversal required
36
- * by the ATRAC1 format for mid and high frequency bands.
37
- *
38
- * @param {Float32Array} spectrum - Input spectrum coefficients
39
- * @param {Object} reversalBuffers - Pre-allocated buffers for different spectrum sizes
40
- * @returns {Float32Array} Spectrum with reversed coefficient order
41
- */
42
- export function reverseSpectrum(spectrum, reversalBuffers) {
43
- const reversed = reversalBuffers[spectrum.length]
44
- for (let i = 0; i < spectrum.length; i++) {
45
- reversed[i] = spectrum[spectrum.length - 1 - i]
46
- }
47
- return reversed
48
- }
49
-
50
- /**
51
- * Calculates the starting offset for a frequency band in the coefficient array
17
+ * Compose stateful stage factories once and return the reusable frame path.
52
18
  *
53
- * @param {number} bandIndex - Band index (0=low, 1=mid, 2=high)
54
- * @returns {number} Starting offset in the 512-sample coefficient array
19
+ * @param {*} context Shared stage ownership and persistent state.
20
+ * @param {...Function} stages Ordered stage factories.
21
+ * @returns {Function} Reusable composed frame operation.
55
22
  */
56
- export function calculateBandOffset(bandIndex) {
57
- return bandIndex === 0 ? 0 : bandIndex === 1 ? 128 : 256
58
- }
59
-
60
- /**
61
- * Extracts coefficients for a specific frequency band from the full coefficient array
62
- *
63
- * @param {Float32Array} coefficients - Full 512-sample coefficient array
64
- * @param {number} bandIndex - Band index (0=low, 1=mid, 2=high)
65
- * @returns {Float32Array} Subarray containing coefficients for the specified band
66
- */
67
- export function extractBandCoefficients(coefficients, bandIndex) {
68
- const offsets = [0, 128, 256, 512]
69
- return coefficients.subarray(offsets[bandIndex], offsets[bandIndex + 1])
23
+ export function pipe(context, ...stages) {
24
+ const operations = stages.map((stage) => stage(context))
25
+ return (input) =>
26
+ operations.reduce((value, operation) => operation(value), input)
70
27
  }