carta1 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +129 -0
- package/bin/cli.js +679 -0
- package/codec/analysis/psychoacoustics.js +589 -0
- package/codec/analysis/transient.js +226 -0
- package/codec/browser/interface.js +188 -0
- package/codec/browser/worker.js +140 -0
- package/codec/coding/bitallocation.js +407 -0
- package/codec/coding/quantization.js +150 -0
- package/codec/core/bitstream.js +82 -0
- package/codec/core/buffers.js +99 -0
- package/codec/core/constants.js +321 -0
- package/codec/core/options.js +151 -0
- package/codec/index.js +27 -0
- package/codec/io/processor.js +566 -0
- package/codec/io/readers.js +56 -0
- package/codec/io/serialization.js +254 -0
- package/codec/pipeline/decoder.js +278 -0
- package/codec/pipeline/encoder.js +369 -0
- package/codec/transforms/fft.js +69 -0
- package/codec/transforms/mdct.js +225 -0
- package/codec/transforms/qmf.js +127 -0
- package/codec/utils.js +151 -0
- package/dist/carta1-worker-interface.min.js +2 -0
- package/dist/carta1-worker-interface.min.js.map +1 -0
- package/dist/carta1-worker.min.js +2 -0
- package/dist/carta1-worker.min.js.map +1 -0
- package/dist/carta1.min.js +2 -0
- package/dist/carta1.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - Audio Processing Module
|
|
3
|
+
*
|
|
4
|
+
* High-level audio processing interface that provides stream-based encoding and decoding
|
|
5
|
+
* for ATRAC1 audio format. Handles both mono and stereo audio processing with proper
|
|
6
|
+
* delay compensation and frame management.
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Stream-based processing for large audio files
|
|
10
|
+
* - Automatic delay compensation for codec processing
|
|
11
|
+
* - Support for mono and stereo audio
|
|
12
|
+
* - Progress tracking callbacks
|
|
13
|
+
* - AEA file format creation and parsing
|
|
14
|
+
* - WAV file generation from decoded audio
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { encode } from '../pipeline/encoder.js'
|
|
18
|
+
import { decode } from '../pipeline/decoder.js'
|
|
19
|
+
import { EncoderOptions } from '../core/options.js'
|
|
20
|
+
import {
|
|
21
|
+
SAMPLES_PER_FRAME,
|
|
22
|
+
SAMPLE_RATE,
|
|
23
|
+
AEA_HEADER_SIZE,
|
|
24
|
+
SOUND_UNIT_SIZE,
|
|
25
|
+
WAV_HEADER_SIZE,
|
|
26
|
+
WAV_BYTES_PER_SAMPLE,
|
|
27
|
+
WAV_BITS_PER_SAMPLE,
|
|
28
|
+
WAV_PCM_MAX_POSITIVE,
|
|
29
|
+
WAV_PCM_MAX_NEGATIVE,
|
|
30
|
+
WAV_DATA_OFFSET,
|
|
31
|
+
} from '../core/constants.js'
|
|
32
|
+
import { serializeFrame, deserializeFrame, AeaFile } from './serialization.js'
|
|
33
|
+
import {
|
|
34
|
+
withFlushSamples,
|
|
35
|
+
withDelayCompensation,
|
|
36
|
+
withStereoFlushSamples,
|
|
37
|
+
withStereoDelayCompensation,
|
|
38
|
+
} from '../utils.js'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Audio processing utility class providing high-level encoding and decoding operations
|
|
42
|
+
*/
|
|
43
|
+
export class AudioProcessor {
|
|
44
|
+
/**
|
|
45
|
+
* Encodes a stream of audio frames to ATRAC1 format
|
|
46
|
+
* @param {AsyncIterable<Float32Array>|AsyncIterable<[Float32Array, Float32Array]>} audioFrames - Audio frame stream
|
|
47
|
+
* @param {Object} [options={}] - Encoding options
|
|
48
|
+
* @param {number} [options.channelCount=1] - Number of audio channels (1 or 2)
|
|
49
|
+
* @param {Function} [options.onProgress] - Progress callback function
|
|
50
|
+
* @param {EncoderOptions} [options.encoderOptions] - Encoder configuration options
|
|
51
|
+
* @returns {AsyncGenerator<Object>} Stream of encoded frame data
|
|
52
|
+
* @throws {Error} If unsupported channel count is provided
|
|
53
|
+
*/
|
|
54
|
+
static async *encodeStream(audioFrames, options = {}) {
|
|
55
|
+
const { channelCount = 1, onProgress, encoderOptions } = options
|
|
56
|
+
|
|
57
|
+
if (channelCount === 1) {
|
|
58
|
+
yield* AudioProcessor._encodeMonoStream(
|
|
59
|
+
audioFrames,
|
|
60
|
+
onProgress,
|
|
61
|
+
encoderOptions
|
|
62
|
+
)
|
|
63
|
+
} else if (channelCount === 2) {
|
|
64
|
+
yield* AudioProcessor._encodeStereoStream(
|
|
65
|
+
audioFrames,
|
|
66
|
+
onProgress,
|
|
67
|
+
encoderOptions
|
|
68
|
+
)
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Internal method for encoding mono audio streams
|
|
76
|
+
* @param {AsyncIterable<Float32Array>} audioFrames - Mono audio frame stream
|
|
77
|
+
* @param {Function} onProgress - Progress callback function
|
|
78
|
+
* @param {EncoderOptions} encoderOptions - Encoder configuration
|
|
79
|
+
* @returns {AsyncGenerator<Object>} Stream of encoded frame data
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
static async *_encodeMonoStream(audioFrames, onProgress, encoderOptions) {
|
|
83
|
+
const options = encoderOptions || new EncoderOptions()
|
|
84
|
+
const encoder = encode(options)
|
|
85
|
+
let frameIndex = 0
|
|
86
|
+
for await (const frame of withFlushSamples(audioFrames)) {
|
|
87
|
+
const result = encoder(frame)
|
|
88
|
+
yield result
|
|
89
|
+
|
|
90
|
+
if (onProgress) {
|
|
91
|
+
onProgress(frameIndex++)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Internal method for encoding stereo audio streams
|
|
98
|
+
* @param {AsyncIterable<[Float32Array, Float32Array]>} audioFrames - Stereo audio frame stream
|
|
99
|
+
* @param {Function} onProgress - Progress callback function
|
|
100
|
+
* @param {EncoderOptions} encoderOptions - Encoder configuration
|
|
101
|
+
* @returns {AsyncGenerator<Object>} Stream of encoded frame data (left then right)
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
static async *_encodeStereoStream(audioFrames, onProgress, encoderOptions) {
|
|
105
|
+
const options = encoderOptions || new EncoderOptions()
|
|
106
|
+
const leftEncoder = encode(options)
|
|
107
|
+
const rightEncoder = encode(options)
|
|
108
|
+
let frameIndex = 0
|
|
109
|
+
|
|
110
|
+
for await (const [leftFrame, rightFrame] of withStereoFlushSamples(
|
|
111
|
+
audioFrames
|
|
112
|
+
)) {
|
|
113
|
+
const leftResult = leftEncoder(leftFrame)
|
|
114
|
+
const rightResult = rightEncoder(rightFrame)
|
|
115
|
+
|
|
116
|
+
yield leftResult
|
|
117
|
+
yield rightResult
|
|
118
|
+
|
|
119
|
+
if (onProgress) {
|
|
120
|
+
onProgress(frameIndex++)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Decodes a stream of ATRAC1 encoded frames back to PCM audio
|
|
127
|
+
* @param {AsyncIterable<Object>} encodedFrames - Stream of encoded frame data
|
|
128
|
+
* @param {Object} [options={}] - Decoding options
|
|
129
|
+
* @param {number} [options.channelCount=1] - Number of audio channels (1 or 2)
|
|
130
|
+
* @param {Function} [options.onProgress] - Progress callback function
|
|
131
|
+
* @returns {AsyncGenerator<Float32Array>|AsyncGenerator<[Float32Array, Float32Array]>} Stream of decoded PCM frames
|
|
132
|
+
* @throws {Error} If unsupported channel count is provided
|
|
133
|
+
*/
|
|
134
|
+
static async *decodeStream(encodedFrames, options = {}) {
|
|
135
|
+
const { channelCount = 1, onProgress } = options
|
|
136
|
+
|
|
137
|
+
if (channelCount === 1) {
|
|
138
|
+
yield* AudioProcessor._decodeMonoStream(encodedFrames, onProgress)
|
|
139
|
+
} else if (channelCount === 2) {
|
|
140
|
+
yield* AudioProcessor._decodeStereoStream(encodedFrames, onProgress)
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Internal method for decoding mono audio streams
|
|
148
|
+
* @param {AsyncIterable<Object>} encodedFrames - Mono encoded frame stream
|
|
149
|
+
* @param {Function} onProgress - Progress callback function
|
|
150
|
+
* @returns {AsyncGenerator<Float32Array>} Stream of decoded mono PCM frames
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
static async *_decodeMonoStream(encodedFrames, onProgress) {
|
|
154
|
+
const decoder = decode()
|
|
155
|
+
let frameIndex = 0
|
|
156
|
+
|
|
157
|
+
// Create raw PCM frame stream
|
|
158
|
+
const rawPcmFrames = async function* () {
|
|
159
|
+
for await (const frame of encodedFrames) {
|
|
160
|
+
const result = decoder(frame)
|
|
161
|
+
yield result
|
|
162
|
+
|
|
163
|
+
if (onProgress) {
|
|
164
|
+
onProgress(frameIndex++)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Always apply delay compensation
|
|
170
|
+
yield* withDelayCompensation(rawPcmFrames())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal method for decoding stereo audio streams
|
|
175
|
+
* @param {AsyncIterable<Object>} encodedFrames - Stereo encoded frame stream (interleaved left/right)
|
|
176
|
+
* @param {Function} onProgress - Progress callback function
|
|
177
|
+
* @returns {AsyncGenerator<[Float32Array, Float32Array]>} Stream of decoded stereo PCM frame pairs
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
static async *_decodeStereoStream(encodedFrames, onProgress) {
|
|
181
|
+
const leftDecoder = decode()
|
|
182
|
+
const rightDecoder = decode()
|
|
183
|
+
let frameIndex = 0
|
|
184
|
+
|
|
185
|
+
const rawPcmFrames = async function* () {
|
|
186
|
+
let leftFrame = null
|
|
187
|
+
let isLeftChannel = true
|
|
188
|
+
|
|
189
|
+
for await (const frame of encodedFrames) {
|
|
190
|
+
if (isLeftChannel) {
|
|
191
|
+
leftFrame = frame
|
|
192
|
+
} else {
|
|
193
|
+
const rightFrame = frame
|
|
194
|
+
|
|
195
|
+
// Decode and yield the pair
|
|
196
|
+
const leftResult = leftDecoder(leftFrame)
|
|
197
|
+
const rightResult = rightDecoder(rightFrame)
|
|
198
|
+
|
|
199
|
+
yield [leftResult, rightResult]
|
|
200
|
+
|
|
201
|
+
if (onProgress) {
|
|
202
|
+
onProgress(frameIndex++)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
isLeftChannel = !isLeftChannel
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!isLeftChannel && leftFrame) {
|
|
210
|
+
const rightFrame = AudioProcessor._createDummyFrame()
|
|
211
|
+
const leftResult = leftDecoder(leftFrame)
|
|
212
|
+
const rightResult = rightDecoder(rightFrame)
|
|
213
|
+
|
|
214
|
+
yield [leftResult, rightResult]
|
|
215
|
+
|
|
216
|
+
if (onProgress) {
|
|
217
|
+
onProgress(frameIndex++)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Always apply stereo delay compensation
|
|
223
|
+
yield* withStereoDelayCompensation(rawPcmFrames())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Converts channel buffer arrays to frame-based stream format
|
|
228
|
+
* @param {Float32Array[]} buffers - Array of channel buffers (1 for mono, 2 for stereo)
|
|
229
|
+
* @param {number} [frameSize=SAMPLES_PER_FRAME] - Frame size in samples
|
|
230
|
+
* @returns {Generator<Float32Array>|Generator<[Float32Array, Float32Array]>} Stream of audio frames
|
|
231
|
+
* @throws {Error} If unsupported channel count is provided
|
|
232
|
+
*/
|
|
233
|
+
static *frameBufferToFrames(buffers, frameSize = SAMPLES_PER_FRAME) {
|
|
234
|
+
const channelCount = buffers.length
|
|
235
|
+
|
|
236
|
+
if (channelCount === 1) {
|
|
237
|
+
const [left] = buffers
|
|
238
|
+
for (let i = 0; i < left.length; i += frameSize) {
|
|
239
|
+
const frame = new Float32Array(frameSize)
|
|
240
|
+
const end = Math.min(i + frameSize, left.length)
|
|
241
|
+
for (let j = 0; j < end - i; j++) {
|
|
242
|
+
frame[j] = left[i + j]
|
|
243
|
+
}
|
|
244
|
+
yield frame
|
|
245
|
+
}
|
|
246
|
+
} else if (channelCount === 2) {
|
|
247
|
+
const [left, right] = buffers
|
|
248
|
+
const maxLength = Math.max(left.length, right.length)
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < maxLength; i += frameSize) {
|
|
251
|
+
const leftFrame = new Float32Array(frameSize)
|
|
252
|
+
const rightFrame = new Float32Array(frameSize)
|
|
253
|
+
|
|
254
|
+
const end = Math.min(i + frameSize, maxLength)
|
|
255
|
+
|
|
256
|
+
for (let j = 0; j < end - i; j++) {
|
|
257
|
+
leftFrame[j] = i + j < left.length ? left[i + j] : 0
|
|
258
|
+
rightFrame[j] = i + j < right.length ? right[i + j] : 0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
yield [leftFrame, rightFrame]
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Collects all frames from a stream into an array
|
|
270
|
+
* @param {AsyncIterable} frameStream - Stream of frames to collect
|
|
271
|
+
* @returns {Promise<Array>} Array of all frames from the stream
|
|
272
|
+
*/
|
|
273
|
+
static async collectFrames(frameStream) {
|
|
274
|
+
const frames = []
|
|
275
|
+
for await (const frame of frameStream) {
|
|
276
|
+
frames.push(frame)
|
|
277
|
+
}
|
|
278
|
+
return frames
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Creates a dummy frame for padding purposes
|
|
283
|
+
* @returns {Object} Empty frame structure
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
286
|
+
static _createDummyFrame() {
|
|
287
|
+
return {
|
|
288
|
+
nBfu: 0,
|
|
289
|
+
blockSizeMode: [0, 0, 0],
|
|
290
|
+
scaleFactorIndices: new Int32Array(0),
|
|
291
|
+
wordLengthIndices: new Int32Array(0),
|
|
292
|
+
quantizedCoefficients: [],
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Creates an AEA file blob from encoded frames
|
|
298
|
+
* @param {AsyncIterable<Object>} encodedFrames - Stream of encoded frame data
|
|
299
|
+
* @param {Object} [options={}] - AEA file options
|
|
300
|
+
* @param {string} [options.title='encoded by atrac1.js'] - File title
|
|
301
|
+
* @param {number} [options.channelCount=1] - Number of audio channels
|
|
302
|
+
* @returns {Promise<Blob>} AEA file blob
|
|
303
|
+
*/
|
|
304
|
+
static async createAeaBlob(encodedFrames, options = {}) {
|
|
305
|
+
const { title = 'encoded by atrac1.js', channelCount = 1 } = options
|
|
306
|
+
|
|
307
|
+
const frames = []
|
|
308
|
+
for await (const frame of encodedFrames) {
|
|
309
|
+
frames.push(serializeFrame(frame))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const header = AeaFile.createHeader(title, frames.length, channelCount)
|
|
313
|
+
const totalSize =
|
|
314
|
+
header.length + frames.reduce((sum, frame) => sum + frame.length, 0)
|
|
315
|
+
|
|
316
|
+
const buffer = new Uint8Array(totalSize)
|
|
317
|
+
buffer.set(header, 0)
|
|
318
|
+
|
|
319
|
+
let offset = header.length
|
|
320
|
+
for (const frame of frames) {
|
|
321
|
+
buffer.set(frame, offset)
|
|
322
|
+
offset += frame.length
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return new Blob([buffer], { type: 'application/octet-stream' })
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Creates a WAV file blob from PCM audio frames
|
|
330
|
+
* @param {Float32Array[]|Float32Array|[Float32Array, Float32Array][]} pcmFrames - PCM audio frames
|
|
331
|
+
* @param {number} [channelCount=1] - Number of audio channels
|
|
332
|
+
* @param {number} [sampleRate=SAMPLE_RATE] - Audio sample rate
|
|
333
|
+
* @returns {Blob} WAV file blob
|
|
334
|
+
* @throws {Error} If unsupported channel count is provided
|
|
335
|
+
*/
|
|
336
|
+
static createWavBlob(pcmFrames, channelCount = 1, sampleRate = SAMPLE_RATE) {
|
|
337
|
+
const frames = Array.isArray(pcmFrames) ? pcmFrames : [pcmFrames]
|
|
338
|
+
|
|
339
|
+
if (channelCount === 1) {
|
|
340
|
+
return AudioProcessor._createMonoWavBlob(frames, sampleRate)
|
|
341
|
+
} else if (channelCount === 2) {
|
|
342
|
+
return AudioProcessor._createStereoWavBlob(frames, sampleRate)
|
|
343
|
+
} else {
|
|
344
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates a mono WAV file blob from PCM frames
|
|
350
|
+
* @param {Float32Array[]} frames - Array of mono PCM frames
|
|
351
|
+
* @param {number} sampleRate - Audio sample rate
|
|
352
|
+
* @returns {Blob} Mono WAV file blob
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
static _createMonoWavBlob(frames, sampleRate) {
|
|
356
|
+
const totalSamples = frames.reduce((sum, frame) => sum + frame.length, 0)
|
|
357
|
+
const buffer = new ArrayBuffer(
|
|
358
|
+
WAV_HEADER_SIZE + totalSamples * WAV_BYTES_PER_SAMPLE
|
|
359
|
+
)
|
|
360
|
+
const view = new DataView(buffer)
|
|
361
|
+
|
|
362
|
+
// WAV header
|
|
363
|
+
AudioProcessor._writeWavHeader(view, totalSamples, 1, sampleRate)
|
|
364
|
+
|
|
365
|
+
// PCM data using DataView for consistency
|
|
366
|
+
let offset = WAV_HEADER_SIZE
|
|
367
|
+
for (const frame of frames) {
|
|
368
|
+
for (let i = 0; i < frame.length; i++) {
|
|
369
|
+
const sample = Math.max(-1, Math.min(1, frame[i]))
|
|
370
|
+
view.setInt16(
|
|
371
|
+
offset,
|
|
372
|
+
sample < 0
|
|
373
|
+
? sample * WAV_PCM_MAX_NEGATIVE
|
|
374
|
+
: sample * WAV_PCM_MAX_POSITIVE,
|
|
375
|
+
true
|
|
376
|
+
)
|
|
377
|
+
offset += WAV_BYTES_PER_SAMPLE
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return new Blob([buffer], { type: 'audio/wav' })
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Creates a stereo WAV file blob from PCM frame pairs
|
|
386
|
+
* @param {[Float32Array, Float32Array][]} framePairs - Array of stereo PCM frame pairs
|
|
387
|
+
* @param {number} sampleRate - Audio sample rate
|
|
388
|
+
* @returns {Blob} Stereo WAV file blob
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
static _createStereoWavBlob(framePairs, sampleRate) {
|
|
392
|
+
const totalSamples = framePairs.reduce(
|
|
393
|
+
(sum, [left, right]) => sum + Math.max(left.length, right.length),
|
|
394
|
+
0
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const buffer = new ArrayBuffer(
|
|
398
|
+
WAV_HEADER_SIZE + totalSamples * WAV_BYTES_PER_SAMPLE * 2
|
|
399
|
+
)
|
|
400
|
+
const view = new DataView(buffer)
|
|
401
|
+
|
|
402
|
+
// WAV header
|
|
403
|
+
AudioProcessor._writeWavHeader(view, totalSamples, 2, sampleRate)
|
|
404
|
+
|
|
405
|
+
// Interleaved PCM data using DataView for consistency
|
|
406
|
+
let offset = WAV_HEADER_SIZE
|
|
407
|
+
for (const [leftFrame, rightFrame] of framePairs) {
|
|
408
|
+
const frameLength = Math.max(leftFrame.length, rightFrame.length)
|
|
409
|
+
for (let i = 0; i < frameLength; i++) {
|
|
410
|
+
const leftSample = i < leftFrame.length ? leftFrame[i] : 0
|
|
411
|
+
const rightSample = i < rightFrame.length ? rightFrame[i] : 0
|
|
412
|
+
const leftClipped = Math.max(-1, Math.min(1, leftSample))
|
|
413
|
+
const rightClipped = Math.max(-1, Math.min(1, rightSample))
|
|
414
|
+
view.setInt16(
|
|
415
|
+
offset,
|
|
416
|
+
leftClipped < 0
|
|
417
|
+
? leftClipped * WAV_PCM_MAX_NEGATIVE
|
|
418
|
+
: leftClipped * WAV_PCM_MAX_POSITIVE,
|
|
419
|
+
true
|
|
420
|
+
)
|
|
421
|
+
offset += WAV_BYTES_PER_SAMPLE
|
|
422
|
+
view.setInt16(
|
|
423
|
+
offset,
|
|
424
|
+
rightClipped < 0
|
|
425
|
+
? rightClipped * WAV_PCM_MAX_NEGATIVE
|
|
426
|
+
: rightClipped * WAV_PCM_MAX_POSITIVE,
|
|
427
|
+
true
|
|
428
|
+
)
|
|
429
|
+
offset += WAV_BYTES_PER_SAMPLE
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return new Blob([buffer], { type: 'audio/wav' })
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Writes WAV file header to DataView
|
|
438
|
+
* @param {DataView} view - DataView to write to
|
|
439
|
+
* @param {number} totalSamples - Total number of samples
|
|
440
|
+
* @param {number} channelCount - Number of audio channels
|
|
441
|
+
* @param {number} sampleRate - Audio sample rate
|
|
442
|
+
* @private
|
|
443
|
+
*/
|
|
444
|
+
static _writeWavHeader(view, totalSamples, channelCount, sampleRate) {
|
|
445
|
+
const byteRate = sampleRate * channelCount * WAV_BYTES_PER_SAMPLE
|
|
446
|
+
const blockAlign = channelCount * WAV_BYTES_PER_SAMPLE
|
|
447
|
+
const dataSize = totalSamples * blockAlign
|
|
448
|
+
const fileSize = WAV_DATA_OFFSET + dataSize
|
|
449
|
+
|
|
450
|
+
let pos = 0
|
|
451
|
+
|
|
452
|
+
// Helper function to write ASCII strings
|
|
453
|
+
const writeString = (offset, string) => {
|
|
454
|
+
for (let i = 0; i < string.length; i++) {
|
|
455
|
+
view.setUint8(offset + i, string.charCodeAt(i))
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// RIFF header
|
|
460
|
+
writeString(pos, 'RIFF')
|
|
461
|
+
pos += 4
|
|
462
|
+
view.setUint32(pos, fileSize, true)
|
|
463
|
+
pos += 4
|
|
464
|
+
writeString(pos, 'WAVE')
|
|
465
|
+
pos += 4
|
|
466
|
+
|
|
467
|
+
// fmt chunk
|
|
468
|
+
writeString(pos, 'fmt ')
|
|
469
|
+
pos += 4
|
|
470
|
+
view.setUint32(pos, 16, true) // chunk size
|
|
471
|
+
pos += 4
|
|
472
|
+
view.setUint16(pos, 1, true) // PCM format
|
|
473
|
+
pos += 2
|
|
474
|
+
view.setUint16(pos, channelCount, true)
|
|
475
|
+
pos += 2
|
|
476
|
+
view.setUint32(pos, sampleRate, true)
|
|
477
|
+
pos += 4
|
|
478
|
+
view.setUint32(pos, byteRate, true)
|
|
479
|
+
pos += 4
|
|
480
|
+
view.setUint16(pos, blockAlign, true)
|
|
481
|
+
pos += 2
|
|
482
|
+
view.setUint16(pos, WAV_BITS_PER_SAMPLE, true) // bits per sample
|
|
483
|
+
pos += 2
|
|
484
|
+
|
|
485
|
+
// data chunk
|
|
486
|
+
writeString(pos, 'data')
|
|
487
|
+
pos += 4
|
|
488
|
+
view.setUint32(pos, dataSize, true)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Parses an AEA file blob to extract header info and frame data
|
|
493
|
+
* @param {Blob} blob - AEA file blob to parse
|
|
494
|
+
* @returns {Promise<Object>} Parsed AEA file data
|
|
495
|
+
* @returns {Object} returns.info - AEA file header information
|
|
496
|
+
* @returns {Uint8Array[]} returns.frameData - Array of frame data buffers
|
|
497
|
+
*/
|
|
498
|
+
static async parseAeaBlob(blob) {
|
|
499
|
+
const buffer = await blob.arrayBuffer()
|
|
500
|
+
const header = new Uint8Array(buffer.slice(0, AEA_HEADER_SIZE))
|
|
501
|
+
const info = AeaFile.parseHeader(header)
|
|
502
|
+
|
|
503
|
+
const frameData = []
|
|
504
|
+
for (let i = AEA_HEADER_SIZE; i < buffer.byteLength; i += SOUND_UNIT_SIZE) {
|
|
505
|
+
const frame = new Uint8Array(buffer.slice(i, i + SOUND_UNIT_SIZE))
|
|
506
|
+
if (frame.length === SOUND_UNIT_SIZE) {
|
|
507
|
+
frameData.push(frame)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return { info, frameData }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Creates a stream of deserialized frames from binary frame data
|
|
516
|
+
* @param {Uint8Array[]} frameData - Array of binary frame data
|
|
517
|
+
* @returns {Generator<Object>} Stream of deserialized frame objects
|
|
518
|
+
*/
|
|
519
|
+
static *deserializedFrameStream(frameData) {
|
|
520
|
+
for (const frame of frameData) {
|
|
521
|
+
yield deserializeFrame(frame)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Assembles PCM frames into a continuous buffer
|
|
527
|
+
* @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames - Array of PCM frames
|
|
528
|
+
* @param {number} channelCount - Number of audio channels
|
|
529
|
+
* @returns {Float32Array} Continuous PCM buffer (interleaved for stereo)
|
|
530
|
+
* @throws {Error} If unsupported channel count is provided
|
|
531
|
+
*/
|
|
532
|
+
static assemblePcmFrames(pcmFrames, channelCount) {
|
|
533
|
+
if (channelCount === 1) {
|
|
534
|
+
const totalSamples = pcmFrames.reduce(
|
|
535
|
+
(sum, frame) => sum + frame.length,
|
|
536
|
+
0
|
|
537
|
+
)
|
|
538
|
+
const pcm = new Float32Array(totalSamples)
|
|
539
|
+
let offset = 0
|
|
540
|
+
for (const frame of pcmFrames) {
|
|
541
|
+
pcm.set(frame, offset)
|
|
542
|
+
offset += frame.length
|
|
543
|
+
}
|
|
544
|
+
return pcm
|
|
545
|
+
} else if (channelCount === 2) {
|
|
546
|
+
const totalSamples = pcmFrames.reduce(
|
|
547
|
+
(sum, [left, right]) => sum + Math.max(left.length, right.length),
|
|
548
|
+
0
|
|
549
|
+
)
|
|
550
|
+
const pcm = new Float32Array(totalSamples * 2)
|
|
551
|
+
|
|
552
|
+
let pcmOffset = 0
|
|
553
|
+
for (const [leftFrame, rightFrame] of pcmFrames) {
|
|
554
|
+
const frameLength = Math.max(leftFrame.length, rightFrame.length)
|
|
555
|
+
for (let j = 0; j < frameLength; j++) {
|
|
556
|
+
pcm[pcmOffset * 2] = j < leftFrame.length ? leftFrame[j] : 0
|
|
557
|
+
pcm[pcmOffset * 2 + 1] = j < rightFrame.length ? rightFrame[j] : 0
|
|
558
|
+
pcmOffset++
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return pcm
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 Audio Codec - File Readers
|
|
3
|
+
*
|
|
4
|
+
* This module provides file reading utilities for AEA format files,
|
|
5
|
+
* supporting both metadata parsing and streaming frame iteration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import { AeaFile, deserializeFrame } from './serialization.js'
|
|
10
|
+
import { AEA_HEADER_SIZE, SOUND_UNIT_SIZE } from '../core/constants.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AEA file reader with async iteration support for streaming frame processing
|
|
14
|
+
*/
|
|
15
|
+
export class AeaReader {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new AEA file reader
|
|
18
|
+
* @param {string} filePath - Path to the AEA file
|
|
19
|
+
*/
|
|
20
|
+
constructor(filePath) {
|
|
21
|
+
this.filePath = filePath
|
|
22
|
+
this.metadata = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load and parse AEA file metadata from header
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
async loadMetadata() {
|
|
30
|
+
const handle = await fs.promises.open(this.filePath, 'r')
|
|
31
|
+
const buffer = Buffer.alloc(AEA_HEADER_SIZE)
|
|
32
|
+
await handle.read(buffer, 0, AEA_HEADER_SIZE, 0)
|
|
33
|
+
await handle.close()
|
|
34
|
+
this.metadata = AeaFile.parseHeader(buffer)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Async iterator for streaming frame data
|
|
39
|
+
* @yields {Object} Deserialized frame data
|
|
40
|
+
*/
|
|
41
|
+
async *[Symbol.asyncIterator]() {
|
|
42
|
+
const stream = fs.createReadStream(this.filePath, {
|
|
43
|
+
start: AEA_HEADER_SIZE,
|
|
44
|
+
})
|
|
45
|
+
let buffer = Buffer.alloc(0)
|
|
46
|
+
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
buffer = Buffer.concat([buffer, chunk])
|
|
49
|
+
while (buffer.length >= SOUND_UNIT_SIZE) {
|
|
50
|
+
const frameData = buffer.slice(0, SOUND_UNIT_SIZE)
|
|
51
|
+
buffer = buffer.slice(SOUND_UNIT_SIZE)
|
|
52
|
+
yield deserializeFrame(frameData)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|