@siteed/audio-studio 3.2.0-beta.1 → 3.2.1-beta.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 (93) hide show
  1. package/CHANGELOG.md +356 -5
  2. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +12 -12
  3. package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +1 -1
  4. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
  5. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +43 -10
  6. package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +1 -1
  7. package/build/cjs/AudioRecorder.provider.js +3 -37
  8. package/build/cjs/AudioRecorder.provider.js.map +1 -1
  9. package/build/cjs/errors/AudioStreamError.js +9 -0
  10. package/build/cjs/errors/AudioStreamError.js.map +1 -1
  11. package/build/cjs/errors/AudioStreamError.test.js +22 -1
  12. package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
  13. package/build/cjs/streamAudioData.js +99 -32
  14. package/build/cjs/streamAudioData.js.map +1 -1
  15. package/build/cjs/utils/audioProcessing.js +14 -10
  16. package/build/cjs/utils/audioProcessing.js.map +1 -1
  17. package/build/esm/AudioRecorder.provider.js +3 -4
  18. package/build/esm/AudioRecorder.provider.js.map +1 -1
  19. package/build/esm/errors/AudioStreamError.js +9 -0
  20. package/build/esm/errors/AudioStreamError.js.map +1 -1
  21. package/build/esm/errors/AudioStreamError.test.js +22 -1
  22. package/build/esm/errors/AudioStreamError.test.js.map +1 -1
  23. package/build/esm/streamAudioData.js +99 -32
  24. package/build/esm/streamAudioData.js.map +1 -1
  25. package/build/esm/utils/audioProcessing.js +14 -10
  26. package/build/esm/utils/audioProcessing.js.map +1 -1
  27. package/build/types/errors/AudioStreamError.d.ts.map +1 -1
  28. package/build/types/streamAudioData.d.ts +5 -0
  29. package/build/types/streamAudioData.d.ts.map +1 -1
  30. package/build/types/utils/audioProcessing.d.ts +2 -2
  31. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  32. package/ios/AudioStreamDecoder.swift +191 -100
  33. package/ios/AudioStudio.podspec +1 -1
  34. package/ios/AudioStudioModule.swift +48 -9
  35. package/package.json +32 -15
  36. package/plugin/tsconfig.json +8 -2
  37. package/src/errors/AudioStreamError.test.ts +29 -2
  38. package/src/errors/AudioStreamError.ts +14 -0
  39. package/src/streamAudioData.ts +146 -42
  40. package/src/utils/audioProcessing.ts +25 -14
  41. package/android/src/androidTest/assets/chorus.wav +0 -0
  42. package/android/src/androidTest/assets/jfk.wav +0 -0
  43. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  44. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  45. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  46. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  47. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  48. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  49. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  50. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  51. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  52. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  53. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  54. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  55. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  56. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  57. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  58. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  59. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  60. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  61. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  62. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  63. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  64. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  65. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  66. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  67. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  68. package/android/src/test/resources/chorus.wav +0 -0
  69. package/android/src/test/resources/generate_test_audio.py +0 -94
  70. package/android/src/test/resources/jfk.wav +0 -0
  71. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  72. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  73. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  74. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  75. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
  76. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  77. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  78. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  79. package/ios/AudioStudioTests/Info.plist +0 -22
  80. package/ios/AudioStudioTests/README.md +0 -39
  81. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  82. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  83. package/ios/tests/README.md +0 -41
  84. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  85. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  86. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  87. package/ios/tests/integration/output_control_test.swift +0 -322
  88. package/ios/tests/integration/run_integration_tests.sh +0 -37
  89. package/ios/tests/opus_support_test_macos.swift +0 -154
  90. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  91. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  92. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  93. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -252,7 +252,7 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
252
252
  LogUtils.d(CLASS_NAME, "⏺️ resumeRecording completed successfully")
253
253
  promise.resolve(value)
254
254
  }
255
- override fun reject(code: String, message: String?, cause: Throwable?) {
255
+ override fun reject(code: String?, message: String?, cause: Throwable?) {
256
256
  LogUtils.e(CLASS_NAME, "⏺️ resumeRecording failed: $code - $message", cause)
257
257
  promise.reject(code, message, cause)
258
258
  }
@@ -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 = ((options["chunkDurationMs"] as? Number)?.toInt() ?: 1000)
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
- (payload.getString("requestId"))?.let { releaseStreamDecoder(it) }
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
- payload.getString("requestId")?.let { releaseStreamDecoder(it) }
1156
+ releaseStreamDecoder(requestId)
1124
1157
  }
