@siteed/audio-studio 3.1.1 → 3.2.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 (96) hide show
  1. package/CHANGELOG.md +375 -4
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +852 -0
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +167 -3
  4. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  5. package/build/cjs/errors/AudioStreamError.js +161 -0
  6. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  7. package/build/cjs/errors/AudioStreamError.test.js +82 -0
  8. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  9. package/build/cjs/index.js +7 -1
  10. package/build/cjs/index.js.map +1 -1
  11. package/build/cjs/streamAudioData.js +534 -0
  12. package/build/cjs/streamAudioData.js.map +1 -0
  13. package/build/cjs/utils/audioProcessing.js +14 -10
  14. package/build/cjs/utils/audioProcessing.js.map +1 -1
  15. package/build/esm/errors/AudioStreamError.js +156 -0
  16. package/build/esm/errors/AudioStreamError.js.map +1 -0
  17. package/build/esm/errors/AudioStreamError.test.js +80 -0
  18. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  19. package/build/esm/index.js +3 -1
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/streamAudioData.js +527 -0
  22. package/build/esm/streamAudioData.js.map +1 -0
  23. package/build/esm/utils/audioProcessing.js +14 -10
  24. package/build/esm/utils/audioProcessing.js.map +1 -1
  25. package/build/types/errors/AudioStreamError.d.ts +25 -0
  26. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  27. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  28. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  29. package/build/types/index.d.ts +5 -1
  30. package/build/types/index.d.ts.map +1 -1
  31. package/build/types/streamAudioData.d.ts +119 -0
  32. package/build/types/streamAudioData.d.ts.map +1 -0
  33. package/build/types/utils/audioProcessing.d.ts +2 -2
  34. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  35. package/ios/AudioProcessingHelpers.swift +10 -5
  36. package/ios/AudioStreamDecoder.swift +614 -0
  37. package/ios/AudioStudioModule.swift +186 -3
  38. package/package.json +163 -146
  39. package/scripts/README.md +58 -0
  40. package/src/errors/AudioStreamError.test.ts +92 -0
  41. package/src/errors/AudioStreamError.ts +199 -0
  42. package/src/index.ts +24 -0
  43. package/src/streamAudioData.ts +758 -0
  44. package/src/utils/audioProcessing.ts +25 -14
  45. package/android/src/androidTest/assets/chorus.wav +0 -0
  46. package/android/src/androidTest/assets/jfk.wav +0 -0
  47. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  48. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  49. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  50. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  51. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  52. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  53. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  54. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  55. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  56. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  57. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  58. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  59. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  60. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  61. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  62. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  63. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  64. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  65. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  66. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  67. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  68. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  69. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  70. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  71. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  72. package/android/src/test/resources/chorus.wav +0 -0
  73. package/android/src/test/resources/generate_test_audio.py +0 -94
  74. package/android/src/test/resources/jfk.wav +0 -0
  75. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  76. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  77. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  78. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  79. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  80. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  81. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  82. package/ios/AudioStudioTests/Info.plist +0 -22
  83. package/ios/AudioStudioTests/README.md +0 -39
  84. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  85. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  86. package/ios/tests/README.md +0 -41
  87. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  88. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  89. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  90. package/ios/tests/integration/output_control_test.swift +0 -322
  91. package/ios/tests/integration/run_integration_tests.sh +0 -37
  92. package/ios/tests/opus_support_test_macos.swift +0 -154
  93. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  94. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  95. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  96. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -0,0 +1,534 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.streamAudioData = streamAudioData;
