@siteed/audio-studio 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/CHANGELOG.md +535 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/android/build.gradle +143 -0
  5. package/android/src/androidTest/assets/chorus.wav +0 -0
  6. package/android/src/androidTest/assets/jfk.wav +0 -0
  7. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  8. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  9. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +197 -0
  10. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +541 -0
  11. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +234 -0
  12. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
  13. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +324 -0
  14. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +253 -0
  15. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +218 -0
  16. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +120 -0
  17. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +345 -0
  18. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +340 -0
  19. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +252 -0
  20. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +95 -0
  21. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +43 -0
  22. package/android/src/main/AndroidManifest.xml +30 -0
  23. package/android/src/main/CMakeLists.txt +29 -0
  24. package/android/src/main/java/net/siteed/audiostudio/AudioAnalysisData.kt +188 -0
  25. package/android/src/main/java/net/siteed/audiostudio/AudioDataEncoder.kt +9 -0
  26. package/android/src/main/java/net/siteed/audiostudio/AudioDeviceManager.kt +1741 -0
  27. package/android/src/main/java/net/siteed/audiostudio/AudioFeaturesNative.kt +26 -0
  28. package/android/src/main/java/net/siteed/audiostudio/AudioFileHandler.kt +136 -0
  29. package/android/src/main/java/net/siteed/audiostudio/AudioFormatUtils.kt +354 -0
  30. package/android/src/main/java/net/siteed/audiostudio/AudioNotificationsManager.kt +439 -0
  31. package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +2237 -0
  32. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +2163 -0
  33. package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +167 -0
  34. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1112 -0
  35. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +1099 -0
  36. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +37 -0
  37. package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +7 -0
  38. package/android/src/main/java/net/siteed/audiostudio/FFT.kt +100 -0
  39. package/android/src/main/java/net/siteed/audiostudio/Features.kt +98 -0
  40. package/android/src/main/java/net/siteed/audiostudio/LogUtils.kt +93 -0
  41. package/android/src/main/java/net/siteed/audiostudio/MelSpectrogramNative.kt +36 -0
  42. package/android/src/main/java/net/siteed/audiostudio/NotificationConfig.kt +72 -0
  43. package/android/src/main/java/net/siteed/audiostudio/PermissionUtils.kt +68 -0
  44. package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +59 -0
  45. package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +259 -0
  46. package/android/src/main/java/net/siteed/audiostudio/WaveformConfig.kt +19 -0
  47. package/android/src/main/java/net/siteed/audiostudio/WaveformRenderer.kt +159 -0
  48. package/android/src/main/jni/AudioFeaturesJNI.cpp +152 -0
  49. package/android/src/main/jni/MelSpectrogramJNI.cpp +165 -0
  50. package/android/src/main/res/drawable/ic_default_action_icon.xml +16 -0
  51. package/android/src/main/res/drawable/ic_microphone.xml +13 -0
  52. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  53. package/android/src/main/res/drawable/ic_play.xml +10 -0
  54. package/android/src/main/res/drawable/ic_stop.xml +10 -0
  55. package/android/src/main/res/layout/notification_recording.xml +37 -0
  56. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +279 -0
  57. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +249 -0
  58. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +151 -0
  59. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +273 -0
  60. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +140 -0
  61. package/android/src/test/resources/chorus.wav +0 -0
  62. package/android/src/test/resources/generate_test_audio.py +94 -0
  63. package/android/src/test/resources/jfk.wav +0 -0
  64. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  65. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  66. package/app.plugin.js +3 -0
  67. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js +4 -0
  68. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  69. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js +164 -0
  70. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
  71. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +213 -0
  72. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  73. package/build/cjs/AudioAnalysis/extractAudioData.js +21 -0
  74. package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -0
  75. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +90 -0
  76. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
  77. package/build/cjs/AudioAnalysis/extractPreview.js +28 -0
  78. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -0
  79. package/build/cjs/AudioAnalysis/extractWaveform.js +18 -0
  80. package/build/cjs/AudioAnalysis/extractWaveform.js.map +1 -0
  81. package/build/cjs/AudioAnalysis/melSpectrogramWasm.js +149 -0
  82. package/build/cjs/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
  83. package/build/cjs/AudioDeviceManager.js +688 -0
  84. package/build/cjs/AudioDeviceManager.js.map +1 -0
  85. package/build/cjs/AudioRecorder.provider.js +78 -0
  86. package/build/cjs/AudioRecorder.provider.js.map +1 -0
  87. package/build/cjs/AudioStudio.native.js +8 -0
  88. package/build/cjs/AudioStudio.native.js.map +1 -0
  89. package/build/cjs/AudioStudio.types.js +11 -0
  90. package/build/cjs/AudioStudio.types.js.map +1 -0
  91. package/build/cjs/AudioStudio.web.js +708 -0
  92. package/build/cjs/AudioStudio.web.js.map +1 -0
  93. package/build/cjs/AudioStudioModule.js +718 -0
  94. package/build/cjs/AudioStudioModule.js.map +1 -0
  95. package/build/cjs/WebRecorder.web.js +865 -0
  96. package/build/cjs/WebRecorder.web.js.map +1 -0
  97. package/build/cjs/constants/platformLimitations.js +99 -0
  98. package/build/cjs/constants/platformLimitations.js.map +1 -0
  99. package/build/cjs/constants.js +20 -0
  100. package/build/cjs/constants.js.map +1 -0
  101. package/build/cjs/events.js +29 -0
  102. package/build/cjs/events.js.map +1 -0
  103. package/build/cjs/hooks/useAudioDevices.js +179 -0
  104. package/build/cjs/hooks/useAudioDevices.js.map +1 -0
  105. package/build/cjs/index.js +64 -0
  106. package/build/cjs/index.js.map +1 -0
  107. package/build/cjs/trimAudio.js +76 -0
  108. package/build/cjs/trimAudio.js.map +1 -0
  109. package/build/cjs/useAudioRecorder.js +535 -0
  110. package/build/cjs/useAudioRecorder.js.map +1 -0
  111. package/build/cjs/utils/BlobFix.js +502 -0
  112. package/build/cjs/utils/BlobFix.js.map +1 -0
  113. package/build/cjs/utils/audioProcessing.js +136 -0
  114. package/build/cjs/utils/audioProcessing.js.map +1 -0
  115. package/build/cjs/utils/cleanNativeOptions.js +22 -0
  116. package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
  117. package/build/cjs/utils/concatenateBuffers.js +25 -0
  118. package/build/cjs/utils/concatenateBuffers.js.map +1 -0
  119. package/build/cjs/utils/convertPCMToFloat32.js +124 -0
  120. package/build/cjs/utils/convertPCMToFloat32.js.map +1 -0
  121. package/build/cjs/utils/crc32.js +52 -0
  122. package/build/cjs/utils/crc32.js.map +1 -0
  123. package/build/cjs/utils/encodingToBitDepth.js +17 -0
  124. package/build/cjs/utils/encodingToBitDepth.js.map +1 -0
  125. package/build/cjs/utils/getWavFileInfo.js +96 -0
  126. package/build/cjs/utils/getWavFileInfo.js.map +1 -0
  127. package/build/cjs/utils/writeWavHeader.js +88 -0
  128. package/build/cjs/utils/writeWavHeader.js.map +1 -0
  129. package/build/cjs/workers/InlineFeaturesExtractor.web.js +294 -0
  130. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -0
  131. package/build/cjs/workers/inlineAudioWebWorker.web.js +190 -0
  132. package/build/cjs/workers/inlineAudioWebWorker.web.js.map +1 -0
  133. package/build/cjs/workers/wasmGlueString.web.js +27 -0
  134. package/build/cjs/workers/wasmGlueString.web.js.map +1 -0
  135. package/build/esm/AudioAnalysis/AudioAnalysis.types.js +3 -0
  136. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  137. package/build/esm/AudioAnalysis/audioFeaturesWasm.js +126 -0
  138. package/build/esm/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
  139. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +205 -0
  140. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  141. package/build/esm/AudioAnalysis/extractAudioData.js +14 -0
  142. package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -0
  143. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +86 -0
  144. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
  145. package/build/esm/AudioAnalysis/extractPreview.js +25 -0
  146. package/build/esm/AudioAnalysis/extractPreview.js.map +1 -0
  147. package/build/esm/AudioAnalysis/extractWaveform.js +11 -0
  148. package/build/esm/AudioAnalysis/extractWaveform.js.map +1 -0
  149. package/build/esm/AudioAnalysis/melSpectrogramWasm.js +111 -0
  150. package/build/esm/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
  151. package/build/esm/AudioDeviceManager.js +681 -0
  152. package/build/esm/AudioDeviceManager.js.map +1 -0
  153. package/build/esm/AudioRecorder.provider.js +40 -0
  154. package/build/esm/AudioRecorder.provider.js.map +1 -0
  155. package/build/esm/AudioStudio.native.js +6 -0
  156. package/build/esm/AudioStudio.native.js.map +1 -0
  157. package/build/esm/AudioStudio.types.js +8 -0
  158. package/build/esm/AudioStudio.types.js.map +1 -0
  159. package/build/esm/AudioStudio.web.js +704 -0
  160. package/build/esm/AudioStudio.web.js.map +1 -0
  161. package/build/esm/AudioStudioModule.js +713 -0
  162. package/build/esm/AudioStudioModule.js.map +1 -0
  163. package/build/esm/WebRecorder.web.js +861 -0
  164. package/build/esm/WebRecorder.web.js.map +1 -0
  165. package/build/esm/constants/platformLimitations.js +90 -0
  166. package/build/esm/constants/platformLimitations.js.map +1 -0
  167. package/build/esm/constants.js +17 -0
  168. package/build/esm/constants.js.map +1 -0
  169. package/build/esm/events.js +21 -0
  170. package/build/esm/events.js.map +1 -0
  171. package/build/esm/hooks/useAudioDevices.js +176 -0
  172. package/build/esm/hooks/useAudioDevices.js.map +1 -0
  173. package/build/esm/index.js +23 -0
  174. package/build/esm/index.js.map +1 -0
  175. package/build/esm/trimAudio.js +69 -0
  176. package/build/esm/trimAudio.js.map +1 -0
  177. package/build/esm/useAudioRecorder.js +529 -0
  178. package/build/esm/useAudioRecorder.js.map +1 -0
  179. package/build/esm/utils/BlobFix.js +498 -0
  180. package/build/esm/utils/BlobFix.js.map +1 -0
  181. package/build/esm/utils/audioProcessing.js +133 -0
  182. package/build/esm/utils/audioProcessing.js.map +1 -0
  183. package/build/esm/utils/cleanNativeOptions.js +19 -0
  184. package/build/esm/utils/cleanNativeOptions.js.map +1 -0
  185. package/build/esm/utils/concatenateBuffers.js +21 -0
  186. package/build/esm/utils/concatenateBuffers.js.map +1 -0
  187. package/build/esm/utils/convertPCMToFloat32.js +120 -0
  188. package/build/esm/utils/convertPCMToFloat32.js.map +1 -0
  189. package/build/esm/utils/crc32.js +50 -0
  190. package/build/esm/utils/crc32.js.map +1 -0
  191. package/build/esm/utils/encodingToBitDepth.js +13 -0
  192. package/build/esm/utils/encodingToBitDepth.js.map +1 -0
  193. package/build/esm/utils/getWavFileInfo.js +92 -0
  194. package/build/esm/utils/getWavFileInfo.js.map +1 -0
  195. package/build/esm/utils/writeWavHeader.js +84 -0
  196. package/build/esm/utils/writeWavHeader.js.map +1 -0
  197. package/build/esm/workers/InlineFeaturesExtractor.web.js +291 -0
  198. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -0
  199. package/build/esm/workers/inlineAudioWebWorker.web.js +187 -0
  200. package/build/esm/workers/inlineAudioWebWorker.web.js.map +1 -0
  201. package/build/esm/workers/wasmGlueString.web.js +24 -0
  202. package/build/esm/workers/wasmGlueString.web.js.map +1 -0
  203. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +198 -0
  204. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  205. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts +24 -0
  206. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts.map +1 -0
  207. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts +74 -0
  208. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  209. package/build/types/AudioAnalysis/extractAudioData.d.ts +3 -0
  210. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -0
  211. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts +20 -0
  212. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -0
  213. package/build/types/AudioAnalysis/extractPreview.d.ts +11 -0
  214. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -0
  215. package/build/types/AudioAnalysis/extractWaveform.d.ts +8 -0
  216. package/build/types/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  217. package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts +16 -0
  218. package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts.map +1 -0
  219. package/build/types/AudioDeviceManager.d.ts +187 -0
  220. package/build/types/AudioDeviceManager.d.ts.map +1 -0
  221. package/build/types/AudioRecorder.provider.d.ts +11 -0
  222. package/build/types/AudioRecorder.provider.d.ts.map +1 -0
  223. package/build/types/AudioStudio.native.d.ts +3 -0
  224. package/build/types/AudioStudio.native.d.ts.map +1 -0
  225. package/build/types/AudioStudio.types.d.ts +760 -0
  226. package/build/types/AudioStudio.types.d.ts.map +1 -0
  227. package/build/types/AudioStudio.web.d.ts +96 -0
  228. package/build/types/AudioStudio.web.d.ts.map +1 -0
  229. package/build/types/AudioStudioModule.d.ts +3 -0
  230. package/build/types/AudioStudioModule.d.ts.map +1 -0
  231. package/build/types/WebRecorder.web.d.ts +208 -0
  232. package/build/types/WebRecorder.web.d.ts.map +1 -0
  233. package/build/types/constants/platformLimitations.d.ts +40 -0
  234. package/build/types/constants/platformLimitations.d.ts.map +1 -0
  235. package/build/types/constants.d.ts +14 -0
  236. package/build/types/constants.d.ts.map +1 -0
  237. package/build/types/events.d.ts +29 -0
  238. package/build/types/events.d.ts.map +1 -0
  239. package/build/types/hooks/useAudioDevices.d.ts +15 -0
  240. package/build/types/hooks/useAudioDevices.d.ts.map +1 -0
  241. package/build/types/index.d.ts +21 -0
  242. package/build/types/index.d.ts.map +1 -0
  243. package/build/types/trimAudio.d.ts +25 -0
  244. package/build/types/trimAudio.d.ts.map +1 -0
  245. package/build/types/useAudioRecorder.d.ts +22 -0
  246. package/build/types/useAudioRecorder.d.ts.map +1 -0
  247. package/build/types/utils/BlobFix.d.ts +9 -0
  248. package/build/types/utils/BlobFix.d.ts.map +1 -0
  249. package/build/types/utils/audioProcessing.d.ts +24 -0
  250. package/build/types/utils/audioProcessing.d.ts.map +1 -0
  251. package/build/types/utils/cleanNativeOptions.d.ts +15 -0
  252. package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
  253. package/build/types/utils/concatenateBuffers.d.ts +8 -0
  254. package/build/types/utils/concatenateBuffers.d.ts.map +1 -0
  255. package/build/types/utils/convertPCMToFloat32.d.ts +13 -0
  256. package/build/types/utils/convertPCMToFloat32.d.ts.map +1 -0
  257. package/build/types/utils/crc32.d.ts +7 -0
  258. package/build/types/utils/crc32.d.ts.map +1 -0
  259. package/build/types/utils/encodingToBitDepth.d.ts +5 -0
  260. package/build/types/utils/encodingToBitDepth.d.ts.map +1 -0
  261. package/build/types/utils/getWavFileInfo.d.ts +26 -0
  262. package/build/types/utils/getWavFileInfo.d.ts.map +1 -0
  263. package/build/types/utils/writeWavHeader.d.ts +34 -0
  264. package/build/types/utils/writeWavHeader.d.ts.map +1 -0
  265. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  266. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  267. package/build/types/workers/inlineAudioWebWorker.web.d.ts +2 -0
  268. package/build/types/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  269. package/build/types/workers/wasmGlueString.web.d.ts +2 -0
  270. package/build/types/workers/wasmGlueString.web.d.ts.map +1 -0
  271. package/cpp/AudioFeatures.cpp +274 -0
  272. package/cpp/AudioFeatures.h +85 -0
  273. package/cpp/AudioFeaturesBridge.cpp +146 -0
  274. package/cpp/AudioFeaturesBridge.h +47 -0
  275. package/cpp/MelSpectrogram.cpp +227 -0
  276. package/cpp/MelSpectrogram.h +82 -0
  277. package/cpp/MelSpectrogramBridge.cpp +112 -0
  278. package/cpp/MelSpectrogramBridge.h +33 -0
  279. package/cpp/kiss_fft/COPYING +11 -0
  280. package/cpp/kiss_fft/_kiss_fft_guts.h +167 -0
  281. package/cpp/kiss_fft/kiss_fft.c +424 -0
  282. package/cpp/kiss_fft/kiss_fft.h +160 -0
  283. package/cpp/kiss_fft/kiss_fft_log.h +36 -0
  284. package/cpp/kiss_fft/kiss_fftr.c +155 -0
  285. package/cpp/kiss_fft/kiss_fftr.h +54 -0
  286. package/expo-module.config.json +10 -0
  287. package/ios/AudioAnalysisData.swift +74 -0
  288. package/ios/AudioDeviceManager.swift +670 -0
  289. package/ios/AudioFeaturesWrapper.h +21 -0
  290. package/ios/AudioFeaturesWrapper.mm +63 -0
  291. package/ios/AudioNotificationManager.swift +154 -0
  292. package/ios/AudioProcessingHelpers.swift +797 -0
  293. package/ios/AudioProcessor.swift +1191 -0
  294. package/ios/AudioStreamError.swift +7 -0
  295. package/ios/AudioStreamManager.swift +2369 -0
  296. package/ios/AudioStreamManagerDelegate.swift +16 -0
  297. package/ios/AudioStudio.podspec +39 -0
  298. package/ios/AudioStudioModule.swift +1111 -0
  299. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +338 -0
  300. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +331 -0
  301. package/ios/AudioStudioTests/AudioTestHelpers.swift +130 -0
  302. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +294 -0
  303. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +105 -0
  304. package/ios/AudioStudioTests/Info.plist +22 -0
  305. package/ios/AudioStudioTests/README.md +39 -0
  306. package/ios/AudioStudioTests/SimpleAudioTest.swift +98 -0
  307. package/ios/AudioStudioTests/TestAudioGenerator.swift +75 -0
  308. package/ios/DataPoint.swift +54 -0
  309. package/ios/DecodingConfig.swift +59 -0
  310. package/ios/FFT.swift +62 -0
  311. package/ios/Features.swift +95 -0
  312. package/ios/ISSUE_IOS.md +68 -0
  313. package/ios/Logger.swift +39 -0
  314. package/ios/MelSpectrogramWrapper.h +30 -0
  315. package/ios/MelSpectrogramWrapper.mm +97 -0
  316. package/ios/NotificationExtension.swift +15 -0
  317. package/ios/RecordingResult.swift +22 -0
  318. package/ios/RecordingSettings.swift +311 -0
  319. package/ios/WaveformExtractor.swift +105 -0
  320. package/ios/tests/README.md +41 -0
  321. package/ios/tests/integration/buffer_and_fallback_test.swift +178 -0
  322. package/ios/tests/integration/buffer_duration_test.swift +185 -0
  323. package/ios/tests/integration/compressed_only_output_test.swift +271 -0
  324. package/ios/tests/integration/output_control_test.swift +322 -0
  325. package/ios/tests/integration/run_integration_tests.sh +37 -0
  326. package/ios/tests/opus_support_test_macos.swift +154 -0
  327. package/ios/tests/standalone/audio_processing_test.swift +144 -0
  328. package/ios/tests/standalone/audio_recording_test.swift +277 -0
  329. package/ios/tests/standalone/audio_streaming_test.swift +249 -0
  330. package/ios/tests/standalone/standalone_test.swift +144 -0
  331. package/package.json +146 -0
  332. package/plugin/build/index.cjs +194 -0
  333. package/plugin/build/index.d.cts +22 -0
  334. package/plugin/build/index.js +194 -0
  335. package/plugin/src/index.ts +285 -0
  336. package/plugin/tsconfig.json +10 -0
  337. package/plugin/tsconfig.tsbuildinfo +1 -0
  338. package/prebuilt/wasm/mel-spectrogram.js +18 -0
  339. package/src/AudioAnalysis/AudioAnalysis.types.ts +226 -0
  340. package/src/AudioAnalysis/audio-features-wasm.d.ts +37 -0
  341. package/src/AudioAnalysis/audioFeaturesWasm.ts +200 -0
  342. package/src/AudioAnalysis/extractAudioAnalysis.ts +350 -0
  343. package/src/AudioAnalysis/extractAudioData.ts +17 -0
  344. package/src/AudioAnalysis/extractMelSpectrogram.ts +140 -0
  345. package/src/AudioAnalysis/extractPreview.ts +34 -0
  346. package/src/AudioAnalysis/extractWaveform.ts +22 -0
  347. package/src/AudioAnalysis/mel-spectrogram-wasm.d.ts +48 -0
  348. package/src/AudioAnalysis/melSpectrogramWasm.ts +179 -0
  349. package/src/AudioDeviceManager.ts +800 -0
  350. package/src/AudioRecorder.provider.tsx +57 -0
  351. package/src/AudioStudio.native.ts +6 -0
  352. package/src/AudioStudio.types.ts +899 -0
  353. package/src/AudioStudio.web.ts +911 -0
  354. package/src/AudioStudioModule.ts +984 -0
  355. package/src/WebRecorder.web.ts +1114 -0
  356. package/src/constants/platformLimitations.ts +118 -0
  357. package/src/constants.ts +21 -0
  358. package/src/events.ts +63 -0
  359. package/src/hooks/useAudioDevices.ts +213 -0
  360. package/src/index.ts +67 -0
  361. package/src/trimAudio.ts +94 -0
  362. package/src/types/crc-32.d.ts +9 -0
  363. package/src/useAudioRecorder.tsx +784 -0
  364. package/src/utils/BlobFix.ts +561 -0
  365. package/src/utils/audioProcessing.ts +205 -0
  366. package/src/utils/cleanNativeOptions.ts +18 -0
  367. package/src/utils/concatenateBuffers.ts +24 -0
  368. package/src/utils/convertPCMToFloat32.ts +170 -0
  369. package/src/utils/crc32.ts +59 -0
  370. package/src/utils/encodingToBitDepth.ts +18 -0
  371. package/src/utils/getWavFileInfo.ts +132 -0
  372. package/src/utils/writeWavHeader.ts +115 -0
  373. package/src/workers/InlineFeaturesExtractor.web.tsx +291 -0
  374. package/src/workers/inlineAudioWebWorker.web.tsx +186 -0
  375. package/src/workers/wasmGlueString.web.ts +23 -0