1125
1158
  }
1126
1159
  }
@@ -1261,7 +1294,7 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
1261
1294
  "isPaused" to true
1262
1295
  ))
1263
1296
  }
1264
- override fun reject(code: String, message: String?, cause: Throwable?) {
1297
+ override fun reject(code: String?, message: String?, cause: Throwable?) {
1265
1298
  LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1266
1299
  }
1267
1300
  })
@@ -1281,7 +1314,7 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
1281
1314
  "isPaused" to true
1282
1315
  ))
1283
1316
  }
1284
- override fun reject(code: String, message: String?, cause: Throwable?) {
1317
+ override fun reject(code: String?, message: String?, cause: Throwable?) {
1285
1318
  LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1286
1319
  }
1287
1320
  })
@@ -1303,7 +1336,7 @@ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
1303
1336
  "isPaused" to true
1304
1337
  ))
1305
1338
  }
1306
- override fun reject(code: String, message: String?, cause: Throwable?) {
1339
+ override fun reject(code: String?, message: String?, cause: Throwable?) {
1307
1340
  LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1308
1341
  }
1309
1342
  })
@@ -41,7 +41,7 @@ class RecordingActionReceiver : BroadcastReceiver() {
41
41
  isProcessingAction.set(false)
42
42
  }
43
43
 