7
+ exports.getAudioDecodeCapabilities = getAudioDecodeCapabilities;
8
+ const expo_modules_core_1 = require("expo-modules-core");
9
+ const AudioStudioModule_1 = __importDefault(require("./AudioStudioModule"));
10
+ const constants_1 = require("./constants");
11
+ const AudioStreamError_1 = require("./errors/AudioStreamError");
12
+ const audioProcessing_1 = require("./utils/audioProcessing");
13
+ const CHUNK_EVENT = 'AudioDataStreamChunk';
14
+ const PROGRESS_EVENT = 'AudioDataStreamProgress';
15
+ const COMPLETE_EVENT = 'AudioDataStreamComplete';
16
+ const ERROR_EVENT = 'AudioDataStreamError';
17
+ let cachedEmitter = null;
18
+ function getEmitter() {
19
+ if (!cachedEmitter) {
20
+ cachedEmitter = new expo_modules_core_1.LegacyEventEmitter(AudioStudioModule_1.default);
21
+ }
22
+ return cachedEmitter;
23
+ }
24
+ function generateRequestId() {
25
+ const g = globalThis;
26
+ if (typeof g.crypto?.randomUUID === 'function') {
27
+ try {
28
+ return g.crypto.randomUUID();
29
+ }
30
+ catch {
31
+ // fall through
32
+ }
33
+ }
34
+ return `asd_${Date.now().toString(36)}_${Math.random()
35
+ .toString(36)
36
+ .slice(2, 10)}`;
37
+ }
38
+ function toFloat32(samples) {
39
+ if (samples instanceof Float32Array)
40
+ return samples;
41
+ if (Array.isArray(samples)) {
42
+ const out = new Float32Array(samples.length);
43
+ for (let i = 0; i < samples.length; i++) {
44
+ const v = Number(samples[i]);
45
+ out[i] = Number.isFinite(v) ? v : 0;
46
+ }
47
+ return out;
48
+ }
49
+ if (typeof samples === 'string') {
50
+ const bytes = base64ToBytes(samples);
51
+ const floatLength = Math.floor(bytes.byteLength / 4);
52
+ if (bytes.byteOffset % 4 === 0) {
53
+ return new Float32Array(bytes.buffer, bytes.byteOffset, floatLength);
54
+ }
55
+ const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
56
+ return new Float32Array(sliced, 0, Math.floor(sliced.byteLength / 4));
57
+ }
58
+ if (samples && typeof samples === 'object' && 'length' in samples) {
59
+ // ArrayLike fallback
60
+ const arr = samples;
61
+ const out = new Float32Array(arr.length);
62
+ for (let i = 0; i < arr.length; i++) {
63
+ const v = Number(arr[i]);
64
+ out[i] = Number.isFinite(v) ? v : 0;
65
+ }
66
+ return out;
67
+ }
68
+ return new Float32Array(0);
69
+ }
70
+ function base64ToBytes(input) {
71
+ const g = globalThis;
72
+ if (typeof g.atob !== 'function') {
73
+ // Buffer path for environments without atob; React Native has atob.
74
+ const Buf = globalThis.Buffer;
75
+ if (Buf)
76
+ return new Uint8Array(Buf.from(input, 'base64'));
77
+ return new Uint8Array(0);
78
+ }
79
+ const bin = g.atob(input);
80
+ const out = new Uint8Array(bin.length);
81
+ for (let i = 0; i < bin.length; i++)
82
+ out[i] = bin.charCodeAt(i);
83
+ return out;
84
+ }
85
+ function rejectInvalidRange(message) {
86
+ throw new AudioStreamError_1.AudioStreamError({
87
+ code: 'ERR_AUDIO_STREAM_INVALID_RANGE',
88
+ message,
89
+ recoverable: false,
90
+ });
91
+ }
92
+ function assertPositiveFiniteOption(value, name, integer = false) {
93
+ if (value === undefined)
94
+ return;
95
+ if (!Number.isFinite(value) ||
96
+ value <= 0 ||
97
+ (integer && !Number.isInteger(value))) {
98
+ rejectInvalidRange(`${name} must be a positive${integer ? ' integer' : ''}`);
99
+ }
100
+ }
101
+ function validateOptions(options) {
102
+ if (!options.fileUri) {
103
+ rejectInvalidRange('fileUri is required');
104
+ }
105
+ if (options.startTimeMs !== undefined &&
106
+ options.endTimeMs !== undefined &&
107
+ options.startTimeMs >= options.endTimeMs) {
108
+ rejectInvalidRange('startTimeMs must be < endTimeMs');
109
+ }
110
+ if (options.endTimeMs !== undefined && options.endTimeMs <= 0) {
111
+ rejectInvalidRange('endTimeMs must be > 0');
112
+ }
113
+ if (options.startTimeMs !== undefined && options.startTimeMs < 0) {
114
+ rejectInvalidRange('startTimeMs must be >= 0');
115
+ }
116
+ if (options.chunkDurationMs !== undefined &&
117
+ (options.chunkDurationMs < 10 || options.chunkDurationMs > 60000)) {
118
+ rejectInvalidRange('chunkDurationMs must be in [10, 60000]');
119
+ }
120
+ if (options.backpressureTimeoutMs !== undefined &&
121
+ options.backpressureTimeoutMs < 0) {
122
+ rejectInvalidRange('backpressureTimeoutMs must be >= 0');
123
+ }
124
+ assertPositiveFiniteOption(options.targetSampleRate, 'targetSampleRate');
125
+ assertPositiveFiniteOption(options.sampleRate, 'sampleRate');
126
+ assertPositiveFiniteOption(options.channels, 'channels', true);
127
+ assertPositiveFiniteOption(options.maxBufferedChunks, 'maxBufferedChunks', true);
128
+ assertPositiveFiniteOption(options.maxChunkBytes, 'maxChunkBytes', true);
129
+ if (options.streamFormat !== undefined &&
130
+ options.streamFormat !== 'float32') {
131
+ throw new AudioStreamError_1.AudioStreamError({
132
+ code: 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',
133
+ message: `Unsupported streamFormat: ${options.streamFormat}`,
134
+ recoverable: false,
135
+ });
136
+ }
137
+ }
138
+ /**
139
+ * Stream decoded audio from a stored file as bounded Float32 PCM chunks.
140
+ *
141
+ * Memory bound:
142
+ * `chunkDurationMs * sampleRate * channels * 4 * maxBufferedChunks` +
143
+ * native decoder buffers.
144
+ *
145
+ * Cancellation: pass `options.signal` and call `abort()`. The returned promise
146
+ * resolves with `cancelled: true` (it does not reject) when cancellation wins.
147
+ *
148
+ * Backpressure: if `onChunk` returns a Promise, native decode is paused until
149
+ * it resolves; if it throws, the stream is aborted with a `decode_failed` error.
150
+ */
151
+ async function streamAudioData(options, callbacks) {
152
+ validateOptions(options);
153
+ if (constants_1.isWeb) {
154
+ return streamAudioDataWeb(options, callbacks);
155
+ }
156
+ return streamAudioDataNative(options, callbacks);
157
+ }
158
+ /** Discover what the running platform supports. */
159
+ async function getAudioDecodeCapabilities() {
160
+ if (constants_1.isWeb) {
161
+ return {
162
+ platform: 'web',
163
+ supportedInputFormats: [
164
+ 'audio/wav',
165
+ 'audio/mpeg',
166
+ 'audio/mp4',
167
+ 'audio/aac',
168
+ 'audio/ogg',
169
+ 'audio/webm',
170
+ ],
171
+ supportedOutputFormats: ['float32'],
172
+ supportsCancellation: true,
173
+ supportsBackpressure: true,
174
+ supportsTimeRange: true,
175
+ supportsTargetSampleRate: true,
176
+ supportsChannelMixing: true,
177
+ knownLimitations: [
178
+ 'Web decodes the entire file via AudioContext.decodeAudioData before chunking; very long files may exceed browser memory.',
179
+ ],
180
+ };
181
+ }
182
+ if (typeof AudioStudioModule_1.default.getAudioDecodeCapabilities === 'function') {
183
+ try {
184
+ const caps = await AudioStudioModule_1.default.getAudioDecodeCapabilities();
185
+ return caps;
186
+ }
187
+ catch (err) {
188
+ throw (0, AudioStreamError_1.mapStreamError)(err, undefined, 'native');
189
+ }
190
+ }
191
+ throw new AudioStreamError_1.AudioStreamError({
192
+ code: 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',
193
+ message: 'getAudioDecodeCapabilities is not available on this build',
194
+ recoverable: false,
195
+ });
196
+ }
197
+ async function streamAudioDataNative(options, callbacks) {
198
+ if (typeof AudioStudioModule_1.default.streamAudioData !== 'function') {
199
+ throw new AudioStreamError_1.AudioStreamError({
200
+ code: 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',
201
+ message: 'streamAudioData native method missing — rebuild the host app with the latest @siteed/audio-studio.',
202
+ recoverable: false,
203
+ });
204
+ }
205
+ const requestId = generateRequestId();
206
+ const emitter = getEmitter();
207
+ const subs = [];
208
+ let chunkCount = 0;
209
+ let sampleCount = 0;
210
+ let processingChain = Promise.resolve();
211
+ let settled = false;
212
+ let abortListener = null;
213
+ let lastProgress = null;
214
+ const finalize = () => {
215
+ for (const sub of subs) {
216
+ try {
217
+ sub.remove();
218
+ }
219
+ catch {
220
+ /* noop */
221
+ }
222
+ }
223
+ subs.length = 0;
224
+ if (abortListener && options.signal) {
225
+ options.signal.removeEventListener('abort', abortListener);
226
+ }
227
+ };
228
+ return new Promise((resolve, reject) => {
229
+ const settle = (fn, mode, value) => {
230
+ if (settled)
231
+ return;
232
+ settled = true;
233
+ finalize();
234
+ fn();
235
+ if (mode === 'resolve') {
236
+ resolve(value);
237
+ }
238
+ else {
239
+ reject(value);
240
+ }
241
+ };
242
+ const handleError = (err) => {
243
+ settle(() => {
244
+ try {
245
+ AudioStudioModule_1.default.cancelStreamAudioData?.(requestId);
246
+ }
247
+ catch {
248
+ /* noop */
249
+ }
250
+ }, 'reject', (0, AudioStreamError_1.mapStreamError)(err, options.fileUri, 'native'));
251
+ };
252
+ subs.push(emitter.addListener(CHUNK_EVENT, (raw) => {
253
+ const evt = raw;
254
+ if (evt.requestId !== requestId)
255
+ return;
256
+ const chunk = {
257
+ requestId: evt.requestId,
258
+ chunkIndex: evt.chunkIndex,
259
+ startTimeMs: evt.startTimeMs,
260
+ endTimeMs: evt.endTimeMs,
261
+ durationMs: evt.endTimeMs - evt.startTimeMs,
262
+ startSample: evt.startSample,
263
+ sampleCount: evt.sampleCount,
264
+ sampleRate: evt.sampleRate,
265
+ channels: evt.channels,
266
+ samples: toFloat32(evt.samples),
267
+ isFinal: evt.isFinal,
268
+ };
269
+ chunkCount += 1;
270
+ sampleCount += chunk.sampleCount;
271
+ processingChain = processingChain
272
+ .then(async () => {
273
+ await callbacks.onChunk(chunk);
274
+ try {
275
+ AudioStudioModule_1.default.acknowledgeStreamAudioChunk?.(requestId, chunk.chunkIndex);
276
+ }
277
+ catch {
278
+ /* noop */
279
+ }
280
+ })
281
+ .catch(handleError);
282
+ }));
283
+ if (callbacks.onProgress) {
284
+ subs.push(emitter.addListener(PROGRESS_EVENT, (raw) => {
285
+ const evt = raw;
286
+ if (evt.requestId !== requestId)
287
+ return;
288
+ lastProgress = evt;
289
+ callbacks.onProgress(evt);
290
+ }));
291
+ }
292
+ subs.push(emitter.addListener(COMPLETE_EVENT, (raw) => {
293
+ const evt = raw;
294
+ if (evt.requestId !== requestId)
295
+ return;
296
+ // Wait for in-flight onChunk callbacks before resolving.
297
+ processingChain
298
+ .then(() => {
299
+ settle(() => { }, 'resolve', {
300
+ requestId,
301
+ durationMs: evt.durationMs > 0
302
+ ? evt.durationMs
303
+ : (lastProgress?.durationMs ?? 0),
304
+ sampleRate: evt.sampleRate,
305
+ channels: evt.channels,
306
+ chunks: evt.chunks ?? chunkCount,
307
+ samples: evt.samples ?? sampleCount,
308
+ cancelled: evt.cancelled,
309
+ });
310
+ })
311
+ .catch(handleError);
312
+ }));
313
+ subs.push(emitter.addListener(ERROR_EVENT, (raw) => {
314
+ const evt = raw;
315
+ if (evt.requestId !== requestId)
316
+ return;
317
+ if (evt.code === 'ERR_AUDIO_STREAM_CANCELLED') {
318
+ processingChain
319
+ .catch(() => { })
320
+ .then(() => {
321
+ settle(() => { }, 'resolve', {
322
+ requestId,
323
+ durationMs: lastProgress?.durationMs ?? 0,
324
+ sampleRate: options.targetSampleRate ??
325
+ options.sampleRate ??
326
+ 0,
327
+ channels: options.channels ?? 1,
328
+ chunks: chunkCount,
329
+ samples: sampleCount,
330
+ cancelled: true,
331
+ });
332
+ });
333
+ return;
334
+ }
335
+ handleError(new AudioStreamError_1.AudioStreamError({
336
+ code: evt.code ??
337
+ 'ERR_AUDIO_STREAM_UNKNOWN',
338
+ message: evt.message ?? 'native stream error',
339
+ nativeMessage: evt.nativeMessage,
340
+ fileUri: options.fileUri,
341
+ platform: 'native',
342
+ recoverable: false,
343
+ }));
344
+ }));
345
+ if (options.signal) {
346
+ if (options.signal.aborted) {
347
+ settle(() => { }, 'resolve', {
348
+ requestId,
349
+ durationMs: lastProgress?.durationMs ?? 0,
350
+ sampleRate: options.targetSampleRate ?? options.sampleRate ?? 0,
351
+ channels: options.channels ?? 1,
352
+ chunks: 0,
353
+ samples: 0,
354
+ cancelled: true,
355
+ });
356
+ return;
357
+ }
358
+ abortListener = () => {
359
+ try {
360
+ AudioStudioModule_1.default.cancelStreamAudioData?.(requestId);
361
+ }
362
+ catch {
363
+ /* noop */
364
+ }
365
+ };
366
+ options.signal.addEventListener('abort', abortListener);
367
+ }
368
+ const { signal: _signal, ...nativeOptions } = options;
369
+ AudioStudioModule_1.default.streamAudioData({
370
+ ...nativeOptions,
371
+ requestId,
372
+ streamFormat: options.streamFormat ?? 'float32',
373
+ chunkDurationMs: options.chunkDurationMs ?? 1000,
374
+ maxBufferedChunks: options.maxBufferedChunks ?? 4,
375
+ normalizeAudio: options.normalizeAudio ?? true,
376
+ }).catch(handleError);
377
+ });
378
+ }
379
+ async function streamAudioDataWeb(options, callbacks) {
380
+ const requestId = generateRequestId();
381
+ const cancelled = () => options.signal?.aborted === true;
382
+ if (cancelled()) {
383
+ return {
384
+ requestId,
385
+ durationMs: 0,
386
+ sampleRate: options.targetSampleRate ?? options.sampleRate ?? 0,
387
+ channels: options.channels ?? 1,
388
+ chunks: 0,
389
+ samples: 0,
390
+ cancelled: true,
391
+ };
392
+ }
393
+ try {
394
+ const processed = await (0, audioProcessing_1.processAudioBuffer)({
395
+ fileUri: options.fileUri,
396
+ targetSampleRate: options.targetSampleRate,
397
+ targetChannels: options.channels,
398
+ normalizeAudio: options.normalizeAudio ?? true,
399
+ startTimeMs: options.startTimeMs,
400
+ endTimeMs: options.endTimeMs,
401
+ });
402
+ const sampleRate = processed.sampleRate;
403
+ const channels = processed.channels;
404
+ const durationMs = processed.durationMs;
405
+ const chunkDurationMs = options.chunkDurationMs ?? 1000;
406
+ let samplesPerChunk = Math.max(channels, Math.floor((chunkDurationMs / 1000) * sampleRate) * channels);
407
+ if (options.maxChunkBytes) {
408
+ // Round down to a multiple of `channels` so we never split an
409
+ // interleaved frame across two chunks (that would produce a
410
+ // fractional `startSample` for the next chunk).
411
+ const rawMax = Math.floor(options.maxChunkBytes / 4);
412
+ const maxSamples = Math.max(channels, Math.floor(rawMax / channels) * channels);
413
+ samplesPerChunk = Math.max(channels, Math.min(samplesPerChunk, maxSamples));
414
+ }
415
+ const all = sanitizeFloat32(interleaveBuffer(processed.buffer, channels), options.normalizeAudio ?? true);
416
+ // Chunk timestamps are absolute (range start + offset) on every
417
+ // platform; progress is *elapsed within the range* so the
418
+ // `processedMs / durationMs` fraction stays in [0, 1] regardless of
419
+ // `startTimeMs`. The native decoders use the same split.
420
+ const rangeStartMs = options.startTimeMs ?? 0;
421
+ let chunkIndex = 0;
422
+ let emittedSamples = 0;
423
+ for (let off = 0; off < all.length; off += samplesPerChunk) {
424
+ if (cancelled()) {
425
+ return {
426
+ requestId,
427
+ durationMs,
428
+ sampleRate,
429
+ channels,
430
+ chunks: chunkIndex,
431
+ samples: emittedSamples,
432
+ cancelled: true,
433
+ };
434
+ }
435
+ const end = Math.min(off + samplesPerChunk, all.length);
436
+ const slice = all.slice(off, end);
437
+ const startSample = off / channels;
438
+ const endSample = end / channels;
439
+ const startMs = Math.round((startSample / sampleRate) * 1000) + rangeStartMs;
440
+ const endMs = Math.round((endSample / sampleRate) * 1000) + rangeStartMs;
441
+ const chunk = {
442
+ requestId,
443
+ chunkIndex,
444
+ startTimeMs: startMs,
445
+ endTimeMs: endMs,
446
+ durationMs: Math.round(((endSample - startSample) / sampleRate) * 1000),
447
+ startSample,
448
+ sampleCount: slice.length,
449
+ sampleRate,
450
+ channels,
451
+ samples: slice,
452
+ isFinal: end >= all.length,
453
+ };
454
+ await callbacks.onChunk(chunk);
455
+ // Resample rounding (Math.ceil in processAudioBuffer) can push
456
+ // elapsed past the source-rate-derived range duration on the tail
457
+ // chunk. Cap so onProgress consumers always see a [0, 1] ratio,
458
+ // matching the native `coerceIn(0, 1)` / `min(1, max(0, …))`
459
+ // clamp.
460
+ const rawElapsedMs = Math.round((endSample / sampleRate) * 1000);
461
+ const elapsedMs = durationMs > 0
462
+ ? Math.min(rawElapsedMs, durationMs)
463
+ : rawElapsedMs;
464
+ callbacks.onProgress?.({
465
+ requestId,
466
+ processedMs: elapsedMs,
467
+ durationMs,
468
+ progress: durationMs > 0
469
+ ? Math.min(1, Math.max(0, elapsedMs / durationMs))
470
+ : 1,
471
+ emittedChunks: chunkIndex + 1,
472
+ });
473
+ chunkIndex += 1;
474
+ emittedSamples += slice.length;
475
+ }
476
+ return {
477
+ requestId,
478
+ durationMs,
479
+ sampleRate,
480
+ channels,
481
+ chunks: chunkIndex,
482
+ samples: emittedSamples,
483
+ cancelled: false,
484
+ };
485
+ }
486
+ catch (err) {
487
+ throw (0, AudioStreamError_1.mapStreamError)(err, options.fileUri, 'web');
488
+ }
489
+ }
490
+ function interleaveBuffer(buffer, channels) {
491
+ const numCh = Math.max(1, Math.min(channels, buffer.numberOfChannels));
492
+ const framesPerCh = buffer.length;
493
+ if (numCh === 1) {
494
+ // Cheap path: clone channel 0 so downstream mutation doesn't touch the
495
+ // underlying AudioBuffer storage.
496
+ return new Float32Array(buffer.getChannelData(0));
497
+ }
498
+ const out = new Float32Array(framesPerCh * numCh);
499
+ const channelData = [];
500
+ for (let c = 0; c < numCh; c++) {
501
+ channelData.push(buffer.getChannelData(c));
502
+ }
503
+ for (let f = 0; f < framesPerCh; f++) {
504
+ for (let c = 0; c < numCh; c++) {
505
+ out[f * numCh + c] = channelData[c][f];
506
+ }
507
+ }
508
+ return out;
509
+ }
510
+ function sanitizeFloat32(input, clamp) {
511
+ if (!clamp) {
512
+ // still need NaN/Inf sanitation
513
+ for (let i = 0; i < input.length; i++) {
514
+ const v = input[i];
515
+ if (!Number.isFinite(v))
516
+ input[i] = 0;
517
+ }
518
+ return input;
519
+ }
520
+ for (let i = 0; i < input.length; i++) {
521
+ const v = input[i];
522
+ if (!Number.isFinite(v)) {
523
+ input[i] = 0;
524
+ }
525
+ else if (v > 1) {
526
+ input[i] = 1;
527
+ }
528
+ else if (v < -1) {
529
+ input[i] = -1;
530
+ }
531
+ }
532
+ return input;
533
+ }
534
+ //# sourceMappingURL=streamAudioData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamAudioData.js","sourceRoot":"","sources":["../../src/streamAudioData.ts"],"names":[],"mappings":"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","sourcesContent":["import { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'\n\nimport AudioStudioModule from './AudioStudioModule'\nimport { isWeb } from './constants'\nimport {\n AudioStreamError,\n AudioStreamErrorCode,\n mapStreamError,\n} from './errors/AudioStreamError'\nimport { processAudioBuffer } from './utils/audioProcessing'\n\n/**\n * High-level API: stream decoded audio from a stored file as bounded Float32\n * chunks without materializing the full PCM range in memory.\n *\n * See `docs/STREAM_AUDIO_DATA.md` for the full contract and rollout notes.\n */\nexport interface StreamAudioDataOptions {\n /** URI of the audio file to decode. */\n fileUri: string\n /** Start time in milliseconds (default: 0). */\n startTimeMs?: number\n /** End time in milliseconds (default: end-of-file). */\n endTimeMs?: number\n /**\n * Source sample rate hint. Ignored if `targetSampleRate` is set; native\n * decoders read the actual rate from the file.\n */\n sampleRate?: number\n /** Output sample rate. Native resamples when this differs from the file. */\n targetSampleRate?: number\n /** Output channel count (1 = mono downmix, 2 = stereo passthrough). */\n channels?: number\n /** Clamp samples to [-1, 1] and replace non-finite values with 0. */\n normalizeAudio?: boolean\n /** Target chunk duration in ms (default: 1000, min: 10, max: 60000). */\n chunkDurationMs?: number\n /** Soft cap on chunk size in bytes (Float32 = 4 bytes/sample). */\n maxChunkBytes?: number\n /** Max chunks queued in native before JS ack pauses decode (default: 4). */\n maxBufferedChunks?: number\n /**\n * Optional timeout for a chunk acknowledgement while backpressure is active.\n * Undefined/0 disables timeout so long transcription callbacks can run.\n */\n backpressureTimeoutMs?: number\n /** Output PCM format; only `'float32'` supported today. */\n streamFormat?: 'float32'\n /** Abort the in-flight request. Resolves promise with `cancelled: true`. */\n signal?: AbortSignal\n}\n\nexport interface StreamAudioDataChunk {\n /** Native request id; constant across all chunks of one call. */\n requestId: string\n /** Zero-based monotonic chunk index. */\n chunkIndex: number\n /** Start time in output-rate ms (rounded to nearest sample). */\n startTimeMs: number\n /** End time in output-rate ms. */\n endTimeMs: number\n /** Duration in ms (`endTimeMs - startTimeMs`). */\n durationMs: number\n /** First sample index in the output timeline. */\n startSample: number\n /** Sample count in `samples` (interleaved if channels > 1). */\n sampleCount: number\n /** Output sample rate. */\n sampleRate: number\n /** Output channel count. */\n channels: number\n /** Interleaved Float32 samples in [-1, 1]. */\n samples: Float32Array\n /** True for the last chunk of a non-cancelled run. */\n isFinal: boolean\n}\n\nexport interface StreamAudioDataProgress {\n requestId: string\n processedMs: number\n durationMs: number\n progress: number\n emittedChunks: number\n bufferedChunks?: number\n}\n\nexport interface StreamAudioDataResult {\n requestId: string\n durationMs: number\n sampleRate: number\n channels: number\n chunks: number\n samples: number\n cancelled: boolean\n}\n\nexport interface StreamAudioDataCallbacks {\n /**\n * Called with each decoded chunk. If this returns a Promise, native decode\n * pauses until it resolves (backpressure). Throwing aborts the stream with\n * `ERR_AUDIO_STREAM_DECODE_FAILED`.\n */\n onChunk: (chunk: StreamAudioDataChunk) => void | Promise<void>\n /** Called whenever native reports progress. */\n onProgress?: (progress: StreamAudioDataProgress) => void\n}\n\nexport interface AudioDecodeCapabilities {\n platform: 'ios' | 'android' | 'web'\n supportedInputFormats: string[]\n supportedOutputFormats: Array<'float32'>\n supportsCancellation: boolean\n supportsBackpressure: boolean\n supportsTimeRange: boolean\n supportsTargetSampleRate: boolean\n supportsChannelMixing: boolean\n knownLimitations?: string[]\n}\n\nconst CHUNK_EVENT = 'AudioDataStreamChunk'\nconst PROGRESS_EVENT = 'AudioDataStreamProgress'\nconst COMPLETE_EVENT = 'AudioDataStreamComplete'\nconst ERROR_EVENT = 'AudioDataStreamError'\n\nlet cachedEmitter: LegacyEventEmitter | null = null\nfunction getEmitter(): LegacyEventEmitter {\n if (!cachedEmitter) {\n cachedEmitter = new LegacyEventEmitter(AudioStudioModule)\n }\n return cachedEmitter\n}\n\nfunction generateRequestId(): string {\n const g = globalThis as { crypto?: { randomUUID?: () => string } }\n if (typeof g.crypto?.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n // fall through\n }\n }\n return `asd_${Date.now().toString(36)}_${Math.random()\n .toString(36)\n .slice(2, 10)}`\n}\n\nfunction toFloat32(samples: unknown): Float32Array {\n if (samples instanceof Float32Array) return samples\n if (Array.isArray(samples)) {\n const out = new Float32Array(samples.length)\n for (let i = 0; i < samples.length; i++) {\n const v = Number(samples[i])\n out[i] = Number.isFinite(v) ? v : 0\n }\n return out\n }\n if (typeof samples === 'string') {\n const bytes = base64ToBytes(samples)\n const floatLength = Math.floor(bytes.byteLength / 4)\n if (bytes.byteOffset % 4 === 0) {\n return new Float32Array(bytes.buffer, bytes.byteOffset, floatLength)\n }\n const sliced = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n )\n return new Float32Array(sliced, 0, Math.floor(sliced.byteLength / 4))\n }\n if (samples && typeof samples === 'object' && 'length' in samples) {\n // ArrayLike fallback\n const arr = samples as ArrayLike<number>\n const out = new Float32Array(arr.length)\n for (let i = 0; i < arr.length; i++) {\n const v = Number(arr[i])\n out[i] = Number.isFinite(v) ? v : 0\n }\n return out\n }\n return new Float32Array(0)\n}\n\nfunction base64ToBytes(input: string): Uint8Array {\n const g = globalThis as { atob?: (s: string) => string }\n if (typeof g.atob !== 'function') {\n // Buffer path for environments without atob; React Native has atob.\n const Buf = (\n globalThis as {\n Buffer?: {\n from: (input: string, encoding: string) => Uint8Array\n }\n }\n ).Buffer\n if (Buf) return new Uint8Array(Buf.from(input, 'base64'))\n return new Uint8Array(0)\n }\n const bin = g.atob(input)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\nfunction rejectInvalidRange(message: string): never {\n throw new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_INVALID_RANGE',\n message,\n recoverable: false,\n })\n}\n\nfunction assertPositiveFiniteOption(\n value: number | undefined,\n name: string,\n integer = false\n): void {\n if (value === undefined) return\n if (\n !Number.isFinite(value) ||\n value <= 0 ||\n (integer && !Number.isInteger(value))\n ) {\n rejectInvalidRange(\n `${name} must be a positive${integer ? ' integer' : ''}`\n )\n }\n}\n\nfunction validateOptions(options: StreamAudioDataOptions): void {\n if (!options.fileUri) {\n rejectInvalidRange('fileUri is required')\n }\n if (\n options.startTimeMs !== undefined &&\n options.endTimeMs !== undefined &&\n options.startTimeMs >= options.endTimeMs\n ) {\n rejectInvalidRange('startTimeMs must be < endTimeMs')\n }\n if (options.endTimeMs !== undefined && options.endTimeMs <= 0) {\n rejectInvalidRange('endTimeMs must be > 0')\n }\n if (options.startTimeMs !== undefined && options.startTimeMs < 0) {\n rejectInvalidRange('startTimeMs must be >= 0')\n }\n if (\n options.chunkDurationMs !== undefined &&\n (options.chunkDurationMs < 10 || options.chunkDurationMs > 60000)\n ) {\n rejectInvalidRange('chunkDurationMs must be in [10, 60000]')\n }\n if (\n options.backpressureTimeoutMs !== undefined &&\n options.backpressureTimeoutMs < 0\n ) {\n rejectInvalidRange('backpressureTimeoutMs must be >= 0')\n }\n assertPositiveFiniteOption(options.targetSampleRate, 'targetSampleRate')\n assertPositiveFiniteOption(options.sampleRate, 'sampleRate')\n assertPositiveFiniteOption(options.channels, 'channels', true)\n assertPositiveFiniteOption(\n options.maxBufferedChunks,\n 'maxBufferedChunks',\n true\n )\n assertPositiveFiniteOption(options.maxChunkBytes, 'maxChunkBytes', true)\n\n if (\n options.streamFormat !== undefined &&\n options.streamFormat !== 'float32'\n ) {\n throw new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',\n message: `Unsupported streamFormat: ${options.streamFormat}`,\n recoverable: false,\n })\n }\n}\n\n/**\n * Stream decoded audio from a stored file as bounded Float32 PCM chunks.\n *\n * Memory bound:\n * `chunkDurationMs * sampleRate * channels * 4 * maxBufferedChunks` +\n * native decoder buffers.\n *\n * Cancellation: pass `options.signal` and call `abort()`. The returned promise\n * resolves with `cancelled: true` (it does not reject) when cancellation wins.\n *\n * Backpressure: if `onChunk` returns a Promise, native decode is paused until\n * it resolves; if it throws, the stream is aborted with a `decode_failed` error.\n */\nexport async function streamAudioData(\n options: StreamAudioDataOptions,\n callbacks: StreamAudioDataCallbacks\n): Promise<StreamAudioDataResult> {\n validateOptions(options)\n\n if (isWeb) {\n return streamAudioDataWeb(options, callbacks)\n }\n\n return streamAudioDataNative(options, callbacks)\n}\n\n/** Discover what the running platform supports. */\nexport async function getAudioDecodeCapabilities(): Promise<AudioDecodeCapabilities> {\n if (isWeb) {\n return {\n platform: 'web',\n supportedInputFormats: [\n 'audio/wav',\n 'audio/mpeg',\n 'audio/mp4',\n 'audio/aac',\n 'audio/ogg',\n 'audio/webm',\n ],\n supportedOutputFormats: ['float32'],\n supportsCancellation: true,\n supportsBackpressure: true,\n supportsTimeRange: true,\n supportsTargetSampleRate: true,\n supportsChannelMixing: true,\n knownLimitations: [\n 'Web decodes the entire file via AudioContext.decodeAudioData before chunking; very long files may exceed browser memory.',\n ],\n }\n }\n if (typeof AudioStudioModule.getAudioDecodeCapabilities === 'function') {\n try {\n const caps = await AudioStudioModule.getAudioDecodeCapabilities()\n return caps as AudioDecodeCapabilities\n } catch (err) {\n throw mapStreamError(err, undefined, 'native')\n }\n }\n throw new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',\n message: 'getAudioDecodeCapabilities is not available on this build',\n recoverable: false,\n })\n}\n\nasync function streamAudioDataNative(\n options: StreamAudioDataOptions,\n callbacks: StreamAudioDataCallbacks\n): Promise<StreamAudioDataResult> {\n if (typeof AudioStudioModule.streamAudioData !== 'function') {\n throw new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',\n message:\n 'streamAudioData native method missing — rebuild the host app with the latest @siteed/audio-studio.',\n recoverable: false,\n })\n }\n\n const requestId = generateRequestId()\n const emitter = getEmitter()\n const subs: EventSubscription[] = []\n let chunkCount = 0\n let sampleCount = 0\n let processingChain: Promise<void> = Promise.resolve()\n let settled = false\n let abortListener: (() => void) | null = null\n let lastProgress: StreamAudioDataProgress | null = null\n\n const finalize = () => {\n for (const sub of subs) {\n try {\n sub.remove()\n } catch {\n /* noop */\n }\n }\n subs.length = 0\n if (abortListener && options.signal) {\n options.signal.removeEventListener('abort', abortListener)\n }\n }\n\n return new Promise<StreamAudioDataResult>((resolve, reject) => {\n const settle = (\n fn: () => void,\n mode: 'resolve' | 'reject',\n value: unknown\n ) => {\n if (settled) return\n settled = true\n finalize()\n fn()\n if (mode === 'resolve') {\n resolve(value as StreamAudioDataResult)\n } else {\n reject(value as Error)\n }\n }\n\n const handleError = (err: unknown) => {\n settle(\n () => {\n try {\n AudioStudioModule.cancelStreamAudioData?.(requestId)\n } catch {\n /* noop */\n }\n },\n 'reject',\n mapStreamError(err, options.fileUri, 'native')\n )\n }\n\n subs.push(\n emitter.addListener(CHUNK_EVENT, (raw: unknown) => {\n const evt = raw as {\n requestId: string\n chunkIndex: number\n startTimeMs: number\n endTimeMs: number\n startSample: number\n sampleCount: number\n sampleRate: number\n channels: number\n samples: Float32Array | number[] | string\n isFinal: boolean\n }\n if (evt.requestId !== requestId) return\n const chunk: StreamAudioDataChunk = {\n requestId: evt.requestId,\n chunkIndex: evt.chunkIndex,\n startTimeMs: evt.startTimeMs,\n endTimeMs: evt.endTimeMs,\n durationMs: evt.endTimeMs - evt.startTimeMs,\n startSample: evt.startSample,\n sampleCount: evt.sampleCount,\n sampleRate: evt.sampleRate,\n channels: evt.channels,\n samples: toFloat32(evt.samples),\n isFinal: evt.isFinal,\n }\n chunkCount += 1\n sampleCount += chunk.sampleCount\n\n processingChain = processingChain\n .then(async () => {\n await callbacks.onChunk(chunk)\n try {\n AudioStudioModule.acknowledgeStreamAudioChunk?.(\n requestId,\n chunk.chunkIndex\n )\n } catch {\n /* noop */\n }\n })\n .catch(handleError)\n })\n )\n\n if (callbacks.onProgress) {\n subs.push(\n emitter.addListener(PROGRESS_EVENT, (raw: unknown) => {\n const evt = raw as StreamAudioDataProgress\n if (evt.requestId !== requestId) return\n lastProgress = evt\n callbacks.onProgress!(evt)\n })\n )\n }\n\n subs.push(\n emitter.addListener(COMPLETE_EVENT, (raw: unknown) => {\n const evt = raw as {\n requestId: string\n durationMs: number\n sampleRate: number\n channels: number\n chunks?: number\n samples?: number\n cancelled: boolean\n }\n if (evt.requestId !== requestId) return\n // Wait for in-flight onChunk callbacks before resolving.\n processingChain\n .then(() => {\n settle(() => {}, 'resolve', {\n requestId,\n durationMs:\n evt.durationMs > 0\n ? evt.durationMs\n : (lastProgress?.durationMs ?? 0),\n sampleRate: evt.sampleRate,\n channels: evt.channels,\n chunks: evt.chunks ?? chunkCount,\n samples: evt.samples ?? sampleCount,\n cancelled: evt.cancelled,\n } satisfies StreamAudioDataResult)\n })\n .catch(handleError)\n })\n )\n\n subs.push(\n emitter.addListener(ERROR_EVENT, (raw: unknown) => {\n const evt = raw as {\n requestId: string\n code?: string\n message?: string\n nativeMessage?: string\n }\n if (evt.requestId !== requestId) return\n if (evt.code === 'ERR_AUDIO_STREAM_CANCELLED') {\n processingChain\n .catch(() => {})\n .then(() => {\n settle(() => {}, 'resolve', {\n requestId,\n durationMs: lastProgress?.durationMs ?? 0,\n sampleRate:\n options.targetSampleRate ??\n options.sampleRate ??\n 0,\n channels: options.channels ?? 1,\n chunks: chunkCount,\n samples: sampleCount,\n cancelled: true,\n } satisfies StreamAudioDataResult)\n })\n return\n }\n handleError(\n new AudioStreamError({\n code:\n (evt.code as AudioStreamErrorCode) ??\n 'ERR_AUDIO_STREAM_UNKNOWN',\n message: evt.message ?? 'native stream error',\n nativeMessage: evt.nativeMessage,\n fileUri: options.fileUri,\n platform: 'native',\n recoverable: false,\n })\n )\n })\n )\n\n if (options.signal) {\n if (options.signal.aborted) {\n settle(() => {}, 'resolve', {\n requestId,\n durationMs: lastProgress?.durationMs ?? 0,\n sampleRate:\n options.targetSampleRate ?? options.sampleRate ?? 0,\n channels: options.channels ?? 1,\n chunks: 0,\n samples: 0,\n cancelled: true,\n } satisfies StreamAudioDataResult)\n return\n }\n abortListener = () => {\n try {\n AudioStudioModule.cancelStreamAudioData?.(requestId)\n } catch {\n /* noop */\n }\n }\n options.signal.addEventListener('abort', abortListener)\n }\n\n const { signal: _signal, ...nativeOptions } = options\n AudioStudioModule.streamAudioData({\n ...nativeOptions,\n requestId,\n streamFormat: options.streamFormat ?? 'float32',\n chunkDurationMs: options.chunkDurationMs ?? 1000,\n maxBufferedChunks: options.maxBufferedChunks ?? 4,\n normalizeAudio: options.normalizeAudio ?? true,\n }).catch(handleError)\n })\n}\n\nasync function streamAudioDataWeb(\n options: StreamAudioDataOptions,\n callbacks: StreamAudioDataCallbacks\n): Promise<StreamAudioDataResult> {\n const requestId = generateRequestId()\n const cancelled = () => options.signal?.aborted === true\n if (cancelled()) {\n return {\n requestId,\n durationMs: 0,\n sampleRate: options.targetSampleRate ?? options.sampleRate ?? 0,\n channels: options.channels ?? 1,\n chunks: 0,\n samples: 0,\n cancelled: true,\n }\n }\n\n try {\n const processed = await processAudioBuffer({\n fileUri: options.fileUri,\n targetSampleRate: options.targetSampleRate,\n targetChannels: options.channels,\n normalizeAudio: options.normalizeAudio ?? true,\n startTimeMs: options.startTimeMs,\n endTimeMs: options.endTimeMs,\n })\n\n const sampleRate = processed.sampleRate\n const channels = processed.channels\n const durationMs = processed.durationMs\n const chunkDurationMs = options.chunkDurationMs ?? 1000\n let samplesPerChunk = Math.max(\n channels,\n Math.floor((chunkDurationMs / 1000) * sampleRate) * channels\n )\n if (options.maxChunkBytes) {\n // Round down to a multiple of `channels` so we never split an\n // interleaved frame across two chunks (that would produce a\n // fractional `startSample` for the next chunk).\n const rawMax = Math.floor(options.maxChunkBytes / 4)\n const maxSamples = Math.max(\n channels,\n Math.floor(rawMax / channels) * channels\n )\n samplesPerChunk = Math.max(\n channels,\n Math.min(samplesPerChunk, maxSamples)\n )\n }\n\n const all = sanitizeFloat32(\n interleaveBuffer(processed.buffer, channels),\n options.normalizeAudio ?? true\n )\n\n // Chunk timestamps are absolute (range start + offset) on every\n // platform; progress is *elapsed within the range* so the\n // `processedMs / durationMs` fraction stays in [0, 1] regardless of\n // `startTimeMs`. The native decoders use the same split.\n const rangeStartMs = options.startTimeMs ?? 0\n let chunkIndex = 0\n let emittedSamples = 0\n for (let off = 0; off < all.length; off += samplesPerChunk) {\n if (cancelled()) {\n return {\n requestId,\n durationMs,\n sampleRate,\n channels,\n chunks: chunkIndex,\n samples: emittedSamples,\n cancelled: true,\n }\n }\n const end = Math.min(off + samplesPerChunk, all.length)\n const slice = all.slice(off, end)\n const startSample = off / channels\n const endSample = end / channels\n const startMs =\n Math.round((startSample / sampleRate) * 1000) + rangeStartMs\n const endMs =\n Math.round((endSample / sampleRate) * 1000) + rangeStartMs\n const chunk: StreamAudioDataChunk = {\n requestId,\n chunkIndex,\n startTimeMs: startMs,\n endTimeMs: endMs,\n durationMs: Math.round(\n ((endSample - startSample) / sampleRate) * 1000\n ),\n startSample,\n sampleCount: slice.length,\n sampleRate,\n channels,\n samples: slice,\n isFinal: end >= all.length,\n }\n await callbacks.onChunk(chunk)\n // Resample rounding (Math.ceil in processAudioBuffer) can push\n // elapsed past the source-rate-derived range duration on the tail\n // chunk. Cap so onProgress consumers always see a [0, 1] ratio,\n // matching the native `coerceIn(0, 1)` / `min(1, max(0, …))`\n // clamp.\n const rawElapsedMs = Math.round((endSample / sampleRate) * 1000)\n const elapsedMs =\n durationMs > 0\n ? Math.min(rawElapsedMs, durationMs)\n : rawElapsedMs\n callbacks.onProgress?.({\n requestId,\n processedMs: elapsedMs,\n durationMs,\n progress:\n durationMs > 0\n ? Math.min(1, Math.max(0, elapsedMs / durationMs))\n : 1,\n emittedChunks: chunkIndex + 1,\n })\n chunkIndex += 1\n emittedSamples += slice.length\n }\n\n return {\n requestId,\n durationMs,\n sampleRate,\n channels,\n chunks: chunkIndex,\n samples: emittedSamples,\n cancelled: false,\n }\n } catch (err) {\n throw mapStreamError(err, options.fileUri, 'web')\n }\n}\n\nfunction interleaveBuffer(buffer: AudioBuffer, channels: number): Float32Array {\n const numCh = Math.max(1, Math.min(channels, buffer.numberOfChannels))\n const framesPerCh = buffer.length\n if (numCh === 1) {\n // Cheap path: clone channel 0 so downstream mutation doesn't touch the\n // underlying AudioBuffer storage.\n return new Float32Array(buffer.getChannelData(0))\n }\n const out = new Float32Array(framesPerCh * numCh)\n const channelData: Float32Array[] = []\n for (let c = 0; c < numCh; c++) {\n channelData.push(buffer.getChannelData(c))\n }\n for (let f = 0; f < framesPerCh; f++) {\n for (let c = 0; c < numCh; c++) {\n out[f * numCh + c] = channelData[c][f]\n }\n }\n return out\n}\n\nfunction sanitizeFloat32(input: Float32Array, clamp: boolean): Float32Array {\n if (!clamp) {\n // still need NaN/Inf sanitation\n for (let i = 0; i < input.length; i++) {\n const v = input[i]\n if (!Number.isFinite(v)) input[i] = 0\n }\n return input\n }\n for (let i = 0; i < input.length; i++) {\n const v = input[i]\n if (!Number.isFinite(v)) {\n input[i] = 0\n } else if (v > 1) {\n input[i] = 1\n } else if (v < -1) {\n input[i] = -1\n }\n }\n return input\n}\n"]}