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.
- package/bin/cli.js +8 -4
- package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
- package/codec/allocation/source.js +69 -0
- package/codec/allocation/stage.js +38 -0
- package/codec/analysis/bands.js +68 -0
- package/codec/analysis/{transient.js → blocks.js} +90 -47
- package/codec/analysis/spectrum.js +160 -0
- package/codec/analysis/stage.js +32 -0
- package/codec/{browser/interface.js → boundary/browser.js} +1 -1
- package/codec/boundary/container.js +96 -0
- package/codec/boundary/pcm.js +193 -0
- package/codec/boundary/processor.js +208 -0
- package/codec/{io/readers.js → boundary/reader.js} +2 -1
- package/codec/boundary/stream.js +361 -0
- package/codec/{browser → boundary}/worker.js +45 -42
- package/codec/core/constants.js +19 -132
- package/codec/core/geometry.js +25 -0
- package/codec/core/tables.js +215 -0
- package/codec/dequantization.js +54 -0
- package/codec/index.js +12 -13
- package/codec/pipeline/decoder.js +10 -399
- package/codec/pipeline/encoder.js +12 -434
- package/codec/quantization/spectrum.js +67 -0
- package/codec/quantization/stage.js +49 -0
- package/codec/signal/fft.js +68 -0
- package/codec/{transforms → signal}/mdct.js +37 -65
- package/codec/signal/order.js +16 -0
- package/codec/signal/qmf.js +539 -0
- package/codec/state.js +112 -0
- package/codec/{io → syntax}/bitstream.js +27 -8
- package/codec/{io/serialization.js → syntax/frame.js} +5 -101
- package/codec/synthesis/bands.js +228 -0
- package/codec/synthesis/pcm.js +64 -0
- package/codec/synthesis/stage.js +17 -0
- package/codec/utils.js +9 -52
- package/dist/carta1-worker-interface.min.js.map +1 -1
- package/dist/carta1-worker.min.js +1 -1
- package/dist/carta1-worker.min.js.map +1 -1
- package/dist/carta1.min.js +1 -1
- package/dist/carta1.min.js.map +1 -1
- package/package.json +1 -1
- package/codec/coding/quantization.js +0 -149
- package/codec/core/buffers.js +0 -81
- package/codec/io/processor.js +0 -671
- package/codec/transforms/fft.js +0 -69
- 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 '
|
|
12
|
+
import { AeaFile } from './container.js'
|
|
13
13
|
import { EncoderOptions } from '../core/options.js'
|
|
14
|
-
import { AudioProcessor } from '
|
|
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.
|
|
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
|
+
}
|
package/codec/core/constants.js
CHANGED
|
@@ -1,128 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carta1
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|