@@ -0,0 +1,784 @@
1
+ // src/useAudioRecorder.ts
2
+ import { EventSubscription, Platform } from 'expo-modules-core'
3
+ import { useCallback, useEffect, useReducer, useRef, useId } from 'react'
4
+
5
+ import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
6
+ import { audioDeviceManager } from './AudioDeviceManager'
7
+ import {
8
+ AudioDataEvent,
9
+ AudioRecording,
10
+ AudioStreamStatus,
11
+ CompressionInfo,
12
+ ConsoleLike,
13
+ RecordingConfig,
14
+ StartRecordingResult,
15
+ } from './AudioStudio.types'
16
+ import AudioStudioModule from './AudioStudioModule'
17
+ import { validateRecordingConfig } from './constants/platformLimitations'
18
+ import {
19
+ addAudioAnalysisListener,
20
+ addAudioEventListener,
21
+ AudioEventPayload,
22
+ addRecordingInterruptionListener,
23
+ } from './events'
24
+ import { cleanNativeOptions } from './utils/cleanNativeOptions'
25
+
26
+ export interface UseAudioRecorderProps {
27
+ logger?: ConsoleLike
28
+ audioWorkletUrl?: string
29
+ featuresExtratorUrl?: string
30
+ }
31
+
32
+ export interface UseAudioRecorderState {
33
+ prepareRecording: (_: RecordingConfig) => Promise<void>
34
+ startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>
35
+ stopRecording: () => Promise<AudioRecording>
36
+ pauseRecording: () => Promise<void>
37
+ resumeRecording: () => Promise<void>
38
+ isRecording: boolean
39
+ isPaused: boolean
40
+ durationMs: number
41
+ size: number
42
+ compression?: CompressionInfo
43
+ analysisData?: AudioAnalysis
44
+ }
45
+
46
+ interface RecorderReducerState {
47
+ isRecording: boolean
48
+ isPaused: boolean
49
+ durationMs: number
50
+ size: number
51
+ compression?: CompressionInfo
52
+ analysisData?: AudioAnalysis
53
+ }
54
+
55
+ type RecorderAction =
56
+ | { type: 'START' | 'STOP' | 'PAUSE' | 'RESUME' }
57
+ | {
58
+ type: 'UPDATE_RECORDING_STATE'
59
+ payload: {
60
+ isRecording: boolean
61
+ isPaused: boolean
62
+ }
63
+ }
64
+ | {
65
+ type: 'UPDATE_STATUS'
66
+ payload: {
67
+ durationMs: number
68
+ size: number
69
+ compression?: CompressionInfo
70
+ }
71
+ }
72
+ | { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
73
+
74
+ const defaultAnalysis: AudioAnalysis = {
75
+ segmentDurationMs: 100,
76
+ bitDepth: 32,
77
+ numberOfChannels: 1,
78
+ durationMs: 0,
79
+ sampleRate: 44100,
80
+ samples: 0,
81
+ dataPoints: [],
82
+ rmsRange: {
83
+ min: Number.POSITIVE_INFINITY,
84
+ max: Number.NEGATIVE_INFINITY,
85
+ },
86
+ amplitudeRange: {
87
+ min: Number.POSITIVE_INFINITY,
88
+ max: Number.NEGATIVE_INFINITY,
89
+ },
90
+ extractionTimeMs: 0,
91
+ }
92
+
93
+ function audioRecorderReducer(
94
+ state: RecorderReducerState,
95
+ action: RecorderAction
96
+ ): RecorderReducerState {
97
+ switch (action.type) {
98
+ case 'START':
99
+ return {
100
+ ...state,
101
+ isRecording: true,
102
+ isPaused: false,
103
+ durationMs: 0,
104
+ size: 0,
105
+ compression: undefined,
106
+ analysisData: defaultAnalysis,
107
+ }
108
+ case 'STOP':
109
+ return {
110
+ ...state,
111
+ isRecording: false,
112
+ isPaused: false,
113
+ durationMs: 0,
114
+ size: 0,
115
+ compression: undefined,
116
+ analysisData: undefined,
117
+ }
118
+ case 'PAUSE':
119
+ return { ...state, isPaused: true, isRecording: false }
120
+ case 'RESUME':
121
+ return { ...state, isPaused: false, isRecording: true }
122
+ case 'UPDATE_RECORDING_STATE':
123
+ return {
124
+ ...state,
125
+ isPaused: action.payload.isPaused,
126
+ isRecording: action.payload.isRecording,
127
+ }
128
+ case 'UPDATE_STATUS': {
129
+ const newState = {
130
+ ...state,
131
+ durationMs: action.payload.durationMs,
132
+ size: action.payload.size,
133
+ compression: action.payload.compression
134
+ ? {
135
+ size: action.payload.compression.size,
136
+ mimeType: action.payload.compression.mimeType,
137
+ bitrate: action.payload.compression.bitrate,
138
+ format: action.payload.compression.format,
139
+ }
140
+ : undefined,
141
+ }
142
+ return newState
143
+ }
144
+ case 'UPDATE_ANALYSIS':
145
+ return {
146
+ ...state,
147
+ analysisData: action.payload,
148
+ }
149
+ default:
150
+ return state
151
+ }
152
+ }
153
+
154
+ interface HandleAudioAnalysisProps {
155
+ analysis: AudioAnalysis
156
+ visualizationDuration: number
157
+ }
158
+
159
+ export function useAudioRecorder({
160
+ logger,
161
+ audioWorkletUrl,
162
+ featuresExtratorUrl,
163
+ }: UseAudioRecorderProps = {}): UseAudioRecorderState {
164
+ // Initialize AudioDeviceManager with logger (once)
165
+ if (logger) {
166
+ audioDeviceManager.setLogger(logger)
167
+ }
168
+ const [state, dispatch] = useReducer(audioRecorderReducer, {
169
+ isRecording: false,
170
+ isPaused: false,
171
+ durationMs: 0,
172
+ size: 0,
173
+ compression: undefined,
174
+ analysisData: undefined,
175
+ })
176
+
177
+ const startResultRef = useRef<StartRecordingResult | null>(null)
178
+
179
+ const analysisListenerRef = useRef<EventSubscription | null>(null)
180
+ // analysisRef is the current analysis data (last 10 seconds by default)
181
+ const analysisRef = useRef<AudioAnalysis>({ ...defaultAnalysis })
182
+ // fullAnalysisRef is the full analysis data (all data points)
183
+ const fullAnalysisRef = useRef<AudioAnalysis>({
184
+ ...defaultAnalysis,
185
+ })
186
+
187
+ // Instantiate the module for web with URLs
188
+ const audioStudio =
189
+ Platform.OS === 'web'
190
+ ? AudioStudioModule({
191
+ audioWorkletUrl,
192
+ featuresExtratorUrl,
193
+ logger,
194
+ })
195
+ : AudioStudioModule
196
+
197
+ const onAudioStreamRef = useRef<
198
+ ((_: AudioDataEvent) => Promise<void>) | null
199
+ >(null)
200
+
201
+ const stateRef = useRef({
202
+ isRecording: false,
203
+ isPaused: false,
204
+ durationMs: 0,
205
+ size: 0,
206
+ compression: undefined as CompressionInfo | undefined,
207
+ })
208
+
209
+ const recordingConfigRef = useRef<RecordingConfig | null>(null)
210
+
211
+ // Generate unique instance ID for debugging
212
+ const instanceId = useId().replace(/:/g, '').slice(0, 5)
213
+
214
+ const handleAudioAnalysis = useCallback(
215
+ async ({
216
+ analysis,
217
+ visualizationDuration,
218
+ }: HandleAudioAnalysisProps) => {
219
+ const savedAnalysisData = analysisRef.current || {
220
+ ...defaultAnalysis,
221
+ }
222
+
223
+ const maxDuration = visualizationDuration
224
+
225
+ logger?.debug(
226
+ `[handleAudioAnalysis] Received audio analysis: maxDuration=${maxDuration} analysis.dataPoints=${analysis.dataPoints.length} analysisData.dataPoints=${savedAnalysisData.dataPoints.length}`
227
+ )
228
+
229
+ // Combine data points
230
+ const combinedDataPoints = [
231
+ ...savedAnalysisData.dataPoints,
232
+ ...analysis.dataPoints,
233
+ ]
234
+
235
+ const fullCombinedDataPoints = [
236
+ ...(fullAnalysisRef.current?.dataPoints ?? []),
237
+ ...analysis.dataPoints,
238
+ ]
239
+
240
+ // Calculate the new duration
241
+ // The number of segments is based on how many segments of segmentDurationMs can fit in visualizationDuration
242
+ const numberOfSegments = Math.ceil(
243
+ visualizationDuration / analysis.segmentDurationMs
244
+ )
245
+ // maxDataPoints should be the number of data points, not milliseconds
246
+ const maxDataPoints = numberOfSegments
247
+
248
+ logger?.debug(
249
+ `[handleAudioAnalysis] Combined data points before trimming: numberOfSegments=${numberOfSegments} visualizationDuration=${visualizationDuration} combinedDataPointsLength=${combinedDataPoints.length} vs maxDataPoints=${maxDataPoints}`
250
+ )
251
+
252
+ // Trim data points to keep within the maximum number of data points
253
+ if (combinedDataPoints.length > maxDataPoints) {
254
+ combinedDataPoints.splice(
255
+ 0,
256
+ combinedDataPoints.length - maxDataPoints
257
+ )
258
+ }
259
+
260
+ // Keep the full data points
261
+ fullAnalysisRef.current = {
262
+ ...fullAnalysisRef.current,
263
+ dataPoints: fullCombinedDataPoints,
264
+ }
265
+ fullAnalysisRef.current.durationMs =
266
+ fullCombinedDataPoints.length * analysis.segmentDurationMs
267
+ savedAnalysisData.dataPoints = combinedDataPoints
268
+ savedAnalysisData.bitDepth =
269
+ analysis.bitDepth || savedAnalysisData.bitDepth
270
+ savedAnalysisData.durationMs =
271
+ combinedDataPoints.length * analysis.segmentDurationMs
272
+
273
+ // Update amplitude range
274
+ const newMin = Math.min(
275
+ savedAnalysisData.amplitudeRange.min,
276
+ analysis.amplitudeRange.min
277
+ )
278
+ const newMax = Math.max(
279
+ savedAnalysisData.amplitudeRange.max,
280
+ analysis.amplitudeRange.max
281
+ )
282
+
283
+ savedAnalysisData.amplitudeRange = {
284
+ min: newMin,
285
+ max: newMax,
286
+ }
287
+ fullAnalysisRef.current.amplitudeRange = {
288
+ min: newMin,
289
+ max: newMax,
290
+ }
291
+
292
+ logger?.debug(
293
+ `[handleAudioAnalysis] Updated analysis data: durationMs=${savedAnalysisData.durationMs}`,
294
+ { dataPoints: savedAnalysisData.dataPoints.length }
295
+ )
296
+
297
+ // Call the onAudioAnalysis callback if it exists in the recording config
298
+ if (recordingConfigRef.current?.onAudioAnalysis) {
299
+ recordingConfigRef.current
300
+ .onAudioAnalysis(analysis)
301
+ .catch((error) => {
302
+ logger?.warn(`Error processing audio analysis:`, error)
303
+ })
304
+ }
305
+
306
+ // Update the ref
307
+ analysisRef.current = savedAnalysisData
308
+
309
+ // Dispatch the updated analysis data to state to trigger re-render
310
+ dispatch({
311
+ type: 'UPDATE_ANALYSIS',
312
+ payload: { ...savedAnalysisData },
313
+ })
314
+ },
315
+ [dispatch]
316
+ )
317
+
318
+ const handleAudioEvent = useCallback(
319
+ async (eventData: AudioEventPayload) => {
320
+ const {
321
+ fileUri,
322
+ deltaSize,
323
+ totalSize,
324
+ lastEmittedSize,
325
+ position,
326
+ streamUuid,
327
+ encoded,
328
+ pcmFloat32,
329
+ mimeType,
330
+ buffer,
331
+ compression,
332
+ } = eventData
333
+ logger?.debug(`[handleAudioEvent] Received audio event:`, {
334
+ fileUri,
335
+ deltaSize,
336
+ totalSize,
337
+ position,
338
+ mimeType,
339
+ lastEmittedSize,
340
+ streamUuid,
341
+ encodedLength: encoded?.length,
342
+ compression,
343
+ })
344
+ if (deltaSize === 0) {
345
+ // Ignore packet with no data
346
+ return
347
+ }
348
+ try {
349
+ // Coming from native ( ios / android ) otherwise buffer is set
350
+ if (Platform.OS !== 'web') {
351
+ const compressionPayload =
352
+ compression && startResultRef.current?.compression
353
+ ? {
354
+ data: compression.data,
355
+ size: compression.totalSize,
356
+ mimeType:
357
+ startResultRef.current.compression
358
+ ?.mimeType,
359
+ bitrate:
360
+ startResultRef.current.compression
361
+ ?.bitrate,
362
+ format: startResultRef.current.compression
363
+ ?.format,
364
+ }
365
+ : undefined
366
+ if (pcmFloat32 != null) {
367
+ // Android new arch delivers Float32Array; iOS delivers number[] — normalize both
368
+ const float32 =
369
+ pcmFloat32 instanceof Float32Array
370
+ ? pcmFloat32
371
+ : new Float32Array(pcmFloat32 as number[])
372
+ onAudioStreamRef.current?.({
373
+ data: float32,
374
+ streamFormat: 'float32',
375
+ position,
376
+ fileUri,
377
+ eventDataSize: deltaSize,
378
+ totalSize,
379
+ compression: compressionPayload,
380
+ })
381
+ } else {
382
+ if (!encoded) {
383
+ logger?.error(`Encoded audio data is missing`)
384
+ throw new Error('Encoded audio data is missing')
385
+ }
386
+ onAudioStreamRef.current?.({
387
+ data: encoded,
388
+ position,
389
+ fileUri,
390
+ eventDataSize: deltaSize,
391
+ totalSize,
392
+ compression: compressionPayload,
393
+ })
394
+ }
395
+ } else if (buffer) {
396
+ // Coming from web
397
+ const webEvent: AudioDataEvent = {
398
+ data: buffer,
399
+ position,
400
+ fileUri,
401
+ eventDataSize: deltaSize,
402
+ totalSize,
403
+ compression:
404
+ compression && startResultRef.current?.compression
405
+ ? {
406
+ data: compression.data,
407
+ size: compression.totalSize,
408
+ mimeType:
409
+ startResultRef.current.compression
410
+ ?.mimeType,
411
+ bitrate:
412
+ startResultRef.current.compression
413
+ ?.bitrate,
414
+ format: startResultRef.current.compression
415
+ ?.format,
416
+ }
417
+ : undefined,
418
+ }
419
+ onAudioStreamRef.current?.(webEvent)
420
+ logger?.debug(
421
+ `[handleAudioEvent] Audio data sent to onAudioStream`,
422
+ webEvent
423
+ )
424
+ }
425
+ } catch (error) {
426
+ logger?.error(`Error processing audio event:`, error)
427
+ }
428
+ },
429
+ []
430
+ )
431
+
432
+ const checkStatus = useCallback(async () => {
433
+ try {
434
+ const status: AudioStreamStatus = audioStudio.status()
435
+ logger?.debug(
436
+ `Status: paused: ${status.isPaused} isRecording: ${status.isRecording} durationMs: ${status.durationMs} size: ${status.size}`,
437
+ status.compression
438
+ )
439
+
440
+ // Only dispatch if values actually changed
441
+ if (
442
+ status.isRecording !== stateRef.current.isRecording ||
443
+ status.isPaused !== stateRef.current.isPaused
444
+ ) {
445
+ stateRef.current.isRecording = status.isRecording
446
+ stateRef.current.isPaused = status.isPaused
447
+ dispatch({
448
+ type: 'UPDATE_RECORDING_STATE',
449
+ payload: {
450
+ isRecording: status.isRecording,
451
+ isPaused: status.isPaused,
452
+ },
453
+ })
454
+ }
455
+
456
+ if (
457
+ status.durationMs !== stateRef.current.durationMs ||
458
+ status.size !== stateRef.current.size
459
+ ) {
460
+ stateRef.current.durationMs = status.durationMs
461
+ stateRef.current.size = status.size
462
+ stateRef.current.compression = status.compression
463
+ dispatch({
464
+ type: 'UPDATE_STATUS',
465
+ payload: {
466
+ durationMs: status.durationMs,
467
+ size: status.size,
468
+ compression: status.compression,
469
+ },
470
+ })
471
+ }
472
+ } catch (error) {
473
+ logger?.error(`Error getting status:`, error)
474
+ }
475
+ }, [audioStudio, logger]) // Only depend on audioStudio and logger
476
+
477
+ // Update ref when state changes
478
+ useEffect(() => {
479
+ stateRef.current = {
480
+ isRecording: state.isRecording,
481
+ isPaused: state.isPaused,
482
+ durationMs: state.durationMs,
483
+ size: state.size,
484
+ compression: state.compression,
485
+ }
486
+ }, [
487
+ state.isRecording,
488
+ state.isPaused,
489
+ state.durationMs,
490
+ state.size,
491
+ state.compression,
492
+ ])
493
+
494
+ const startRecording = useCallback(
495
+ async (recordingOptions: RecordingConfig) => {
496
+ // Validate the encoding configuration
497
+ const validationResult = validateRecordingConfig({
498
+ encoding: recordingOptions.encoding,
499
+ })
500
+
501
+ // Log warnings if any
502
+ if (validationResult.warnings.length > 0) {
503
+ validationResult.warnings.forEach((warning) => {
504
+ logger?.warn(warning)
505
+ })
506
+ }
507
+
508
+ // Update recording options with validated values
509
+ const validatedOptions = {
510
+ ...recordingOptions,
511
+ encoding: validationResult.encoding,
512
+ }
513
+
514
+ recordingConfigRef.current = validatedOptions
515
+ logger?.debug(
516
+ `start recording with validated config`,
517
+ validatedOptions
518
+ )
519
+
520
+ analysisRef.current = { ...defaultAnalysis } // Reset analysis data
521
+ fullAnalysisRef.current = { ...defaultAnalysis }
522
+ const {
523
+ onAudioStream,
524
+ onRecordingInterrupted,
525
+ onAudioAnalysis,
526
+ ...options
527
+ } = validatedOptions
528
+ const { enableProcessing } = options
529
+
530
+ const maxRecentDataDuration = 10000 // TODO compute maxRecentDataDuration based on screen dimensions
531
+ if (typeof onAudioStream === 'function') {
532
+ onAudioStreamRef.current = onAudioStream
533
+ } else {
534
+ logger?.warn(`onAudioStream is not a function`, onAudioStream)
535
+ onAudioStreamRef.current = null
536
+ }
537
+ // Strip undefined values and functions that can't cross the native bridge
538
+ const cleanOptions = cleanNativeOptions(options)
539
+ const startResult: StartRecordingResult =
540
+ await audioStudio.startRecording(cleanOptions)
541
+ dispatch({ type: 'START' })
542
+
543
+ startResultRef.current = startResult
544
+
545
+ if (enableProcessing) {
546
+ logger?.debug(`Enabling audio analysis listener`)
547
+ const listener = addAudioAnalysisListener(
548
+ async (analysisData) => {
549
+ try {
550
+ await handleAudioAnalysis({
551
+ analysis: analysisData,
552
+ visualizationDuration: maxRecentDataDuration,
553
+ })
554
+ } catch (error) {
555
+ logger?.warn(
556
+ `Error processing audio analysis:`,
557
+ error
558
+ )
559
+ }
560
+ }
561
+ )
562
+
563
+ analysisListenerRef.current = listener
564
+ }
565
+
566
+ return startResult
567
+ },
568
+ [handleAudioAnalysis, dispatch]
569
+ )
570
+
571
+ const prepareRecording = useCallback(
572
+ async (recordingOptions: RecordingConfig) => {
573
+ recordingConfigRef.current = recordingOptions
574
+ logger?.debug(`preparing recording`, recordingOptions)
575
+
576
+ analysisRef.current = { ...defaultAnalysis } // Reset analysis data
577
+ fullAnalysisRef.current = { ...defaultAnalysis }
578
+ const {
579
+ onAudioStream,
580
+ onRecordingInterrupted,
581
+ onAudioAnalysis,
582
+ ...options
583
+ } = recordingOptions
584
+
585
+ // Store onAudioStream for later use when recording starts
586
+ if (typeof onAudioStream === 'function') {
587
+ onAudioStreamRef.current = onAudioStream
588
+ } else {
589
+ logger?.warn(`onAudioStream is not a function`, onAudioStream)
590
+ onAudioStreamRef.current = null
591
+ }
592
+
593
+ // Strip undefined values and functions that can't cross the native bridge
594
+ const cleanOptions = cleanNativeOptions(options)
595
+ // Call the native prepareRecording method
596
+ await audioStudio.prepareRecording(cleanOptions)
597
+ logger?.debug(`recording prepared successfully`)
598
+ },
599
+ []
600
+ )
601
+
602
+ const stopRecording = useCallback(async () => {
603
+ logger?.debug(`stoping recording`)
604
+
605
+ const stopResult: AudioRecording = await audioStudio.stopRecording()
606
+ stopResult.analysisData = fullAnalysisRef.current
607
+
608
+ if (analysisListenerRef.current) {
609
+ analysisListenerRef.current.remove()
610
+ analysisListenerRef.current = null
611
+ }
612
+ onAudioStreamRef.current = null
613
+
614
+ // Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
615
+ logger?.debug(`recording stopped`, stopResult)
616
+ dispatch({ type: 'STOP' })
617
+ return stopResult
618
+ }, [dispatch])
619
+
620
+ const pauseRecording = useCallback(async () => {
621
+ logger?.debug(`pause recording`)
622
+ const pauseResult = await audioStudio.pauseRecording()
623
+ dispatch({ type: 'PAUSE' })
624
+ return pauseResult
625
+ }, [dispatch])
626
+
627
+ const resumeRecording = useCallback(async () => {
628
+ logger?.debug(`resume recording`)
629
+ const resumeResult = await audioStudio.resumeRecording()
630
+ dispatch({ type: 'RESUME' })
631
+ return resumeResult
632
+ }, [dispatch])
633
+
634
+ useEffect(() => {
635
+ let intervalId: ReturnType<typeof setInterval> | undefined
636
+
637
+ if (state.isRecording || state.isPaused) {
638
+ // Immediately check status when starting
639
+ checkStatus()
640
+
641
+ // Start interval
642
+ intervalId = setInterval(checkStatus, 1000)
643
+ }
644
+
645
+ return () => {
646
+ if (intervalId) {
647
+ clearInterval(intervalId)
648
+ intervalId = undefined
649
+ }
650
+ }
651
+ }, [checkStatus, state.isRecording, state.isPaused])
652
+
653
+ useEffect(() => {
654
+ logger?.debug(`Registering audio event listener`)
655
+ const subscribeAudio = addAudioEventListener(handleAudioEvent)
656
+
657
+ logger?.debug(
658
+ `Subscribed to audio event listener and analysis listener`,
659
+ {
660
+ subscribeAudio,
661
+ }
662
+ )
663
+
664
+ return () => {
665
+ logger?.debug(`Removing audio event listener`)
666
+ subscribeAudio.remove()
667
+ }
668
+ }, [handleAudioEvent, handleAudioAnalysis])
669
+
670
+ useEffect(() => {
671
+ // Add event subscription for recording interruptions
672
+ logger?.debug(
673
+ `Setting up recording interruption listener [${instanceId}]`
674
+ )
675
+
676
+ const subscription = addRecordingInterruptionListener((event) => {
677
+ logger?.debug(
678
+ `[${instanceId}] Received recording interruption event:`,
679
+ event
680
+ )
681
+
682
+ // Handle device disconnection for UI updates
683
+ if (event.reason === 'deviceDisconnected') {
684
+ logger?.debug(
685
+ `[${instanceId}] Device disconnected - temporarily hiding last device from UI`
686
+ )
687
+
688
+ // Get current device list before the native layer updates
689
+ const currentDevices = audioDeviceManager.getRawDevices()
690
+
691
+ // Wait a moment for native layer to update, then compare
692
+ setTimeout(async () => {
693
+ try {
694
+ // Get updated devices without notifying yet
695
+ const updatedDevices =
696
+ await audioDeviceManager.getAvailableDevices({
697
+ refresh: true,
698
+ })
699
+
700
+ // Find missing devices by comparing lists
701
+ const missingDevices = currentDevices.filter(
702
+ (oldDevice) =>
703
+ !updatedDevices.some(
704
+ (newDevice) => newDevice.id === oldDevice.id
705
+ )
706
+ )
707
+
708
+ if (missingDevices.length > 0) {
709
+ // Mark all missing devices as disconnected (silently)
710
+ missingDevices.forEach((missingDevice) => {
711
+ logger?.debug(
712
+ `[${instanceId}] Confirmed disconnected device: ${missingDevice.name} (${missingDevice.id})`
713
+ )
714
+ audioDeviceManager.markDeviceAsDisconnected(
715
+ missingDevice.id,
716
+ false
717
+ )
718
+ })
719
+ }
720
+
721
+ // Notify listeners once with the final filtered state
722
+ audioDeviceManager.notifyListeners()
723
+ } catch (error) {
724
+ logger?.warn(
725
+ `[${instanceId}] Error in delayed device disconnection handling:`,
726
+ error
727
+ )
728
+ }
729
+ }, 500) // 500ms delay to let native layer update
730
+ } else if (event.reason === 'deviceConnected') {
731
+ // Device reconnected - force refresh to show it immediately
732
+ logger?.debug(
733
+ `[${instanceId}] Device connected, forcing refresh`
734
+ )
735
+ audioDeviceManager.forceRefreshDevices()
736
+ }
737
+
738
+ // Check if we have a callback configured
739
+ logger?.debug(
740
+ `[${instanceId}] recordingConfigRef.current exists:`,
741
+ !!recordingConfigRef.current
742
+ )
743
+
744
+ if (recordingConfigRef.current?.onRecordingInterrupted) {
745
+ try {
746
+ logger?.debug(
747
+ `[${instanceId}] Calling recording interruption callback`
748
+ )
749
+ recordingConfigRef.current.onRecordingInterrupted(event)
750
+ } catch (error) {
751
+ logger?.error(
752
+ `[${instanceId}] Error in recording interruption callback:`,
753
+ error
754
+ )
755
+ }
756
+ } else {
757
+ logger?.debug(
758
+ `[${instanceId}] No recording interruption callback configured`
759
+ )
760
+ }
761
+ })
762
+
763
+ return () => {
764
+ logger?.debug(
765
+ `[${instanceId}] Removing recording interruption listener`
766
+ )
767
+ subscription.remove()
768
+ }
769
+ }, [instanceId, logger]) // Include instanceId and logger in dependencies
770
+
771
+ return {
772
+ prepareRecording,
773
+ startRecording,
774
+ stopRecording,
775
+ pauseRecording,
776
+ resumeRecording,
777
+ isPaused: state.isPaused,
778
+ isRecording: state.isRecording,
779
+ durationMs: state.durationMs,
780
+ size: state.size,
781
+ compression: state.compression,
782
+ analysisData: state.analysisData,
783
+ }
784
+ }