44
- override fun reject(code: String, message: String?, cause: Throwable?) {
44
+ override fun reject(code: String?, message: String?, cause: Throwable?) {
45
45
  Log.e("RecordingActionReceiver", "$action failed: $message", cause)
46
46
  isProcessingAction.set(false)
47
47
  }
@@ -1,41 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.useSharedAudioRecorder = exports.AudioRecorderProvider = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
37
5
  // packages/audio-studio/src/AudioRecorder.provider.tsx
38
- const react_1 = __importStar(require("react"));
6
+ const react_1 = require("react");
39
7
  const useAudioRecorder_1 = require("./useAudioRecorder");
40
8
  const initContext = {
41
9
  isRecording: false,
@@ -62,9 +30,7 @@ const initContext = {
62
30
  const AudioRecorderContext = (0, react_1.createContext)(initContext);
63
31
  const AudioRecorderProvider = ({ children, config = {}, }) => {
64
32
  const audioRecorder = (0, useAudioRecorder_1.useAudioRecorder)(config);
65
- return (<AudioRecorderContext.Provider value={audioRecorder}>
66
- {children}
67
- </AudioRecorderContext.Provider>);
33
+ return ((0, jsx_runtime_1.jsx)(AudioRecorderContext.Provider, { value: audioRecorder, children: children }));
68
34
  };
69
35
  exports.AudioRecorderProvider = AudioRecorderProvider;
70
36
  const useSharedAudioRecorder = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"AudioRecorder.provider.js","sourceRoot":"","sources":["../../src/AudioRecorder.provider.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uDAAuD;AACvD,+CAAwD;AAGxD,yDAA4E;AAE5E,MAAM,WAAW,GAA0B;IACvC,WAAW,EAAE,KAAK;IAClB,QAAQ,EAAE,KAAK;IACf,UAAU,EAAE,CAAC;IACb,IAAI,EAAE,CAAC;IACP,WAAW,EAAE,SAAS;IACtB,cAAc,EAAE,KAAK,IAAI,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,aAAa,EAAE,KAAK,IAAI,EAAE;QACtB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,cAAc,EAAE,KAAK,IAAI,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,eAAe,EAAE,KAAK,IAAI,EAAE;QACxB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,gBAAgB,EAAE,KAAK,IAAI,EAAE;QACzB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;CACJ,CAAA;AAED,MAAM,oBAAoB,GAAG,IAAA,qBAAa,EAAwB,WAAW,CAAC,CAAA;AAOvE,MAAM,qBAAqB,GAAyC,CAAC,EACxE,QAAQ,EACR,MAAM,GAAG,EAAE,GACd,EAAE,EAAE;IACD,MAAM,aAAa,GAAG,IAAA,mCAAgB,EAAC,MAAM,CAAC,CAAA;IAC9C,OAAO,CACH,CAAC,oBAAoB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAChD;YAAA,CAAC,QAAQ,CACb;QAAA,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CACnC,CAAA;AACL,CAAC,CAAA;AAVY,QAAA,qBAAqB,yBAUjC;AAEM,MAAM,sBAAsB,GAAG,GAAG,EAAE;IACvC,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,oBAAoB,CAAC,CAAA;IAChD,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACX,qEAAqE,CACxE,CAAA;IACL,CAAC;IACD,OAAO,OAAO,CAAA;AAClB,CAAC,CAAA;AARY,QAAA,sBAAsB,0BAQlC","sourcesContent":["// packages/audio-studio/src/AudioRecorder.provider.tsx\nimport React, { createContext, useContext } from 'react'\n\nimport { UseAudioRecorderState } from './AudioStudio.types'\nimport { UseAudioRecorderProps, useAudioRecorder } from './useAudioRecorder'\n\nconst initContext: UseAudioRecorderState = {\n isRecording: false,\n isPaused: false,\n durationMs: 0,\n size: 0,\n compression: undefined,\n startRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n stopRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n pauseRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n resumeRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n prepareRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n}\n\nconst AudioRecorderContext = createContext<UseAudioRecorderState>(initContext)\n\ninterface AudioRecorderProviderProps {\n children: React.ReactNode\n config?: UseAudioRecorderProps\n}\n\nexport const AudioRecorderProvider: React.FC<AudioRecorderProviderProps> = ({\n children,\n config = {},\n}) => {\n const audioRecorder = useAudioRecorder(config)\n return (\n <AudioRecorderContext.Provider value={audioRecorder}>\n {children}\n </AudioRecorderContext.Provider>\n )\n}\n\nexport const useSharedAudioRecorder = () => {\n const context = useContext(AudioRecorderContext)\n if (!context) {\n throw new Error(\n 'useSharedAudioRecorder must be used within an AudioRecorderProvider'\n )\n }\n return context\n}\n"]}
1
+ {"version":3,"file":"AudioRecorder.provider.js","sourceRoot":"","sources":["../../src/AudioRecorder.provider.tsx"],"names":[],"mappings":";;;;AAAA,uDAAuD;AACvD,iCAAwD;AAGxD,yDAA4E;AAE5E,MAAM,WAAW,GAA0B;IACvC,WAAW,EAAE,KAAK;IAClB,QAAQ,EAAE,KAAK;IACf,UAAU,EAAE,CAAC;IACb,IAAI,EAAE,CAAC;IACP,WAAW,EAAE,SAAS;IACtB,cAAc,EAAE,KAAK,IAAI,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,aAAa,EAAE,KAAK,IAAI,EAAE;QACtB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,cAAc,EAAE,KAAK,IAAI,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,eAAe,EAAE,KAAK,IAAI,EAAE;QACxB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;IACD,gBAAgB,EAAE,KAAK,IAAI,EAAE;QACzB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACtD,CAAC;CACJ,CAAA;AAED,MAAM,oBAAoB,GAAG,IAAA,qBAAa,EAAwB,WAAW,CAAC,CAAA;AAOvE,MAAM,qBAAqB,GAAyC,CAAC,EACxE,QAAQ,EACR,MAAM,GAAG,EAAE,GACd,EAAE,EAAE;IACD,MAAM,aAAa,GAAG,IAAA,mCAAgB,EAAC,MAAM,CAAC,CAAA;IAC9C,OAAO,CACH,uBAAC,oBAAoB,CAAC,QAAQ,IAAC,KAAK,EAAE,aAAa,YAC9C,QAAQ,GACmB,CACnC,CAAA;AACL,CAAC,CAAA;AAVY,QAAA,qBAAqB,yBAUjC;AAEM,MAAM,sBAAsB,GAAG,GAAG,EAAE;IACvC,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,oBAAoB,CAAC,CAAA;IAChD,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACX,qEAAqE,CACxE,CAAA;IACL,CAAC;IACD,OAAO,OAAO,CAAA;AAClB,CAAC,CAAA;AARY,QAAA,sBAAsB,0BAQlC","sourcesContent":["// packages/audio-studio/src/AudioRecorder.provider.tsx\nimport React, { createContext, useContext } from 'react'\n\nimport { UseAudioRecorderState } from './AudioStudio.types'\nimport { UseAudioRecorderProps, useAudioRecorder } from './useAudioRecorder'\n\nconst initContext: UseAudioRecorderState = {\n isRecording: false,\n isPaused: false,\n durationMs: 0,\n size: 0,\n compression: undefined,\n startRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n stopRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n pauseRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n resumeRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n prepareRecording: async () => {\n throw new Error('AudioRecorderProvider not found')\n },\n}\n\nconst AudioRecorderContext = createContext<UseAudioRecorderState>(initContext)\n\ninterface AudioRecorderProviderProps {\n children: React.ReactNode\n config?: UseAudioRecorderProps\n}\n\nexport const AudioRecorderProvider: React.FC<AudioRecorderProviderProps> = ({\n children,\n config = {},\n}) => {\n const audioRecorder = useAudioRecorder(config)\n return (\n <AudioRecorderContext.Provider value={audioRecorder}>\n {children}\n </AudioRecorderContext.Provider>\n )\n}\n\nexport const useSharedAudioRecorder = () => {\n const context = useContext(AudioRecorderContext)\n if (!context) {\n throw new Error(\n 'useSharedAudioRecorder must be used within an AudioRecorderProvider'\n )\n }\n return context\n}\n"]}
@@ -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)({ code: 'FILE_NOT_FOUND', message: 'gone' });
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":"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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({ code: 'FILE_NOT_FOUND', message: 'gone' })\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(new Error('No suitable codec for audio/opus'))\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('falls back to UNKNOWN', () => {\n const mapped = mapStreamError({})\n expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')\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"]}
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;YAC1B,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,MAAM;SAClB,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAA;QAC3D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,IAAA,iCAAc,EACzB,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAChD,CAAA;QACD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QACzC,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC;YAC1B,IAAI,EAAE,4BAA4B;YAClC,OAAO,EAAE,gBAAgB;SAC5B,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAA;QACtD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC;YAC1B,IAAI,EAAE,uCAAuC;YAC7C,OAAO,EAAE,eAAe;SAC3B,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAA;QACjE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACxD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACrE,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC;YAC1B,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,iBAAiB;SAC7B,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAC7B,oFAAoF,CACvF,CAAA;QACD,IAAI,CAAC,WAAW,EAAE,CAAA;IACtB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,IAAA,iCAAc,EAAC;YAC1B,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,oCAAoC;SAChD,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnD,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC3C,MAAM,GAAG,GAAG,IAAI,mCAAgB,CAAC;YAC7B,IAAI,EAAE,gCAAgC;YACtC,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,KAAK;YAClB,OAAO,EAAE,eAAe;YACxB,QAAQ,EAAE,KAAK;SAClB,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC;YACzB,IAAI,EAAE,gCAAgC;YACtC,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,KAAK;YAClB,OAAO,EAAE,eAAe;YACxB,QAAQ,EAAE,KAAK;YACf,UAAU,EAAE,SAAS;YACrB,aAAa,EAAE,SAAS;SAC3B,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AACN,CAAC,CAAC,CAAA","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 aligned = bytes.byteOffset % 4 === 0
52
- ? new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4)
53
- : new Float32Array(bytes.buffer.slice(bytes.byteOffset));
54
- return new Float32Array(aligned);
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
- throw new AudioStreamError_1.AudioStreamError({
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
- throw new AudioStreamError_1.AudioStreamError({
95
- code: 'ERR_AUDIO_STREAM_INVALID_RANGE',
96
- message: 'startTimeMs must be < endTimeMs',
97
- recoverable: false,
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
- throw new AudioStreamError_1.AudioStreamError({
103
- code: 'ERR_AUDIO_STREAM_INVALID_RANGE',
104
- message: 'chunkDurationMs must be in [10, 60000]',
105
- recoverable: false,
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 ?? 16000,
372
- targetChannels: options.channels ?? 1,
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(1, Math.floor((chunkDurationMs / 1000) * sampleRate) * channels);
406
+ let samplesPerChunk = Math.max(channels, Math.floor((chunkDurationMs / 1000) * sampleRate) * channels);
382
407
  if (options.maxChunkBytes) {
383
- const maxSamples = Math.floor(options.maxChunkBytes / 4);
384
- samplesPerChunk = Math.min(samplesPerChunk, maxSamples);
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.channelData, options.normalizeAudio ?? true);
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: Math.round((startSample / sampleRate) * 1000),
409
- endTimeMs: Math.round((endSample / sampleRate) * 1000),
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: chunk.endTimeMs,
466
+ processedMs: elapsedMs,
422
467
  durationMs,
423
- progress: durationMs > 0 ? chunk.endTimeMs / durationMs : 1,
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