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,160 @@
1
+ /**
2
+ * Carta1 spectral analysis application.
3
+ */
4
+
5
+ import {
6
+ MDCT_OVERLAP_SIZE,
7
+ MDCT_SHORT_BLOCK_SIZE,
8
+ MDCT_SIZE_LONG,
9
+ MDCT_SIZE_MID,
10
+ MDCT_SIZE_SHORT,
11
+ } from '../core/constants.js'
12
+ import { calculateBandOffset } from '../core/geometry.js'
13
+ import { MDCT_BAND_CONFIGS, WINDOW_SHORT } from '../core/tables.js'
14
+ import { MDCT } from '../signal/mdct.js'
15
+ import { reverseSpectrum } from '../signal/order.js'
16
+ import { throwError } from '../utils.js'
17
+
18
+ const shortMdct = new MDCT(MDCT_SIZE_SHORT, 0.5)
19
+ const middleMdct = new MDCT(MDCT_SIZE_MID, 0.5)
20
+ const longMdct = new MDCT(MDCT_SIZE_LONG, 1)
21
+
22
+ /**
23
+ * Build the ATRAC1 spectrum from analyzed bands.
24
+ *
25
+ * @param {object} context
26
+ * @returns {Function}
27
+ * @throws {Error} If the buffer pool is missing
28
+ */
29
+ export function transformSpectrum(context) {
30
+ const bufferPool =
31
+ context?.bufferPool ??
32
+ throwError('transformSpectrum: bufferPool is required')
33
+ const overlapBuffers = bufferPool.mdctOverlap
34
+ const transformFunctions = [middleMdct, middleMdct, longMdct]
35
+
36
+ /**
37
+ * @param {Float32Array} samples
38
+ * @param {number} bandIndex
39
+ * @param {number} blockMode
40
+ * @param {Float32Array} overlapBuffer
41
+ * @returns {Float32Array}
42
+ */
43
+ function transformBand(samples, bandIndex, blockMode, overlapBuffer) {
44
+ const config = MDCT_BAND_CONFIGS[bandIndex]
45
+
46
+ if (blockMode === 0) {
47
+ return transformLongBlock(
48
+ samples,
49
+ bandIndex,
50
+ config,
51
+ transformFunctions[bandIndex],
52
+ overlapBuffer
53
+ )
54
+ }
55
+
56
+ return transformShortBlocks(samples, bandIndex, config, overlapBuffer)
57
+ }
58
+
59
+ /**
60
+ * @param {Float32Array} samples
61
+ * @param {number} bandIndex
62
+ * @param {Object} config
63
+ * @param {Object} transformFunction
64
+ * @param {Float32Array} overlapBuffer
65
+ * @returns {Float32Array}
66
+ */
67
+ function transformLongBlock(
68
+ samples,
69
+ bandIndex,
70
+ config,
71
+ transformFunction,
72
+ overlapBuffer
73
+ ) {
74
+ const mdctSize = bandIndex === 2 ? MDCT_SIZE_LONG : MDCT_SIZE_MID
75
+ const mdctInput = bufferPool.transformBuffers[mdctSize]
76
+ mdctInput.fill(0)
77
+ mdctInput.set(overlapBuffer, config.windowStart)
78
+
79
+ applyTailWindowing(samples, overlapBuffer, config.size)
80
+ mdctInput.set(samples, config.windowStart + MDCT_OVERLAP_SIZE)
81
+
82
+ let spectrum = transformFunction.transform(
83
+ mdctInput,
84
+ bufferPool.mdctBuffers
85
+ )
86
+ if (bandIndex > 0) {
87
+ spectrum = reverseSpectrum(spectrum, bufferPool.reversalBuffers)
88
+ }
89
+ return spectrum
90
+ }
91
+
92
+ /**
93
+ * @param {Float32Array} samples
94
+ * @param {number} bandIndex
95
+ * @param {Object} config
96
+ * @param {Float32Array} overlapBuffer
97
+ * @returns {Float32Array}
98
+ */
99
+ function transformShortBlocks(samples, bandIndex, config, overlapBuffer) {
100
+ const numBlocks = 1 << (config.size === 256 ? 3 : 2)
101
+ const output = new Float32Array(config.size)
102
+
103
+ for (let block = 0; block < numBlocks; block++) {
104
+ const blockStart = block * MDCT_SHORT_BLOCK_SIZE
105
+ const blockSamples = samples.subarray(
106
+ blockStart,
107
+ blockStart + MDCT_SHORT_BLOCK_SIZE
108
+ )
109
+ const mdctInput = bufferPool.transformBuffers[MDCT_SIZE_SHORT]
110
+ mdctInput.fill(0)
111
+ mdctInput.set(overlapBuffer, 0)
112
+
113
+ applyTailWindowing(blockSamples, overlapBuffer, MDCT_SHORT_BLOCK_SIZE)
114
+ mdctInput.set(blockSamples, MDCT_OVERLAP_SIZE)
115
+
116
+ let spectrum = shortMdct.transform(mdctInput, bufferPool.mdctBuffers)
117
+ if (bandIndex > 0) {
118
+ spectrum = reverseSpectrum(spectrum, bufferPool.reversalBuffers)
119
+ }
120
+ output.set(spectrum, blockStart)
121
+ }
122
+
123
+ return output
124
+ }
125
+
126
+ /**
127
+ * @param {Float32Array} samples
128
+ * @param {Float32Array} overlapBuffer
129
+ * @param {number} blockSize
130
+ */
131
+ function applyTailWindowing(samples, overlapBuffer, blockSize) {
132
+ const tailStart = blockSize - MDCT_OVERLAP_SIZE
133
+ for (let i = 0; i < MDCT_OVERLAP_SIZE; i++) {
134
+ const tailValue = samples[tailStart + i]
135
+ overlapBuffer[i] = WINDOW_SHORT[i] * tailValue
136
+ samples[tailStart + i] = tailValue * WINDOW_SHORT[31 - i]
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {object} input
142
+ * @returns {object}
143
+ */
144
+ return (input) => {
145
+ const { bands, blockModes, originalFrame } = input
146
+ const coefficients = new Float32Array(512)
147
+
148
+ bands.forEach((bandSamples, bandIndex) => {
149
+ const transformed = transformBand(
150
+ bandSamples,
151
+ bandIndex,
152
+ blockModes[bandIndex],
153
+ overlapBuffers[bandIndex]
154
+ )
155
+ coefficients.set(transformed, calculateBandOffset(bandIndex))
156
+ })
157
+
158
+ return { bands, coefficients, blockModes, originalFrame }
159
+ }
160
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Carta1 analysis stage.
3
+ */
4
+
5
+ import { pipe } from '../utils.js'
6
+ import { analyzeBands } from './bands.js'
7
+ import { selectBlocks } from './blocks.js'
8
+ import { transformSpectrum } from './spectrum.js'
9
+
10
+ /**
11
+ * Analyze a PCM frame in the ATRAC1 signal domain.
12
+ *
13
+ * @param {object} context
14
+ * @returns {Function}
15
+ */
16
+ export function analyze(context) {
17
+ const analyzeFrame = pipe(
18
+ context,
19
+ analyzeBands,
20
+ selectBlocks,
21
+ transformSpectrum
22
+ )
23
+
24
+ /**
25
+ * @param {Float32Array} pcmSamples
26
+ * @returns {object}
27
+ */
28
+ return (pcmSamples) => {
29
+ const { coefficients, blockModes } = analyzeFrame(pcmSamples)
30
+ return { coefficients, blockModes }
31
+ }
32
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Carta1 Worker Interface - Client-side API
2
+ * Carta1 browser worker boundary.
3
3
  *
4
4
  * Provides a clean async interface for encoding and decoding ATRAC1 audio
5
5
  * using Web Workers for non-blocking operation.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Carta1 AEA container boundary.
3
+ */
4
+
5
+ import {
6
+ AEA_CHANNEL_COUNT_OFFSET,
7
+ AEA_FRAME_COUNT_OFFSET,
8
+ AEA_HEADER_SIZE,
9
+ AEA_PRIMING_SAMPLE_COUNT_OFFSET,
10
+ AEA_SOURCE_SAMPLE_COUNT_OFFSET,
11
+ AEA_TITLE_OFFSET,
12
+ AEA_TITLE_SIZE,
13
+ STREAM_PRIMING_SAMPLES,
14
+ } from '../core/constants.js'
15
+ import { AEA_MAGIC } from '../core/tables.js'
16
+
17
+ /**
18
+ * AEA file-format handler.
19
+ */
20
+ export class AeaFile {
21
+ /**
22
+ * Create an AEA header.
23
+ *
24
+ * @param {string} [title='']
25
+ * @param {number} [frameCount=0]
26
+ * @param {number} [channelCount=1]
27
+ * @param {number} [sampleCount=0]
28
+ * @param {number} [primingSampleCount=STREAM_PRIMING_SAMPLES]
29
+ * @returns {Uint8Array}
30
+ */
31
+ static createHeader(
32
+ title = '',
33
+ frameCount = 0,
34
+ channelCount = 1,
35
+ sampleCount = 0,
36
+ primingSampleCount = STREAM_PRIMING_SAMPLES
37
+ ) {
38
+ const header = new Uint8Array(AEA_HEADER_SIZE)
39
+ const view = new DataView(header.buffer)
40
+ header.set(AEA_MAGIC, 0)
41
+
42
+ const titleBytes = new TextEncoder().encode(title)
43
+ header.set(
44
+ titleBytes.subarray(0, Math.min(titleBytes.length, AEA_TITLE_SIZE - 1)),
45
+ AEA_TITLE_OFFSET
46
+ )
47
+ view.setUint32(AEA_FRAME_COUNT_OFFSET, frameCount, true)
48
+ header[AEA_CHANNEL_COUNT_OFFSET] = channelCount
49
+ view.setUint32(
50
+ AEA_PRIMING_SAMPLE_COUNT_OFFSET,
51
+ primingSampleCount >>> 0,
52
+ true
53
+ )
54
+ view.setUint32(AEA_SOURCE_SAMPLE_COUNT_OFFSET, sampleCount >>> 0, true)
55
+ return header
56
+ }
57
+
58
+ /**
59
+ * Parse an AEA header.
60
+ *
61
+ * @param {Uint8Array} header
62
+ * @returns {object} Container metadata.
63
+ * @throws {Error} If the header is invalid
64
+ */
65
+ static parseHeader(header) {
66
+ if (header.length !== AEA_HEADER_SIZE) {
67
+ throw new Error(`Header must be ${AEA_HEADER_SIZE} bytes`)
68
+ }
69
+
70
+ for (let i = 0; i < AEA_MAGIC.length; i++) {
71
+ if (header[i] !== AEA_MAGIC[i]) {
72
+ throw new Error('Invalid AEA file')
73
+ }
74
+ }
75
+
76
+ const view = new DataView(
77
+ header.buffer,
78
+ header.byteOffset,
79
+ header.byteLength
80
+ )
81
+ const titleEnd = header.indexOf(0, AEA_TITLE_OFFSET)
82
+ const titleLength =
83
+ titleEnd === -1 ? AEA_TITLE_SIZE : titleEnd - AEA_TITLE_OFFSET
84
+ const title = new TextDecoder().decode(
85
+ header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + titleLength)
86
+ )
87
+
88
+ return {
89
+ title,
90
+ frameCount: view.getUint32(AEA_FRAME_COUNT_OFFSET, true),
91
+ channelCount: header[AEA_CHANNEL_COUNT_OFFSET],
92
+ primingSampleCount: view.getUint32(AEA_PRIMING_SAMPLE_COUNT_OFFSET, true),
93
+ sampleCount: view.getUint32(AEA_SOURCE_SAMPLE_COUNT_OFFSET, true),
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Carta1 PCM boundary conversions.
3
+ */
4
+
5
+ import {
6
+ SAMPLE_RATE,
7
+ SAMPLES_PER_FRAME,
8
+ WAV_BITS_PER_SAMPLE,
9
+ WAV_BYTES_PER_SAMPLE,
10
+ WAV_DATA_OFFSET,
11
+ WAV_HEADER_SIZE,
12
+ WAV_PCM_MAX_NEGATIVE,
13
+ WAV_PCM_MAX_POSITIVE,
14
+ } from '../core/constants.js'
15
+
16
+ /**
17
+ * Write a four-character code.
18
+ *
19
+ * @param {DataView} output
20
+ * @param {number} offset
21
+ * @param {string} value
22
+ */
23
+ function writeFourCc(output, offset, value) {
24
+ for (let i = 0; i < value.length; i++) {
25
+ output.setUint8(offset + i, value.charCodeAt(i))
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Join complete PCM frames into one channel.
31
+ *
32
+ * @param {Float32Array[]} frames
33
+ * @returns {Float32Array}
34
+ */
35
+ export function joinChannelFrames(frames) {
36
+ const sampleCount = frames.reduce((total, frame) => total + frame.length, 0)
37
+ const channel = new Float32Array(sampleCount)
38
+ let offset = 0
39
+
40
+ for (const frame of frames) {
41
+ channel.set(frame, offset)
42
+ offset += frame.length
43
+ }
44
+ return channel
45
+ }
46
+
47
+ /**
48
+ * Split planar PCM buffers into complete zero-padded codec frames.
49
+ *
50
+ * @param {Float32Array[]} buffers
51
+ * @param {number} [frameSize=SAMPLES_PER_FRAME]
52
+ * @returns {Generator<Float32Array>|Generator<[Float32Array, Float32Array]>}
53
+ * @throws {Error} If the channel count is unsupported
54
+ */
55
+ export function* frameBufferToFrames(buffers, frameSize = SAMPLES_PER_FRAME) {
56
+ if (
57
+ !Array.isArray(buffers) ||
58
+ (buffers.length !== 1 && buffers.length !== 2) ||
59
+ !buffers.every(
60
+ (buffer) =>
61
+ buffer instanceof Float32Array && buffer.length === buffers[0].length
62
+ ) ||
63
+ !Number.isInteger(frameSize) ||
64
+ frameSize < 1
65
+ ) {
66
+ throw new RangeError(
67
+ 'ATRAC1 framing requires one or two equally sized Float32 channels and a positive integer frame size'
68
+ )
69
+ }
70
+ if (buffers.length === 1) {
71
+ const [left] = buffers
72
+ for (let offset = 0; offset < left.length; offset += frameSize) {
73
+ const frame = new Float32Array(frameSize)
74
+ frame.set(left.subarray(offset, offset + frameSize))
75
+ yield frame
76
+ }
77
+ return
78
+ }
79
+
80
+ if (buffers.length === 2) {
81
+ const [left, right] = buffers
82
+ const sampleCount = left.length
83
+ for (let offset = 0; offset < sampleCount; offset += frameSize) {
84
+ const leftFrame = new Float32Array(frameSize)
85
+ const rightFrame = new Float32Array(frameSize)
86
+ leftFrame.set(left.subarray(offset, offset + frameSize))
87
+ rightFrame.set(right.subarray(offset, offset + frameSize))
88
+ yield [leftFrame, rightFrame]
89
+ }
90
+ return
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Convert one normalized sample to signed PCM16.
96
+ *
97
+ * @param {number} sample
98
+ * @returns {number}
99
+ */
100
+ export function floatToPcm16(sample) {
101
+ const clipped = Math.max(-1, Math.min(1, sample))
102
+ return clipped < 0
103
+ ? clipped * WAV_PCM_MAX_NEGATIVE
104
+ : clipped * WAV_PCM_MAX_POSITIVE
105
+ }
106
+
107
+ /**
108
+ * Create a PCM WAVE header.
109
+ *
110
+ * @param {Object} options
111
+ * @param {number} options.sampleCount
112
+ * @param {number} options.sampleRate
113
+ * @param {number} options.channels
114
+ * @returns {Uint8Array}
115
+ */
116
+ export function createPcmWaveHeader({ sampleCount, sampleRate, channels }) {
117
+ const byteRate = sampleRate * channels * WAV_BYTES_PER_SAMPLE
118
+ const blockAlign = channels * WAV_BYTES_PER_SAMPLE
119
+ const dataSize = sampleCount * blockAlign
120
+ const output = new Uint8Array(WAV_HEADER_SIZE)
121
+ const view = new DataView(output.buffer)
122
+
123
+ writeFourCc(view, 0, 'RIFF')
124
+ view.setUint32(4, WAV_DATA_OFFSET + dataSize, true)
125
+ writeFourCc(view, 8, 'WAVE')
126
+ writeFourCc(view, 12, 'fmt ')
127
+ view.setUint32(16, 16, true)
128
+ view.setUint16(20, 1, true)
129
+ view.setUint16(22, channels, true)
130
+ view.setUint32(24, sampleRate, true)
131
+ view.setUint32(28, byteRate, true)
132
+ view.setUint16(32, blockAlign, true)
133
+ view.setUint16(34, WAV_BITS_PER_SAMPLE, true)
134
+ writeFourCc(view, 36, 'data')
135
+ view.setUint32(40, dataSize, true)
136
+ return output
137
+ }
138
+
139
+ /**
140
+ * Interleave planar normalized channels as PCM16.
141
+ *
142
+ * @param {Float32Array[]} channels
143
+ * @returns {Uint8Array}
144
+ */
145
+ export function interleavePcm16(channels) {
146
+ if (
147
+ !Array.isArray(channels) ||
148
+ channels.length < 1 ||
149
+ !channels.every(
150
+ (channel) =>
151
+ channel instanceof Float32Array && channel.length === channels[0].length
152
+ )
153
+ ) {
154
+ throw new RangeError('PCM must contain equally sized Float32 channels')
155
+ }
156
+ const sampleCount = channels[0].length
157
+ const output = new Uint8Array(
158
+ sampleCount * channels.length * WAV_BYTES_PER_SAMPLE
159
+ )
160
+ const view = new DataView(output.buffer)
161
+ let offset = 0
162
+
163
+ for (let sample = 0; sample < sampleCount; sample++) {
164
+ for (const channel of channels) {
165
+ view.setInt16(offset, floatToPcm16(channel[sample]), true)
166
+ offset += WAV_BYTES_PER_SAMPLE
167
+ }
168
+ }
169
+ return output
170
+ }
171
+
172
+ /**
173
+ * Create a complete PCM WAVE byte image from planar channels.
174
+ *
175
+ * @param {Float32Array[]} channels
176
+ * @param {Object} [options]
177
+ * @param {number} [options.sampleRate=SAMPLE_RATE]
178
+ * @returns {Uint8Array}
179
+ */
180
+ export function createPcmWave(channels, options = {}) {
181
+ const { sampleRate = SAMPLE_RATE } = options
182
+ const pcm = interleavePcm16(channels)
183
+ const sampleCount = channels[0].length
184
+ const header = createPcmWaveHeader({
185
+ sampleCount,
186
+ sampleRate,
187
+ channels: channels.length,
188
+ })
189
+ const output = new Uint8Array(header.length + pcm.length)
190
+ output.set(header)
191
+ output.set(pcm, header.length)
192
+ return output
193
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Carta1 public processing facade.
3
+ */
4
+
5
+ import {
6
+ AEA_HEADER_SIZE,
7
+ SAMPLE_RATE,
8
+ SAMPLES_PER_FRAME,
9
+ SOUND_UNIT_SIZE,
10
+ STREAM_FLUSH_TAIL_FRAMES,
11
+ STREAM_PRIMING_SAMPLES,
12
+ } from '../core/constants.js'
13
+ import { deserializeFrame, serializeFrame } from '../syntax/frame.js'
14
+ import { AeaFile } from './container.js'
15
+ import { createPcmWave, frameBufferToFrames, joinChannelFrames } from './pcm.js'
16
+ import {
17
+ createAeaStreamingDecoder,
18
+ createAeaStreamingEncoder,
19
+ decodeAeaPcm,
20
+ encodeAeaPcm,
21
+ } from './stream.js'
22
+
23
+ /**
24
+ * High-level audio processing facade.
25
+ */
26
+ export class AudioProcessor {
27
+ /**
28
+ * @param {Float32Array[]} channels
29
+ * @param {object} [options]
30
+ * @returns {Promise<Uint8Array>}
31
+ */
32
+ static encodeAeaPcm(channels, options = {}) {
33
+ return encodeAeaPcm(channels, options)
34
+ }
35
+
36
+ /**
37
+ * @param {Uint8Array|ArrayBuffer|Blob} input
38
+ * @returns {Promise<Float32Array[]>}
39
+ */
40
+ static decodeAeaPcm(input) {
41
+ return decodeAeaPcm(input)
42
+ }
43
+
44
+ /**
45
+ * @param {AsyncIterable<object>} audioFrames
46
+ * @param {object} [options]
47
+ * @returns {AsyncGenerator<object>}
48
+ */
49
+ static async *encodeStream(audioFrames, options = {}) {
50
+ const encoder = createAeaStreamingEncoder(options)
51
+ yield* encoder.frames(audioFrames)
52
+ yield* encoder.finish()
53
+ }
54
+
55
+ /**
56
+ * @param {AsyncIterable<object>} encodedFrames
57
+ * @param {object} [options]
58
+ * @returns {AsyncGenerator<object>}
59
+ */
60
+ static async *decodeStream(encodedFrames, options = {}) {
61
+ const decoder = createAeaStreamingDecoder(options)
62
+ yield* decoder.frames(encodedFrames)
63
+ yield* decoder.finish()
64
+ }
65
+
66
+ /**
67
+ * @param {Float32Array[]} buffers
68
+ * @param {number} [frameSize]
69
+ * @returns {Generator<Float32Array>|Generator<[Float32Array, Float32Array]>}
70
+ */
71
+ static *frameBufferToFrames(buffers, frameSize) {
72
+ yield* frameBufferToFrames(buffers, frameSize)
73
+ }
74
+
75
+ /**
76
+ * @param {AsyncIterable|Iterable} frameStream
77
+ * @returns {Promise<Array>}
78
+ */
79
+ static async collectFrames(frameStream) {
80
+ const frames = []
81
+ for await (const frame of frameStream) {
82
+ frames.push(frame)
83
+ }
84
+ return frames
85
+ }
86
+
87
+ /**
88
+ * @param {AsyncIterable<object>} encodedFrames
89
+ * @param {object} [options]
90
+ * @returns {Promise<Blob>}
91
+ */
92
+ static async createAeaBlob(encodedFrames, options = {}) {
93
+ const { title = 'encoded by atrac1.js', channelCount = 1 } = options
94
+ const frames = []
95
+ for await (const frame of encodedFrames) {
96
+ frames.push(serializeFrame(frame))
97
+ }
98
+
99
+ const timelineFrames = frames.length / channelCount
100
+ const sampleCount =
101
+ options.sampleCount ??
102
+ Math.max(0, timelineFrames - STREAM_FLUSH_TAIL_FRAMES) * SAMPLES_PER_FRAME
103
+ const header = AeaFile.createHeader(
104
+ title,
105
+ frames.length,
106
+ channelCount,
107
+ sampleCount,
108
+ STREAM_PRIMING_SAMPLES
109
+ )
110
+ return new Blob([header, ...frames], { type: 'application/octet-stream' })
111
+ }
112
+
113
+ /**
114
+ * @param {Blob} blob
115
+ * @returns {Promise<object>} Parsed AEA data.
116
+ */
117
+ static async parseAeaBlob(blob) {
118
+ const buffer = await blob.arrayBuffer()
119
+ const header = new Uint8Array(buffer.slice(0, AEA_HEADER_SIZE))
120
+ const info = AeaFile.parseHeader(header)
121
+ const frameData = []
122
+
123
+ for (let offset = AEA_HEADER_SIZE; offset < buffer.byteLength;) {
124
+ const frame = new Uint8Array(
125
+ buffer.slice(offset, offset + SOUND_UNIT_SIZE)
126
+ )
127
+ if (frame.length === SOUND_UNIT_SIZE) {
128
+ frameData.push(frame)
129
+ }
130
+ offset += SOUND_UNIT_SIZE
131
+ }
132
+ return { info, frameData }
133
+ }
134
+
135
+ /**
136
+ * @param {Uint8Array[]} frameData
137
+ * @returns {Generator<object>}
138
+ */
139
+ static *deserializedFrameStream(frameData) {
140
+ for (const frame of frameData) {
141
+ yield deserializeFrame(frame)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
147
+ * @param {number} [channelCount=1]
148
+ * @param {number} [sampleRate=SAMPLE_RATE]
149
+ * @returns {Blob}
150
+ */
151
+ static createPcmWaveBlob(
152
+ pcmFrames,
153
+ channelCount = 1,
154
+ sampleRate = SAMPLE_RATE
155
+ ) {
156
+ let channels
157
+ if (channelCount === 1) {
158
+ channels = [joinChannelFrames(pcmFrames)]
159
+ } else if (channelCount === 2) {
160
+ channels = [
161
+ joinChannelFrames(pcmFrames.map(([left]) => left)),
162
+ joinChannelFrames(pcmFrames.map(([, right]) => right)),
163
+ ]
164
+ } else {
165
+ throw new Error(`Unsupported channel count: ${channelCount}`)
166
+ }
167
+ return new Blob([createPcmWave(channels, { sampleRate })], {
168
+ type: 'audio/wav',
169
+ })
170
+ }
171
+
172
+ /**
173
+ * Preserve the supported WAVE facade name.
174
+ *
175
+ * @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
176
+ * @param {number} [channelCount=1]
177
+ * @param {number} [sampleRate=SAMPLE_RATE]
178
+ * @returns {Blob}
179
+ */
180
+ static createWavBlob(pcmFrames, channelCount = 1, sampleRate = SAMPLE_RATE) {
181
+ return AudioProcessor.createPcmWaveBlob(pcmFrames, channelCount, sampleRate)
182
+ }
183
+
184
+ /**
185
+ * Assemble PCM frames using the supported interleaved result shape.
186
+ *
187
+ * @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
188
+ * @param {number} channelCount
189
+ * @returns {Float32Array}
190
+ */
191
+ static assemblePcmFrames(pcmFrames, channelCount) {
192
+ if (channelCount === 1) {
193
+ return joinChannelFrames(pcmFrames)
194
+ }
195
+ if (channelCount !== 2) {
196
+ throw new Error(`Unsupported channel count: ${channelCount}`)
197
+ }
198
+
199
+ const left = joinChannelFrames(pcmFrames.map(([channel]) => channel))
200
+ const right = joinChannelFrames(pcmFrames.map(([, channel]) => channel))
201
+ const pcm = new Float32Array(Math.max(left.length, right.length) * 2)
202
+ for (let i = 0; i < pcm.length / 2; i++) {
203
+ pcm[i * 2] = left[i] ?? 0
204
+ pcm[i * 2 + 1] = right[i] ?? 0
205
+ }
206
+ return pcm
207
+ }
208
+ }
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import fs from 'fs'
9
- import { AeaFile, deserializeFrame } from './serialization.js'
9
+ import { AeaFile } from './container.js'
10
+ import { deserializeFrame } from '../syntax/frame.js'
10
11
  import { AEA_HEADER_SIZE, SOUND_UNIT_SIZE } from '../core/constants.js'
11
12
 
12
13
  /**