carta1 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }