@siteed/audio-studio 3.2.0-beta.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.
- package/CHANGELOG.md +356 -5
- package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +39 -6
- package/build/cjs/errors/AudioStreamError.js +9 -0
- package/build/cjs/errors/AudioStreamError.js.map +1 -1
- package/build/cjs/errors/AudioStreamError.test.js +22 -1
- package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
- package/build/cjs/streamAudioData.js +99 -32
- package/build/cjs/streamAudioData.js.map +1 -1
- package/build/cjs/utils/audioProcessing.js +14 -10
- package/build/cjs/utils/audioProcessing.js.map +1 -1
- package/build/esm/errors/AudioStreamError.js +9 -0
- package/build/esm/errors/AudioStreamError.js.map +1 -1
- package/build/esm/errors/AudioStreamError.test.js +22 -1
- package/build/esm/errors/AudioStreamError.test.js.map +1 -1
- package/build/esm/streamAudioData.js +99 -32
- package/build/esm/streamAudioData.js.map +1 -1
- package/build/esm/utils/audioProcessing.js +14 -10
- package/build/esm/utils/audioProcessing.js.map +1 -1
- package/build/types/errors/AudioStreamError.d.ts.map +1 -1
- package/build/types/streamAudioData.d.ts +5 -0
- package/build/types/streamAudioData.d.ts.map +1 -1
- package/build/types/utils/audioProcessing.d.ts +2 -2
- package/build/types/utils/audioProcessing.d.ts.map +1 -1
- package/ios/AudioStreamDecoder.swift +191 -100
- package/ios/AudioStudioModule.swift +48 -9
- package/package.json +163 -146
- package/scripts/README.md +58 -0
- package/src/errors/AudioStreamError.test.ts +29 -2
- package/src/errors/AudioStreamError.ts +14 -0
- package/src/streamAudioData.ts +146 -42
- package/src/utils/audioProcessing.ts +25 -14
- package/android/src/androidTest/assets/chorus.wav +0 -0
- package/android/src/androidTest/assets/jfk.wav +0 -0
- package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
- package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
- package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
- package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
- package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
- package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
- package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
- package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +0 -94
- package/android/src/test/resources/jfk.wav +0 -0
- package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
- package/android/src/test/resources/recorder_hello_world.wav +0 -0
- package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
- package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
- package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
- package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
- package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
- package/ios/AudioStudioTests/Info.plist +0 -22
- package/ios/AudioStudioTests/README.md +0 -39
- package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
- package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
- package/ios/tests/README.md +0 -41
- package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
- package/ios/tests/integration/buffer_duration_test.swift +0 -185
- package/ios/tests/integration/compressed_only_output_test.swift +0 -271
- package/ios/tests/integration/output_control_test.swift +0 -322
- package/ios/tests/integration/run_integration_tests.sh +0 -37
- package/ios/tests/opus_support_test_macos.swift +0 -154
- package/ios/tests/standalone/audio_processing_test.swift +0 -144
- package/ios/tests/standalone/audio_recording_test.swift +0 -277
- package/ios/tests/standalone/audio_streaming_test.swift +0 -249
- package/ios/tests/standalone/standalone_test.swift +0 -144
|
@@ -664,6 +664,21 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
|
|
|
664
664
|
// while keeping the same module instance, and a fully cancelled
|
|
665
665
|
// scope would silently no-op every subsequent launch.
|
|
666
666
|
coroutineScope.coroutineContext.cancelChildren()
|
|
667
|
+
// Cancel any in-flight streamAudioData decoders before teardown.
|
|
668
|
+
// Their worker threads are not children of `coroutineScope` and
|
|
669
|
+
// would otherwise keep running, emitting events through a
|
|
670
|
+
// destroyed module. We clear the map *before* cancelling so that
|
|
671
|
+
// any terminal events the worker emits after observing
|
|
672
|
+
// `cancel()` are dropped by `streamDecoderEmit` (which gates on
|
|
673
|
+
// map membership). The map itself stays usable so Expo can
|
|
674
|
+
// recreate decoders on a dev-client reload without resetting any
|
|
675
|
+
// global flag.
|
|
676
|
+
val inflight = synchronized(streamDecodersLock) {
|
|
677
|
+
val snapshot = streamDecoders.values.toList()
|
|
678
|
+
streamDecoders.clear()
|
|
679
|
+
snapshot
|
|
680
|
+
}
|
|
681
|
+
inflight.forEach { it.cancel() }
|
|
667
682
|
AudioRecorderManager.destroy()
|
|
668
683
|
}
|
|
669
684
|
|
|
@@ -1021,20 +1036,29 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
|
|
|
1021
1036
|
val context = appContext.reactContext
|
|
1022
1037
|
?: throw IllegalStateException("React context not available")
|
|
1023
1038
|
|
|
1039
|
+
val chunkDurationMs = (options["chunkDurationMs"] as? Number)?.toInt() ?: 1000
|
|
1040
|
+
if (chunkDurationMs !in 10..60000) {
|
|
1041
|
+
promise.reject(
|
|
1042
|
+
"ERR_AUDIO_STREAM_INVALID_RANGE",
|
|
1043
|
+
"chunkDurationMs must be in [10, 60000]",
|
|
1044
|
+
null
|
|
1045
|
+
)
|
|
1046
|
+
return@AsyncFunction
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1024
1049
|
val decoderOptions = AudioStreamDecoder.Options(
|
|
1025
1050
|
requestId = requestId,
|
|
1026
1051
|
fileUri = fileUri,
|
|
1027
1052
|
startTimeMs = (options["startTimeMs"] as? Number)?.toLong(),
|
|
1028
1053
|
endTimeMs = (options["endTimeMs"] as? Number)?.toLong(),
|
|
1029
|
-
targetSampleRate = (options["targetSampleRate"] as? Number)?.toInt()
|
|
1030
|
-
?: (options["sampleRate"] as? Number)?.toInt(),
|
|
1054
|
+
targetSampleRate = (options["targetSampleRate"] as? Number)?.toInt(),
|
|
1031
1055
|
channels = (options["channels"] as? Number)?.toInt(),
|
|
1032
1056
|
normalizeAudio = (options["normalizeAudio"] as? Boolean) ?: true,
|
|
1033
|
-
chunkDurationMs =
|
|
1034
|
-
.coerceIn(10, 60000),
|
|
1057
|
+
chunkDurationMs = chunkDurationMs,
|
|
1035
1058
|
maxChunkBytes = (options["maxChunkBytes"] as? Number)?.toInt(),
|
|
1036
1059
|
maxBufferedChunks = ((options["maxBufferedChunks"] as? Number)?.toInt() ?: 4)
|
|
1037
1060
|
.coerceAtLeast(1),
|
|
1061
|
+
backpressureTimeoutMs = (options["backpressureTimeoutMs"] as? Number)?.toLong(),
|
|
1038
1062
|
)
|
|
1039
1063
|
|
|
1040
1064
|
val decoder = AudioStreamDecoder(context, decoderOptions, this@AudioStudioModule)
|
|
@@ -1113,14 +1137,23 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
|
|
|
1113
1137
|
}
|
|
1114
1138
|
|
|
1115
1139
|
override fun streamDecoderEmit(eventName: String, payload: Bundle) {
|
|
1140
|
+
// Drop events from decoders that are no longer tracked. This catches
|
|
1141
|
+
// both the OnDestroy teardown path (map cleared before `cancel()`) and
|
|
1142
|
+
// post-completion stragglers, without depending on a one-way shutdown
|
|
1143
|
+
// flag that would survive Expo's `definition()` re-runs.
|
|
1144
|
+
val requestId = payload.getString("requestId") ?: return
|
|
1145
|
+
val isActive = synchronized(streamDecodersLock) {
|
|
1146
|
+
streamDecoders.containsKey(requestId)
|
|
1147
|
+
}
|
|
1148
|
+
if (!isActive) return
|
|
1116
1149
|
when (eventName) {
|
|
1117
1150
|
Constants.AUDIO_STREAM_COMPLETE_EVENT -> {
|
|
1118
|
-
(
|
|
1151
|
+
releaseStreamDecoder(requestId)
|
|
1119
1152
|
}
|
|
1120
1153
|
Constants.AUDIO_STREAM_ERROR_EVENT -> {
|
|
1121
1154
|
val code = payload.getString("code") ?: ""
|
|
1122
1155
|
if (code != "ERR_AUDIO_STREAM_CANCELLED") {
|
|
1123
|
-
|
|
1156
|
+
releaseStreamDecoder(requestId)
|
|
1124
1157
|
}
|
|
1125
1158
|
}
|
|
1126
1159
|
}
|
|
@@ -58,6 +58,12 @@ function getNativeCode(err) {
|
|
|
58
58
|
}
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
61
|
+
function isUnknownAudioStreamCode(raw) {
|
|
62
|
+
if (!raw)
|
|
63
|
+
return false;
|
|
64
|
+
return (raw.toUpperCase().startsWith('ERR_AUDIO_STREAM_') &&
|
|
65
|
+
normalizeCode(raw) === null);
|
|
66
|
+
}
|
|
61
67
|
function normalizeCode(raw) {
|
|
62
68
|
if (!raw)
|
|
63
69
|
return null;
|
|
@@ -118,6 +124,9 @@ function mapStreamError(err, fileUri, platform) {
|
|
|
118
124
|
const nativeMessage = getNativeMessage(err);
|
|
119
125
|
const nativeCode = getNativeCode(err);
|
|
120
126
|
const lower = nativeMessage.toLowerCase();
|
|
127
|
+
if (isUnknownAudioStreamCode(nativeCode)) {
|
|
128
|
+
console.warn(`[AudioStreamError] Unknown native audio stream error code: ${nativeCode}`);
|
|
129
|
+
}
|
|
121
130
|
let code = normalizeCode(nativeCode) ??
|
|
122
131
|
normalizeCode(nativeMessage) ??
|
|
123
132
|
'ERR_AUDIO_STREAM_UNKNOWN';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioStreamError.js","sourceRoot":"","sources":["../../../src/errors/AudioStreamError.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stable typed errors for `streamAudioData`. Callers can switch on `code`.\n */\nexport type AudioStreamErrorCode =\n | 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n | 'ERR_AUDIO_STREAM_INVALID_RANGE'\n | 'ERR_AUDIO_STREAM_DECODE_FAILED'\n | 'ERR_AUDIO_STREAM_CANCELLED'\n | 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n | 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n | 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'\n | 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE'\n | 'ERR_AUDIO_STREAM_BUSY'\n | 'ERR_AUDIO_STREAM_UNKNOWN'\n\nexport interface AudioStreamErrorPayload {\n code: AudioStreamErrorCode\n message: string\n recoverable: boolean\n fileUri?: string\n platform?: string\n nativeCode?: string\n nativeMessage?: string\n}\n\nconst RECOVERABLE: AudioStreamErrorCode[] = [\n 'ERR_AUDIO_STREAM_CANCELLED',\n 'ERR_AUDIO_STREAM_BUSY',\n 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',\n 'ERR_AUDIO_STREAM_PERMISSION_DENIED',\n]\n\nexport class AudioStreamError extends Error {\n readonly code: AudioStreamErrorCode\n readonly recoverable: boolean\n readonly fileUri?: string\n readonly platform?: string\n readonly nativeCode?: string\n readonly nativeMessage?: string\n\n constructor(payload: AudioStreamErrorPayload) {\n super(payload.message)\n this.name = 'AudioStreamError'\n this.code = payload.code\n this.recoverable = payload.recoverable\n this.fileUri = payload.fileUri\n this.platform = payload.platform\n this.nativeCode = payload.nativeCode\n this.nativeMessage = payload.nativeMessage\n }\n\n toJSON(): AudioStreamErrorPayload {\n return {\n code: this.code,\n message: this.message,\n recoverable: this.recoverable,\n fileUri: this.fileUri,\n platform: this.platform,\n nativeCode: this.nativeCode,\n nativeMessage: this.nativeMessage,\n }\n }\n}\n\nfunction getNativeMessage(err: unknown): string {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n try {\n return JSON.stringify(err) ?? String(err)\n } catch {\n return String(err)\n }\n}\n\nfunction getNativeCode(err: unknown): string | undefined {\n if (err && typeof err === 'object' && 'code' in err) {\n const code = (err as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n return undefined\n}\n\nfunction normalizeCode(raw: string | undefined): AudioStreamErrorCode | null {\n if (!raw) return null\n const upper = raw.toUpperCase()\n if (upper.startsWith('ERR_AUDIO_STREAM_')) {\n const known: AudioStreamErrorCode[] = [\n 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',\n 'ERR_AUDIO_STREAM_INVALID_RANGE',\n 'ERR_AUDIO_STREAM_DECODE_FAILED',\n 'ERR_AUDIO_STREAM_CANCELLED',\n 'ERR_AUDIO_STREAM_PERMISSION_DENIED',\n 'ERR_AUDIO_STREAM_FILE_NOT_FOUND',\n 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',\n 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',\n 'ERR_AUDIO_STREAM_BUSY',\n 'ERR_AUDIO_STREAM_UNKNOWN',\n ]\n if ((known as string[]).includes(upper)) {\n return upper as AudioStreamErrorCode\n }\n }\n if (upper.includes('FILE_NOT_FOUND') || upper === 'ENOENT') {\n return 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n }\n if (upper.includes('PERMISSION') || upper === 'EACCES') {\n return 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n }\n if (\n upper.includes('UNSUPPORTED') ||\n upper.includes('NO_SUITABLE_CODEC') ||\n upper.includes('NO SUITABLE CODEC') ||\n upper.includes('NOT SUPPORTED')\n ) {\n return 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n }\n if (\n upper.includes('INVALID_RANGE') ||\n upper.includes('OUT_OF_RANGE') ||\n upper.includes('INVALID_TIME')\n ) {\n return 'ERR_AUDIO_STREAM_INVALID_RANGE'\n }\n if (upper.includes('CANCELLED') || upper.includes('CANCELED')) {\n return 'ERR_AUDIO_STREAM_CANCELLED'\n }\n if (upper.includes('BUSY')) {\n return 'ERR_AUDIO_STREAM_BUSY'\n }\n if (upper.includes('BACKPRESSURE')) {\n return 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'\n }\n if (\n upper.includes('DECODE') ||\n upper.includes('CODEC') ||\n upper.includes('MALFORMED')\n ) {\n return 'ERR_AUDIO_STREAM_DECODE_FAILED'\n }\n return null\n}\n\nexport function mapStreamError(\n err: unknown,\n fileUri?: string,\n platform?: string\n): AudioStreamError {\n if (err instanceof AudioStreamError) return err\n\n const nativeMessage = getNativeMessage(err)\n const nativeCode = getNativeCode(err)\n const lower = nativeMessage.toLowerCase()\n\n let code =\n normalizeCode(nativeCode) ??\n normalizeCode(nativeMessage) ??\n 'ERR_AUDIO_STREAM_UNKNOWN'\n\n if (code === 'ERR_AUDIO_STREAM_UNKNOWN') {\n if (lower.includes('not found') || lower.includes('does not exist')) {\n code = 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n } else if (\n lower.includes('unsupported') ||\n lower.includes('no suitable codec')\n ) {\n code = 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n } else if (lower.includes('permission') || lower.includes('denied')) {\n code = 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n } else if (lower.includes('decode') || lower.includes('codec')) {\n code = 'ERR_AUDIO_STREAM_DECODE_FAILED'\n } else if (lower.includes('cancel')) {\n code = 'ERR_AUDIO_STREAM_CANCELLED'\n }\n }\n\n return new AudioStreamError({\n code,\n message: `Audio stream failed (${code}): ${nativeMessage}`,\n recoverable: RECOVERABLE.includes(code),\n fileUri,\n platform,\n nativeCode,\n nativeMessage,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AudioStreamError.js","sourceRoot":"","sources":["../../../src/errors/AudioStreamError.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stable typed errors for `streamAudioData`. Callers can switch on `code`.\n */\nexport type AudioStreamErrorCode =\n | 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n | 'ERR_AUDIO_STREAM_INVALID_RANGE'\n | 'ERR_AUDIO_STREAM_DECODE_FAILED'\n | 'ERR_AUDIO_STREAM_CANCELLED'\n | 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n | 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n | 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'\n | 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE'\n | 'ERR_AUDIO_STREAM_BUSY'\n | 'ERR_AUDIO_STREAM_UNKNOWN'\n\nexport interface AudioStreamErrorPayload {\n code: AudioStreamErrorCode\n message: string\n recoverable: boolean\n fileUri?: string\n platform?: string\n nativeCode?: string\n nativeMessage?: string\n}\n\nconst RECOVERABLE: AudioStreamErrorCode[] = [\n 'ERR_AUDIO_STREAM_CANCELLED',\n 'ERR_AUDIO_STREAM_BUSY',\n 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',\n 'ERR_AUDIO_STREAM_PERMISSION_DENIED',\n]\n\nexport class AudioStreamError extends Error {\n readonly code: AudioStreamErrorCode\n readonly recoverable: boolean\n readonly fileUri?: string\n readonly platform?: string\n readonly nativeCode?: string\n readonly nativeMessage?: string\n\n constructor(payload: AudioStreamErrorPayload) {\n super(payload.message)\n this.name = 'AudioStreamError'\n this.code = payload.code\n this.recoverable = payload.recoverable\n this.fileUri = payload.fileUri\n this.platform = payload.platform\n this.nativeCode = payload.nativeCode\n this.nativeMessage = payload.nativeMessage\n }\n\n toJSON(): AudioStreamErrorPayload {\n return {\n code: this.code,\n message: this.message,\n recoverable: this.recoverable,\n fileUri: this.fileUri,\n platform: this.platform,\n nativeCode: this.nativeCode,\n nativeMessage: this.nativeMessage,\n }\n }\n}\n\nfunction getNativeMessage(err: unknown): string {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n try {\n return JSON.stringify(err) ?? String(err)\n } catch {\n return String(err)\n }\n}\n\nfunction getNativeCode(err: unknown): string | undefined {\n if (err && typeof err === 'object' && 'code' in err) {\n const code = (err as { code?: unknown }).code\n if (typeof code === 'string') return code\n }\n return undefined\n}\n\nfunction isUnknownAudioStreamCode(raw: string | undefined): boolean {\n if (!raw) return false\n return (\n raw.toUpperCase().startsWith('ERR_AUDIO_STREAM_') &&\n normalizeCode(raw) === null\n )\n}\n\nfunction normalizeCode(raw: string | undefined): AudioStreamErrorCode | null {\n if (!raw) return null\n const upper = raw.toUpperCase()\n if (upper.startsWith('ERR_AUDIO_STREAM_')) {\n const known: AudioStreamErrorCode[] = [\n 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',\n 'ERR_AUDIO_STREAM_INVALID_RANGE',\n 'ERR_AUDIO_STREAM_DECODE_FAILED',\n 'ERR_AUDIO_STREAM_CANCELLED',\n 'ERR_AUDIO_STREAM_PERMISSION_DENIED',\n 'ERR_AUDIO_STREAM_FILE_NOT_FOUND',\n 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',\n 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',\n 'ERR_AUDIO_STREAM_BUSY',\n 'ERR_AUDIO_STREAM_UNKNOWN',\n ]\n if ((known as string[]).includes(upper)) {\n return upper as AudioStreamErrorCode\n }\n }\n if (upper.includes('FILE_NOT_FOUND') || upper === 'ENOENT') {\n return 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n }\n if (upper.includes('PERMISSION') || upper === 'EACCES') {\n return 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n }\n if (\n upper.includes('UNSUPPORTED') ||\n upper.includes('NO_SUITABLE_CODEC') ||\n upper.includes('NO SUITABLE CODEC') ||\n upper.includes('NOT SUPPORTED')\n ) {\n return 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n }\n if (\n upper.includes('INVALID_RANGE') ||\n upper.includes('OUT_OF_RANGE') ||\n upper.includes('INVALID_TIME')\n ) {\n return 'ERR_AUDIO_STREAM_INVALID_RANGE'\n }\n if (upper.includes('CANCELLED') || upper.includes('CANCELED')) {\n return 'ERR_AUDIO_STREAM_CANCELLED'\n }\n if (upper.includes('BUSY')) {\n return 'ERR_AUDIO_STREAM_BUSY'\n }\n if (upper.includes('BACKPRESSURE')) {\n return 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'\n }\n if (\n upper.includes('DECODE') ||\n upper.includes('CODEC') ||\n upper.includes('MALFORMED')\n ) {\n return 'ERR_AUDIO_STREAM_DECODE_FAILED'\n }\n return null\n}\n\nexport function mapStreamError(\n err: unknown,\n fileUri?: string,\n platform?: string\n): AudioStreamError {\n if (err instanceof AudioStreamError) return err\n\n const nativeMessage = getNativeMessage(err)\n const nativeCode = getNativeCode(err)\n const lower = nativeMessage.toLowerCase()\n\n if (isUnknownAudioStreamCode(nativeCode)) {\n console.warn(\n `[AudioStreamError] Unknown native audio stream error code: ${nativeCode}`\n )\n }\n\n let code =\n normalizeCode(nativeCode) ??\n normalizeCode(nativeMessage) ??\n 'ERR_AUDIO_STREAM_UNKNOWN'\n\n if (code === 'ERR_AUDIO_STREAM_UNKNOWN') {\n if (lower.includes('not found') || lower.includes('does not exist')) {\n code = 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'\n } else if (\n lower.includes('unsupported') ||\n lower.includes('no suitable codec')\n ) {\n code = 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'\n } else if (lower.includes('permission') || lower.includes('denied')) {\n code = 'ERR_AUDIO_STREAM_PERMISSION_DENIED'\n } else if (lower.includes('decode') || lower.includes('codec')) {\n code = 'ERR_AUDIO_STREAM_DECODE_FAILED'\n } else if (lower.includes('cancel')) {\n code = 'ERR_AUDIO_STREAM_CANCELLED'\n }\n }\n\n return new AudioStreamError({\n code,\n message: `Audio stream failed (${code}): ${nativeMessage}`,\n recoverable: RECOVERABLE.includes(code),\n fileUri,\n platform,\n nativeCode,\n nativeMessage,\n })\n}\n"]}
|
|
@@ -11,7 +11,10 @@ describe('AudioStreamError', () => {
|
|
|
11
11
|
expect((0, AudioStreamError_1.mapStreamError)(original)).toBe(original);
|
|
12
12
|
});
|
|
13
13
|
it('maps native FILE_NOT_FOUND code', () => {
|
|
14
|
-
const mapped = (0, AudioStreamError_1.mapStreamError)({
|
|
14
|
+
const mapped = (0, AudioStreamError_1.mapStreamError)({
|
|
15
|
+
code: 'FILE_NOT_FOUND',
|
|
16
|
+
message: 'gone',
|
|
17
|
+
});
|
|
15
18
|
expect(mapped.code).toBe('ERR_AUDIO_STREAM_FILE_NOT_FOUND');
|
|
16
19
|
expect(mapped.recoverable).toBe(false);
|
|
17
20
|
});
|
|
@@ -27,10 +30,28 @@ describe('AudioStreamError', () => {
|
|
|
27
30
|
expect(mapped.code).toBe('ERR_AUDIO_STREAM_CANCELLED');
|
|
28
31
|
expect(mapped.recoverable).toBe(true);
|
|
29
32
|
});
|
|
33
|
+
it('maps backpressure timeout as recoverable', () => {
|
|
34
|
+
const mapped = (0, AudioStreamError_1.mapStreamError)({
|
|
35
|
+
code: 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',
|
|
36
|
+
message: 'ack timed out',
|
|
37
|
+
});
|
|
38
|
+
expect(mapped.code).toBe('ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT');
|
|
39
|
+
expect(mapped.recoverable).toBe(true);
|
|
40
|
+
});
|
|
30
41
|
it('falls back to UNKNOWN', () => {
|
|
31
42
|
const mapped = (0, AudioStreamError_1.mapStreamError)({});
|
|
32
43
|
expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN');
|
|
33
44
|
});
|
|
45
|
+
it('warns when native returns an unknown audio stream code', () => {
|
|
46
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
47
|
+
const mapped = (0, AudioStreamError_1.mapStreamError)({
|
|
48
|
+
code: 'ERR_AUDIO_STREAM_FOOBAR',
|
|
49
|
+
message: 'new native code',
|
|
50
|
+
});
|
|
51
|
+
expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN');
|
|
52
|
+
expect(warn).toHaveBeenCalledWith('[AudioStreamError] Unknown native audio stream error code: ERR_AUDIO_STREAM_FOOBAR');
|
|
53
|
+
warn.mockRestore();
|
|
54
|
+
});
|
|
34
55
|
it('preserves nativeCode and nativeMessage', () => {
|
|
35
56
|
const mapped = (0, AudioStreamError_1.mapStreamError)({
|
|
36
57
|
code: 'WEIRD_NATIVE_CODE',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioStreamError.test.js","sourceRoot":"","sources":["../../../src/errors/AudioStreamError.test.ts"],"names":[],"mappings":";;AAAA,yDAAqE;AAErE,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC7D,MAAM,QAAQ,GAAG,IAAI,mCAAgB,CAAC;YAClC,IAAI,EAAE,4BAA4B;YAClC,OAAO,EAAE,SAAS;YAClB,WAAW,EAAE,IAAI;SACpB,CAAC,CAAA;QACF,MAAM,CAAC,IAAA,iCAAc,EAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACvC,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC,
|
|
1
|
+
{"version":3,"file":"AudioStreamError.test.js","sourceRoot":"","sources":["../../../src/errors/AudioStreamError.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { AudioStreamError, mapStreamError } from './AudioStreamError'\n\ndescribe('AudioStreamError', () => {\n it('passes through an existing AudioStreamError unchanged', () => {\n const original = new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_CANCELLED',\n message: 'aborted',\n recoverable: true,\n })\n expect(mapStreamError(original)).toBe(original)\n })\n\n it('maps native FILE_NOT_FOUND code', () => {\n const mapped = mapStreamError({\n code: 'FILE_NOT_FOUND',\n message: 'gone',\n })\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_FILE_NOT_FOUND')\n expect(mapped.recoverable).toBe(false)\n })\n\n it('maps unsupported codec text', () => {\n const mapped = mapStreamError(\n new Error('No suitable codec for audio/opus')\n )\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT')\n })\n\n it('marks cancellation as recoverable', () => {\n const mapped = mapStreamError({\n code: 'ERR_AUDIO_STREAM_CANCELLED',\n message: 'user cancelled',\n })\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_CANCELLED')\n expect(mapped.recoverable).toBe(true)\n })\n\n it('maps backpressure timeout as recoverable', () => {\n const mapped = mapStreamError({\n code: 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',\n message: 'ack timed out',\n })\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT')\n expect(mapped.recoverable).toBe(true)\n })\n\n it('falls back to UNKNOWN', () => {\n const mapped = mapStreamError({})\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')\n })\n\n it('warns when native returns an unknown audio stream code', () => {\n const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})\n const mapped = mapStreamError({\n code: 'ERR_AUDIO_STREAM_FOOBAR',\n message: 'new native code',\n })\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')\n expect(warn).toHaveBeenCalledWith(\n '[AudioStreamError] Unknown native audio stream error code: ERR_AUDIO_STREAM_FOOBAR'\n )\n warn.mockRestore()\n })\n\n it('preserves nativeCode and nativeMessage', () => {\n const mapped = mapStreamError({\n code: 'WEIRD_NATIVE_CODE',\n message: 'something went wrong on the bridge',\n })\n expect(mapped.nativeCode).toBe('WEIRD_NATIVE_CODE')\n expect(mapped.nativeMessage).toContain('bridge')\n })\n\n it('serialises to a stable JSON payload', () => {\n const err = new AudioStreamError({\n code: 'ERR_AUDIO_STREAM_DECODE_FAILED',\n message: 'decoder bust',\n recoverable: false,\n fileUri: 'file:///a.m4a',\n platform: 'ios',\n })\n expect(err.toJSON()).toEqual({\n code: 'ERR_AUDIO_STREAM_DECODE_FAILED',\n message: 'decoder bust',\n recoverable: false,\n fileUri: 'file:///a.m4a',\n platform: 'ios',\n nativeCode: undefined,\n nativeMessage: undefined,\n })\n })\n})\n"]}
|
|
@@ -48,10 +48,12 @@ function toFloat32(samples) {
|
|
|
48
48
|
}
|
|
49
49
|
if (typeof samples === 'string') {
|
|
50
50
|
const bytes = base64ToBytes(samples);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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));
|
|
55
57
|
}
|
|
56
58
|
if (samples && typeof samples === 'object' && 'length' in samples) {
|
|
57
59
|
// ArrayLike fallback
|
|
@@ -80,31 +82,50 @@ function base64ToBytes(input) {
|
|
|
80
82
|
out[i] = bin.charCodeAt(i);
|
|
81
83
|
return out;
|
|
82
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
|
+
}
|
|
83
101
|
function validateOptions(options) {
|
|
84
102
|
if (!options.fileUri) {
|
|
85
|
-
|
|
86
|
-
code: 'ERR_AUDIO_STREAM_INVALID_RANGE',
|
|
87
|
-
message: 'fileUri is required',
|
|
88
|
-
recoverable: false,
|
|
89
|
-
});
|
|
103
|
+
rejectInvalidRange('fileUri is required');
|
|
90
104
|
}
|
|
91
105
|
if (options.startTimeMs !== undefined &&
|
|
92
106
|
options.endTimeMs !== undefined &&
|
|
93
107
|
options.startTimeMs >= options.endTimeMs) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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');
|
|
99
115
|
}
|
|
100
116
|
if (options.chunkDurationMs !== undefined &&
|
|
101
117
|
(options.chunkDurationMs < 10 || options.chunkDurationMs > 60000)) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
rejectInvalidRange('chunkDurationMs must be in [10, 60000]');
|
|
119
|
+
}
|
|
120
|
+
if (options.backpressureTimeoutMs !== undefined &&
|
|
121
|
+
options.backpressureTimeoutMs < 0) {
|
|
122
|
+
rejectInvalidRange('backpressureTimeoutMs must be >= 0');
|
|
107
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);
|
|
108
129
|
if (options.streamFormat !== undefined &&
|
|
109
130
|
options.streamFormat !== 'float32') {
|
|
110
131
|
throw new AudioStreamError_1.AudioStreamError({
|
|
@@ -189,6 +210,7 @@ async function streamAudioDataNative(options, callbacks) {
|
|
|
189
210
|
let processingChain = Promise.resolve();
|
|
190
211
|
let settled = false;
|
|
191
212
|
let abortListener = null;
|
|
213
|
+
let lastProgress = null;
|
|
192
214
|
const finalize = () => {
|
|
193
215
|
for (const sub of subs) {
|
|
194
216
|
try {
|
|
@@ -263,6 +285,7 @@ async function streamAudioDataNative(options, callbacks) {
|
|
|
263
285
|
const evt = raw;
|
|
264
286
|
if (evt.requestId !== requestId)
|
|
265
287
|
return;
|
|
288
|
+
lastProgress = evt;
|
|
266
289
|
callbacks.onProgress(evt);
|
|
267
290
|
}));
|
|
268
291
|
}
|
|
@@ -275,7 +298,9 @@ async function streamAudioDataNative(options, callbacks) {
|
|
|
275
298
|
.then(() => {
|
|
276
299
|
settle(() => { }, 'resolve', {
|
|
277
300
|
requestId,
|
|
278
|
-
durationMs: evt.durationMs
|
|
301
|
+
durationMs: evt.durationMs > 0
|
|
302
|
+
? evt.durationMs
|
|
303
|
+
: (lastProgress?.durationMs ?? 0),
|
|
279
304
|
sampleRate: evt.sampleRate,
|
|
280
305
|
channels: evt.channels,
|
|
281
306
|
chunks: evt.chunks ?? chunkCount,
|
|
@@ -295,7 +320,7 @@ async function streamAudioDataNative(options, callbacks) {
|
|
|
295
320
|
.then(() => {
|
|
296
321
|
settle(() => { }, 'resolve', {
|
|
297
322
|
requestId,
|
|
298
|
-
durationMs: 0,
|
|
323
|
+
durationMs: lastProgress?.durationMs ?? 0,
|
|
299
324
|
sampleRate: options.targetSampleRate ??
|
|
300
325
|
options.sampleRate ??
|
|
301
326
|
0,
|
|
@@ -321,7 +346,7 @@ async function streamAudioDataNative(options, callbacks) {
|
|
|
321
346
|
if (options.signal.aborted) {
|
|
322
347
|
settle(() => { }, 'resolve', {
|
|
323
348
|
requestId,
|
|
324
|
-
durationMs: 0,
|
|
349
|
+
durationMs: lastProgress?.durationMs ?? 0,
|
|
325
350
|
sampleRate: options.targetSampleRate ?? options.sampleRate ?? 0,
|
|
326
351
|
channels: options.channels ?? 1,
|
|
327
352
|
chunks: 0,
|
|
@@ -368,8 +393,8 @@ async function streamAudioDataWeb(options, callbacks) {
|
|
|
368
393
|
try {
|
|
369
394
|
const processed = await (0, audioProcessing_1.processAudioBuffer)({
|
|
370
395
|
fileUri: options.fileUri,
|
|
371
|
-
targetSampleRate: options.targetSampleRate
|
|
372
|
-
targetChannels: options.channels
|
|
396
|
+
targetSampleRate: options.targetSampleRate,
|
|
397
|
+
targetChannels: options.channels,
|
|
373
398
|
normalizeAudio: options.normalizeAudio ?? true,
|
|
374
399
|
startTimeMs: options.startTimeMs,
|
|
375
400
|
endTimeMs: options.endTimeMs,
|
|
@@ -378,12 +403,21 @@ async function streamAudioDataWeb(options, callbacks) {
|
|
|
378
403
|
const channels = processed.channels;
|
|
379
404
|
const durationMs = processed.durationMs;
|
|
380
405
|
const chunkDurationMs = options.chunkDurationMs ?? 1000;
|
|
381
|
-
let samplesPerChunk = Math.max(
|
|
406
|
+
let samplesPerChunk = Math.max(channels, Math.floor((chunkDurationMs / 1000) * sampleRate) * channels);
|
|
382
407
|
if (options.maxChunkBytes) {
|
|
383
|
-
|
|
384
|
-
|
|
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));
|
|
385
414
|
}
|
|
386
|
-
const all = sanitizeFloat32(processed.
|
|
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;
|
|
387
421
|
let chunkIndex = 0;
|
|
388
422
|
let emittedSamples = 0;
|
|
389
423
|
for (let off = 0; off < all.length; off += samplesPerChunk) {
|
|
@@ -402,11 +436,13 @@ async function streamAudioDataWeb(options, callbacks) {
|
|
|
402
436
|
const slice = all.slice(off, end);
|
|
403
437
|
const startSample = off / channels;
|
|
404
438
|
const endSample = end / channels;
|
|
439
|
+
const startMs = Math.round((startSample / sampleRate) * 1000) + rangeStartMs;
|
|
440
|
+
const endMs = Math.round((endSample / sampleRate) * 1000) + rangeStartMs;
|
|
405
441
|
const chunk = {
|
|
406
442
|
requestId,
|
|
407
443
|
chunkIndex,
|
|
408
|
-
startTimeMs:
|
|
409
|
-
endTimeMs:
|
|
444
|
+
startTimeMs: startMs,
|
|
445
|
+
endTimeMs: endMs,
|
|
410
446
|
durationMs: Math.round(((endSample - startSample) / sampleRate) * 1000),
|
|
411
447
|
startSample,
|
|
412
448
|
sampleCount: slice.length,
|
|
@@ -416,11 +452,22 @@ async function streamAudioDataWeb(options, callbacks) {
|
|
|
416
452
|
isFinal: end >= all.length,
|
|
417
453
|
};
|
|
418
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;
|
|
419
464
|
callbacks.onProgress?.({
|
|
420
465
|
requestId,
|
|
421
|
-
processedMs:
|
|
466
|
+
processedMs: elapsedMs,
|
|
422
467
|
durationMs,
|
|
423
|
-
progress: durationMs > 0
|
|
468
|
+
progress: durationMs > 0
|
|
469
|
+
? Math.min(1, Math.max(0, elapsedMs / durationMs))
|
|
470
|
+
: 1,
|
|
424
471
|
emittedChunks: chunkIndex + 1,
|
|
425
472
|
});
|
|
426
473
|
chunkIndex += 1;
|
|
@@ -440,6 +487,26 @@ async function streamAudioDataWeb(options, callbacks) {
|
|
|
440
487
|
throw (0, AudioStreamError_1.mapStreamError)(err, options.fileUri, 'web');
|
|
441
488
|
}
|
|
442
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
|
+
}
|
|
443
510
|
function sanitizeFloat32(input, clamp) {
|
|
444
511
|
if (!clamp) {
|
|
445
512
|
// still need NaN/Inf sanitation
|