carta1 1.1.10 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/bin/cli.js +8 -4
  2. package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
  3. package/codec/allocation/source.js +69 -0
  4. package/codec/allocation/stage.js +38 -0
  5. package/codec/analysis/bands.js +68 -0
  6. package/codec/analysis/{transient.js → blocks.js} +90 -47
  7. package/codec/analysis/spectrum.js +160 -0
  8. package/codec/analysis/stage.js +32 -0
  9. package/codec/{browser/interface.js → boundary/browser.js} +1 -1
  10. package/codec/boundary/container.js +96 -0
  11. package/codec/boundary/pcm.js +193 -0
  12. package/codec/boundary/processor.js +208 -0
  13. package/codec/{io/readers.js → boundary/reader.js} +2 -1
  14. package/codec/boundary/stream.js +361 -0
  15. package/codec/{browser → boundary}/worker.js +45 -42
  16. package/codec/core/constants.js +19 -132
  17. package/codec/core/geometry.js +25 -0
  18. package/codec/core/tables.js +215 -0
  19. package/codec/dequantization.js +54 -0
  20. package/codec/index.js +12 -13
  21. package/codec/pipeline/decoder.js +10 -399
  22. package/codec/pipeline/encoder.js +12 -434
  23. package/codec/quantization/spectrum.js +67 -0
  24. package/codec/quantization/stage.js +49 -0
  25. package/codec/signal/fft.js +68 -0
  26. package/codec/{transforms → signal}/mdct.js +37 -65
  27. package/codec/signal/order.js +16 -0
  28. package/codec/signal/qmf.js +539 -0
  29. package/codec/state.js +112 -0
  30. package/codec/{io → syntax}/bitstream.js +27 -8
  31. package/codec/{io/serialization.js → syntax/frame.js} +5 -101
  32. package/codec/synthesis/bands.js +228 -0
  33. package/codec/synthesis/pcm.js +64 -0
  34. package/codec/synthesis/stage.js +17 -0
  35. package/codec/utils.js +9 -52
  36. package/dist/carta1-worker-interface.min.js.map +1 -1
  37. package/dist/carta1-worker.min.js +1 -1
  38. package/dist/carta1-worker.min.js.map +1 -1
  39. package/dist/carta1.min.js +1 -1
  40. package/dist/carta1.min.js.map +1 -1
  41. package/package.json +1 -1
  42. package/codec/coding/quantization.js +0 -149
  43. package/codec/core/buffers.js +0 -81
  44. package/codec/io/processor.js +0 -671
  45. package/codec/transforms/fft.js +0 -69
  46. package/codec/transforms/qmf.js +0 -105
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Carta1 chronological stream adapters.
3
+ */
4
+
5
+ import {
6
+ AEA_HEADER_SIZE,
7
+ SAMPLES_PER_FRAME,
8
+ SOUND_UNIT_SIZE,
9
+ STREAM_FLUSH_TAIL_FRAMES,
10
+ STREAM_PRIMING_SAMPLES,
11
+ } from '../core/constants.js'
12
+ import { EncoderOptions } from '../core/options.js'
13
+ import { decode } from '../pipeline/decoder.js'
14
+ import { encode } from '../pipeline/encoder.js'
15
+ import { deserializeFrame, serializeFrame } from '../syntax/frame.js'
16
+ import { AeaFile } from './container.js'
17
+ import { frameBufferToFrames, joinChannelFrames } from './pcm.js'
18
+
19
+ /**
20
+ * Validate a supported channel mode.
21
+ *
22
+ * @param {number} channelCount
23
+ * @throws {Error} If the channel count is unsupported
24
+ */
25
+ function validateChannels(channelCount) {
26
+ if (channelCount !== 1 && channelCount !== 2) {
27
+ throw new Error(`Unsupported channel count: ${channelCount}`)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Validate one PCM frame in the selected channel mode.
33
+ *
34
+ * @param {object} frame
35
+ * @param {number} channelCount
36
+ * @throws {TypeError} If the frame does not match the selected mode
37
+ * @throws {RangeError} If a selected channel is not one complete frame
38
+ */
39
+ function validateChunk(frame, channelCount) {
40
+ const channels = channelCount === 1 ? [frame] : frame
41
+ if (
42
+ !Array.isArray(channels) ||
43
+ channels.length !== channelCount ||
44
+ channels.some((channel) => !(channel instanceof Float32Array))
45
+ ) {
46
+ throw new TypeError(
47
+ `ATRAC1 streaming requires ${channelCount} Float32 channel${
48
+ channelCount === 1 ? '' : 's'
49
+ } per frame`
50
+ )
51
+ }
52
+
53
+ if (channels.some((channel) => channel.length !== SAMPLES_PER_FRAME)) {
54
+ throw new RangeError(
55
+ `ATRAC1 streaming requires exactly ${SAMPLES_PER_FRAME} samples per channel`
56
+ )
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Create an empty structured frame for an unmatched stereo channel.
62
+ *
63
+ * @returns {object}
64
+ */
65
+ function createDummyFrame() {
66
+ return {
67
+ nBfu: 0,
68
+ blockModes: [0, 0, 0],
69
+ scaleFactorIndices: new Int32Array(0),
70
+ wordLengthIndices: new Int32Array(0),
71
+ quantizedCoefficients: [],
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Collect an async iterable.
77
+ *
78
+ * @param {AsyncIterable} frames
79
+ * @returns {Promise<Array>}
80
+ */
81
+ async function collect(frames) {
82
+ const output = []
83
+ for await (const frame of frames) {
84
+ output.push(frame)
85
+ }
86
+ return output
87
+ }
88
+
89
+ /**
90
+ * Encode chronological PCM frames.
91
+ */
92
+ export class AeaStreamingEncoder {
93
+ /**
94
+ * @param {object} [options]
95
+ */
96
+ constructor(options = {}) {
97
+ const { channelCount = 1, onProgress, encoderOptions } = options
98
+ validateChannels(channelCount)
99
+
100
+ const codecOptions = encoderOptions || new EncoderOptions()
101
+ this.channelCount = channelCount
102
+ this.onProgress = onProgress
103
+ this.frameIndex = 0
104
+ this.sampleCount = 0
105
+ this.finalized = false
106
+ this.leftEncoder = encode(codecOptions)
107
+ this.rightEncoder = channelCount === 2 ? encode(codecOptions) : null
108
+ }
109
+
110
+ /**
111
+ * @param {AsyncIterable<object>} frames
112
+ * @returns {AsyncGenerator<object>}
113
+ */
114
+ async *frames(frames) {
115
+ if (this.finalized) {
116
+ throw new Error('ATRAC1 encoder has already been finalized')
117
+ }
118
+ for await (const frame of frames) {
119
+ validateChunk(frame, this.channelCount)
120
+ if (this.channelCount === 1) {
121
+ yield this.leftEncoder(frame)
122
+ } else {
123
+ const [left, right] = frame
124
+ yield this.leftEncoder(left)
125
+ yield this.rightEncoder(right)
126
+ }
127
+
128
+ this.sampleCount += SAMPLES_PER_FRAME
129
+
130
+ if (this.onProgress) {
131
+ this.onProgress(this.frameIndex++)
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Flush the synthesis tail after a nonempty stream.
138
+ *
139
+ * @returns {Generator<object>}
140
+ */
141
+ *finish() {
142
+ if (this.finalized) return
143
+ this.finalized = true
144
+ if (this.sampleCount === 0) return
145
+ const silence = new Float32Array(SAMPLES_PER_FRAME)
146
+ for (let frame = 0; frame < STREAM_FLUSH_TAIL_FRAMES; frame++) {
147
+ yield this.leftEncoder(silence)
148
+ if (this.channelCount === 2) yield this.rightEncoder(silence)
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Decode chronological structured frames.
155
+ */
156
+ export class AeaStreamingDecoder {
157
+ /**
158
+ * @param {object} [options]
159
+ */
160
+ constructor(options = {}) {
161
+ const {
162
+ channelCount = 1,
163
+ onProgress,
164
+ primingSampleCount = 0,
165
+ sampleCount = Number.POSITIVE_INFINITY,
166
+ } = options
167
+ validateChannels(channelCount)
168
+
169
+ this.channelCount = channelCount
170
+ this.onProgress = onProgress
171
+ this.frameIndex = 0
172
+ this.primingSampleCount = primingSampleCount
173
+ this.sampleCount = sampleCount
174
+ this.timelinePosition = 0
175
+ this.emittedSamples = 0
176
+ this.leftFrame = null
177
+ this.leftDecoder = decode()
178
+ this.rightDecoder = channelCount === 2 ? decode() : null
179
+ }
180
+
181
+ /**
182
+ * @param {AsyncIterable<object>} frames
183
+ * @returns {AsyncGenerator<object>}
184
+ */
185
+ async *frames(frames) {
186
+ for await (const frame of frames) {
187
+ if (this.channelCount === 1) {
188
+ yield this.trim(this.leftDecoder(frame))
189
+ this.reportProgress()
190
+ } else if (this.leftFrame === null) {
191
+ this.leftFrame = frame
192
+ } else {
193
+ yield this.trim([
194
+ this.leftDecoder(this.leftFrame),
195
+ this.rightDecoder(frame),
196
+ ])
197
+ this.leftFrame = null
198
+ this.reportProgress()
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Complete an unmatched stereo pair.
205
+ *
206
+ * @returns {Generator<[Float32Array, Float32Array]>}
207
+ */
208
+ *finish() {
209
+ if (this.channelCount === 2 && this.leftFrame !== null) {
210
+ yield this.trim([
211
+ this.leftDecoder(this.leftFrame),
212
+ this.rightDecoder(createDummyFrame()),
213
+ ])
214
+ this.leftFrame = null
215
+ this.reportProgress()
216
+ }
217
+ if (
218
+ Number.isFinite(this.sampleCount) &&
219
+ this.emittedSamples !== this.sampleCount
220
+ ) {
221
+ throw new RangeError(
222
+ `Truncated ATRAC1 timeline: decoded ${this.emittedSamples} of ${this.sampleCount} samples`
223
+ )
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Apply stream priming and visible-length trimming once per PCM frame.
229
+ *
230
+ * @param {object} frame
231
+ * @returns {object}
232
+ * @private
233
+ */
234
+ trim(frame) {
235
+ const start = Math.max(0, this.primingSampleCount - this.timelinePosition)
236
+ const remaining = this.sampleCount - this.emittedSamples
237
+ const count = Math.max(0, Math.min(SAMPLES_PER_FRAME - start, remaining))
238
+ this.timelinePosition += SAMPLES_PER_FRAME
239
+ this.emittedSamples += count
240
+ if (this.channelCount === 1) return frame.slice(start, start + count)
241
+ return frame.map((channel) => channel.slice(start, start + count))
242
+ }
243
+
244
+ /**
245
+ * Publish one frame's progress.
246
+ *
247
+ * @private
248
+ */
249
+ reportProgress() {
250
+ if (this.onProgress) {
251
+ this.onProgress(this.frameIndex++)
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Create a chronological encoder.
258
+ *
259
+ * @param {object} [options]
260
+ * @returns {AeaStreamingEncoder}
261
+ */
262
+ export function createAeaStreamingEncoder(options) {
263
+ return new AeaStreamingEncoder(options)
264
+ }
265
+
266
+ /**
267
+ * Create a chronological decoder.
268
+ *
269
+ * @param {object} [options]
270
+ * @returns {AeaStreamingDecoder}
271
+ */
272
+ export function createAeaStreamingDecoder(options) {
273
+ return new AeaStreamingDecoder(options)
274
+ }
275
+
276
+ /**
277
+ * Encode complete planar PCM buffers into an AEA byte image.
278
+ *
279
+ * @param {Float32Array[]} channels
280
+ * @param {object} [options]
281
+ * @returns {Promise<Uint8Array>}
282
+ */
283
+ export async function encodeAeaPcm(channels, options = {}) {
284
+ if (
285
+ !Array.isArray(channels) ||
286
+ (channels.length !== 1 && channels.length !== 2) ||
287
+ channels.some((channel) => !(channel instanceof Float32Array))
288
+ ) {
289
+ throw new TypeError('ATRAC1 encoding requires one or two Float32 channels')
290
+ }
291
+
292
+ const { title = 'encoded by carta1', ...encoderValues } = options
293
+ const encoder = createAeaStreamingEncoder({
294
+ channelCount: channels.length,
295
+ encoderOptions: new EncoderOptions(encoderValues),
296
+ })
297
+ const frames = encoder.frames(frameBufferToFrames(channels))
298
+ const encoded = await collect(frames)
299
+ encoded.push(...encoder.finish())
300
+
301
+ const header = AeaFile.createHeader(
302
+ title,
303
+ encoded.length,
304
+ channels.length,
305
+ channels[0].length,
306
+ STREAM_PRIMING_SAMPLES
307
+ )
308
+ const output = new Uint8Array(
309
+ header.length + encoded.length * SOUND_UNIT_SIZE
310
+ )
311
+ output.set(header)
312
+ encoded.forEach((frame, index) => {
313
+ output.set(serializeFrame(frame), header.length + index * SOUND_UNIT_SIZE)
314
+ })
315
+ return output
316
+ }
317
+
318
+ /**
319
+ * Decode a complete AEA byte image into planar normalized PCM buffers.
320
+ *
321
+ * @param {Uint8Array|ArrayBuffer|Blob} input
322
+ * @returns {Promise<Float32Array[]>}
323
+ */
324
+ export async function decodeAeaPcm(input) {
325
+ let bytes
326
+ if (input instanceof Blob) {
327
+ bytes = new Uint8Array(await input.arrayBuffer())
328
+ } else if (input instanceof Uint8Array) {
329
+ bytes = input
330
+ } else if (input instanceof ArrayBuffer) {
331
+ bytes = new Uint8Array(input)
332
+ } else {
333
+ throw new TypeError('ATRAC1 decoding requires AEA bytes or a Blob')
334
+ }
335
+
336
+ const info = AeaFile.parseHeader(bytes.subarray(0, AEA_HEADER_SIZE))
337
+ const decoder = createAeaStreamingDecoder({
338
+ channelCount: info.channelCount,
339
+ primingSampleCount: info.primingSampleCount,
340
+ sampleCount: info.sampleCount,
341
+ })
342
+ const frames = (async function* () {
343
+ for (
344
+ let offset = AEA_HEADER_SIZE;
345
+ offset + SOUND_UNIT_SIZE <= bytes.length;
346
+ offset += SOUND_UNIT_SIZE
347
+ ) {
348
+ yield deserializeFrame(bytes.subarray(offset, offset + SOUND_UNIT_SIZE))
349
+ }
350
+ })()
351
+ const decoded = await collect(decoder.frames(frames))
352
+ decoded.push(...decoder.finish())
353
+
354
+ if (info.channelCount === 1) {
355
+ return [joinChannelFrames(decoded)]
356
+ }
357
+ return [
358
+ joinChannelFrames(decoded.map(([left]) => left)),
359
+ joinChannelFrames(decoded.map(([, right]) => right)),
360
+ ]
361
+ }
@@ -9,50 +9,11 @@
9
9
  * new Worker('carta1-worker.min.js')
10
10
  */
11
11
 
12
- import { AeaFile } from '../io/serialization.js'
12
+ import { AeaFile } from './container.js'
13
13
  import { EncoderOptions } from '../core/options.js'
14
- import { AudioProcessor } from '../io/processor.js'
14
+ import { AudioProcessor } from './processor.js'
15
15
  import { SAMPLE_RATE } from '../core/constants.js'
16
16
 
17
- /**
18
- * Handle messages from the main thread
19
- *
20
- * Supported message types:
21
- * - 'encode': Encode PCM to ATRAC1
22
- * - 'decode': Decode ATRAC1 to PCM
23
- * - 'parseAea': Parse AEA blob metadata
24
- * - 'getEncoderOptions': Get default encoder options
25
- */
26
- self.onmessage = async (e) => {
27
- const { jobId, type, pcmData, aea, options, title, blob } = e.data
28
-
29
- try {
30
- let result
31
-
32
- switch (type) {
33
- case 'encode':
34
- result = await encodeAtrac(pcmData, options, title)
35
- break
36
- case 'decode':
37
- result = await decodeAtrac(aea)
38
- break
39
- case 'parseAea':
40
- result = await AudioProcessor.parseAeaBlob(blob)
41
- break
42
- case 'getEncoderOptions':
43
- result = getEncoderOptionsMetadata()
44
- break
45
- default:
46
- throw new Error(`Unknown worker message type: ${type}`)
47
- }
48
-
49
- self.postMessage({ jobId, result })
50
- } catch (error) {
51
- console.error('Worker error:', error)
52
- self.postMessage({ jobId, error: error.message })
53
- }
54
- }
55
-
56
17
  /**
57
18
  * Encode PCM audio data to ATRAC1 format
58
19
  *
@@ -79,6 +40,7 @@ async function encodeAtrac(pcmData, options, title) {
79
40
  const aeaBlob = await AudioProcessor.createAeaBlob(frameArray, {
80
41
  title,
81
42
  channelCount,
43
+ sampleCount: pcmData[0].length,
82
44
  })
83
45
 
84
46
  // Return the blob
@@ -125,12 +87,14 @@ async function decodeAtrac(aea) {
125
87
  const encodedFrameStream = AudioProcessor.deserializedFrameStream(aeaData)
126
88
  const decodedFrames = AudioProcessor.decodeStream(encodedFrameStream, {
127
89
  channelCount: decodingInfo.channelCount,
90
+ primingSampleCount: decodingInfo.primingSampleCount,
91
+ sampleCount: decodingInfo.sampleCount,
128
92
  })
129
93
 
130
94
  const pcmFrames = await AudioProcessor.collectFrames(decodedFrames)
131
95
 
132
96
  // Create WAV blob directly
133
- const wavBlob = AudioProcessor.createWavBlob(
97
+ const wavBlob = AudioProcessor.createPcmWaveBlob(
134
98
  pcmFrames,
135
99
  decodingInfo.channelCount,
136
100
  decodingInfo.sampleRate
@@ -138,3 +102,42 @@ async function decodeAtrac(aea) {
138
102
 
139
103
  return { wavBlob, info: decodingInfo }
140
104
  }
105
+
106
+ /**
107
+ * Handle messages from the main thread
108
+ *
109
+ * Supported message types:
110
+ * - 'encode': Encode PCM to ATRAC1
111
+ * - 'decode': Decode ATRAC1 to PCM
112
+ * - 'parseAea': Parse AEA blob metadata
113
+ * - 'getEncoderOptions': Get default encoder options
114
+ */
115
+ self.onmessage = async (e) => {
116
+ const { jobId, type, pcmData, aea, options, title, blob } = e.data
117
+
118
+ try {
119
+ let result
120
+
121
+ switch (type) {
122
+ case 'encode':
123
+ result = await encodeAtrac(pcmData, options, title)
124
+ break
125
+ case 'decode':
126
+ result = await decodeAtrac(aea)
127
+ break
128
+ case 'parseAea':
129
+ result = await AudioProcessor.parseAeaBlob(blob)
130
+ break
131
+ case 'getEncoderOptions':
132
+ result = getEncoderOptionsMetadata()
133
+ break
134
+ default:
135
+ throw new Error(`Unknown worker message type: ${type}`)
136
+ }
137
+
138
+ self.postMessage({ jobId, result })
139
+ } catch (error) {
140
+ console.error('Worker error:', error)
141
+ self.postMessage({ jobId, error: error.message })
142
+ }
143
+ }
@@ -1,128 +1,56 @@
1
1
  /**
2
- * Carta1 Audio Codec - Core Constants
2
+ * Carta1 scalar format constants.
3
3
  */
4
4
 
5
- // Audio format
6
5
  export const SAMPLE_RATE = 44100
7
6
  export const SAMPLES_PER_FRAME = 512
8
7
  export const FRAME_RATE = SAMPLE_RATE / SAMPLES_PER_FRAME
9
8
 
10
- // AEA file format
11
- export const AEA_MAGIC = new Uint8Array([0x00, 0x08, 0x00, 0x00])
12
9
  export const AEA_HEADER_SIZE = 2048
13
10
  export const AEA_TITLE_OFFSET = 4
14
11
  export const AEA_TITLE_SIZE = 256
15
12
  export const AEA_FRAME_COUNT_OFFSET = 260
16
13
  export const AEA_CHANNEL_COUNT_OFFSET = 264
14
+ export const AEA_PRIMING_SAMPLE_COUNT_OFFSET = 268
15
+ export const AEA_SOURCE_SAMPLE_COUNT_OFFSET = 272
16
+
17
+ export const STREAM_PRIMING_SAMPLES = 266
18
+ export const STREAM_FLUSH_TAIL_FRAMES = 1
17
19
 
18
- // Frame structure
19
20
  export const SOUND_UNIT_SIZE = 212
20
21
  export const FRAME_BITS = SOUND_UNIT_SIZE * 8
21
22
  export const FRAME_OVERHEAD_BITS = 40
22
23
  export const BITRATE_PER_CHANNEL = SOUND_UNIT_SIZE * FRAME_RATE * 8
23
24
 
24
- // BFU (Block Floating Unit) configuration
25
25
  export const NUM_BFUS = 52
26
26
  export const MAX_BFU_SIZE = 20
27
27
  export const BITS_PER_BFU_METADATA = 10
28
-
29
- export const SPECS_PER_BFU = new Int32Array([
30
- 8, 8, 8, 8, 4, 4, 4, 4, 8, 8, 8, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7,
31
- 7, 7, 9, 9, 9, 9, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 20, 20, 20,
32
- 20, 20, 20, 20, 20,
33
- ])
34
-
35
28
  export const BFU_AMOUNTS_COUNT = 8
36
- export const BFU_AMOUNTS = new Int32Array([20, 28, 32, 36, 40, 44, 48, 52])
37
- export const BFU_BAND_BOUNDARIES = new Int32Array([20, 36, 52])
38
29
 
39
- // BFU start positions
40
- export const BFU_START_LONG = new Int32Array([
41
- 0, 8, 16, 24, 32, 36, 40, 44, 48, 56, 64, 72, 80, 86, 92, 98, 104, 110, 116,
42
- 122, 128, 134, 140, 146, 152, 159, 166, 173, 180, 189, 198, 207, 216, 226,
43
- 236, 246, 256, 268, 280, 292, 304, 316, 328, 340, 352, 372, 392, 412, 432,
44
- 452, 472, 492,
45
- ])
46
-
47
- export const BFU_START_SHORT = new Int32Array([
48
- 0, 32, 64, 96, 8, 40, 72, 104, 12, 44, 76, 108, 20, 52, 84, 116, 26, 58, 90,
49
- 122, 128, 160, 192, 224, 134, 166, 198, 230, 141, 173, 205, 237, 150, 182,
50
- 214, 246, 256, 288, 320, 352, 384, 416, 448, 480, 268, 300, 332, 364, 396,
51
- 428, 460, 492,
52
- ])
53
-
54
- // Transform sizes
55
30
  export const MDCT_SIZE_SHORT = 64
56
31
  export const MDCT_SIZE_MID = 256
57
32
  export const MDCT_SIZE_LONG = 512
33
+ export const MDCT_SHORT_BLOCK_SIZE = 32
34
+ export const MDCT_OVERLAP_SIZE = 32
35
+ export const MDCT_TAIL_WINDOW_SIZE = 16
58
36
 
59
- // Window functions
60
- export const WINDOW_SHORT = (() => {
61
- const table = new Float64Array(32)
62
- for (let i = 0; i < 32; i++) {
63
- table[i] = Math.sin(((i + 0.5) * Math.PI) / 64)
64
- }
65
- return table
66
- })()
67
-
68
- // QMF configuration
37
+ export const QMF_BANDS = 2
69
38
  export const QMF_TAPS = 48
70
- export const QMF_DELAY = 46
39
+ export const QMF_ANALYSIS_START_SAMPLE = 0
40
+ export const QMF_ANALYSIS_STEP_SAMPLES = QMF_BANDS
41
+ export const QMF_ANALYSIS_WINDOW_SAMPLES = QMF_TAPS
42
+ export const QMF_ANALYSIS_MODULATION_SCALE = 1
43
+ export const QMF_ANALYSIS_TERMS = 24
44
+ export const QMF_ANALYSIS_DELAY_SAMPLES = QMF_TAPS - QMF_BANDS
45
+ export const QMF_SYNTHESIS_DELAY_ROWS = 24
46
+ export const QMF_SYNTHESIS_TERMS = 24
47
+ export const QMF_SYNTHESIS_DELAY_SAMPLES = QMF_BANDS * QMF_SYNTHESIS_DELAY_ROWS
71
48
  export const QMF_HIGH_BAND_DELAY = 39
72
49
 
73
- // QMF prototype filter coefficients
74
- export const QMF_COEFFS = new Float32Array([
75
- -0.00001461907, -0.00009205479, -0.000056157569, 0.00030117269, 0.0002422519,
76
- -0.00085293897, -0.0005205574, 0.0020340169, 0.00078333891, -0.0042153862,
77
- -0.00075614988, 0.0078402944, -0.000061169922, -0.01344162, 0.0024626821,
78
- 0.021736089, -0.007801671, -0.034090221, 0.01880949, 0.054326009,
79
- -0.043596379, -0.099384367, 0.13207909, 0.46424159,
80
- ])
81
-
82
- // QMF window function
83
- export const QMF_WINDOW = (() => {
84
- const window = new Float32Array(QMF_TAPS)
85
- for (let i = 0; i < 24; i++) {
86
- window[i] = QMF_COEFFS[i] * 2.0
87
- window[47 - i] = QMF_COEFFS[i] * 2.0
88
- }
89
- return window
90
- })()
91
-
92
- // QMF even/odd taps for optimization
93
- export const QMF_EVEN = (() => {
94
- const even = new Float32Array(24)
95
- for (let i = 0; i < 24; i++) {
96
- even[i] = QMF_WINDOW[i * 2]
97
- }
98
- return even
99
- })()
100
-
101
- export const QMF_ODD = (() => {
102
- const odd = new Float32Array(24)
103
- for (let i = 0; i < 24; i++) {
104
- odd[i] = QMF_WINDOW[i * 2 + 1]
105
- }
106
- return odd
107
- })()
108
-
109
- // FFT sizes for transient detection
110
50
  export const FFT_SIZE_LOW = 128
111
51
  export const FFT_SIZE_MID = 128
112
52
  export const FFT_SIZE_HIGH = 256
113
53
 
114
- // MDCT/IMDCT transform configuration
115
- export const MDCT_BAND_CONFIGS = [
116
- { size: 128, windowStart: 48 }, // Low band (0-5.5kHz)
117
- { size: 128, windowStart: 48 }, // Mid band (5.5-11kHz)
118
- { size: 256, windowStart: 112 }, // High band (11-22kHz)
119
- ]
120
-
121
- export const MDCT_SHORT_BLOCK_SIZE = 32
122
- export const MDCT_OVERLAP_SIZE = 32
123
- export const MDCT_TAIL_WINDOW_SIZE = 16
124
-
125
- // WAV format
126
54
  export const WAV_HEADER_SIZE = 44
127
55
  export const WAV_BYTES_PER_SAMPLE = 2
128
56
  export const WAV_BITS_PER_SAMPLE = 16
@@ -130,50 +58,9 @@ export const WAV_PCM_MAX_POSITIVE = 0x7fff
130
58
  export const WAV_PCM_MAX_NEGATIVE = 0x8000
131
59
  export const WAV_DATA_OFFSET = 36
132
60
 
133
- // Frame serialization
134
61
  export const FRAME_HEADER_BITS = 16
135
62
  export const FRAME_WORD_LENGTH_BITS = 4
136
63
  export const FRAME_SCALE_FACTOR_BITS = 6
137
64
 
138
- // Quantization
139
65
  export const QUANTIZATION_SIGN_BIT_SHIFT = 1
140
66
  export const MAX_WORD_LENGTH_INDEX = 15
141
- export const WORD_LENGTH_BITS = new Int32Array([
142
- 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
143
- ])
144
- export const SCALE_FACTORS = (() => {
145
- const table = new Float64Array(64)
146
- for (let i = 0; i < 64; i++) {
147
- table[i] = Math.pow(2.0, i / 3.0 - 21)
148
- }
149
- return table
150
- })()
151
-
152
- // Bit allocation
153
- export const INV_POWER_OF_TWO = (() => {
154
- const maxBits = WORD_LENGTH_BITS[MAX_WORD_LENGTH_INDEX]
155
- const table = new Float64Array(maxBits + 1)
156
- for (let b = 0; b <= maxBits; b++) {
157
- table[b] = Math.pow(2, -b)
158
- }
159
- return table
160
- })()
161
-
162
- export const WORD_LENGTH_DELTA_BITS = (() => {
163
- const table = new Int32Array(MAX_WORD_LENGTH_INDEX)
164
- for (let i = 0; i < MAX_WORD_LENGTH_INDEX; i++) {
165
- table[i] = (WORD_LENGTH_BITS[i + 1] - WORD_LENGTH_BITS[i]) | 0
166
- }
167
- return table
168
- })()
169
-
170
- export const DISTORTION_DELTA_FACTORS = (() => {
171
- const table = new Float64Array(MAX_WORD_LENGTH_INDEX)
172
- table[0] = 2.0 - INV_POWER_OF_TWO[WORD_LENGTH_BITS[1]]
173
- for (let i = 1; i < MAX_WORD_LENGTH_INDEX; i++) {
174
- const bits1 = WORD_LENGTH_BITS[i] | 0
175
- const bits2 = WORD_LENGTH_BITS[i + 1] | 0
176
- table[i] = INV_POWER_OF_TWO[bits1] - INV_POWER_OF_TWO[bits2]
177
- }
178
- return table
179
- })()
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Stable ATRAC1 spectral geometry.
3
+ */
4
+
5
+ /**
6
+ * Calculate a band's offset in the 512-coefficient spectrum.
7
+ *
8
+ * @param {number} bandIndex
9
+ * @returns {number}
10
+ */
11
+ export function calculateBandOffset(bandIndex) {
12
+ return bandIndex === 0 ? 0 : bandIndex === 1 ? 128 : 256
13
+ }
14
+
15
+ /**
16
+ * Extract one band from the full coefficient spectrum.
17
+ *
18
+ * @param {Float32Array} coefficients
19
+ * @param {number} bandIndex
20
+ * @returns {Float32Array}
21
+ */
22
+ export function extractBandCoefficients(coefficients, bandIndex) {
23
+ const offsets = [0, 128, 256, 512]
24
+ return coefficients.subarray(offsets[bandIndex], offsets[bandIndex + 1])
25
+ }