@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,1112 @@
1
+ // packages/audio-studio/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt
2
+ package net.siteed.audiostudio
3
+
4
+ import android.Manifest
5
+ import android.os.Build
6
+ import android.os.Bundle
7
+ import android.util.Log
8
+ import android.content.pm.PackageManager
9
+ import androidx.annotation.RequiresApi
10
+ import androidx.core.content.ContextCompat
11
+ import androidx.core.os.bundleOf
12
+ import expo.modules.kotlin.Promise
13
+ import expo.modules.kotlin.modules.Module
14
+ import expo.modules.kotlin.modules.ModuleDefinition
15
+ import expo.modules.interfaces.permissions.Permissions
16
+ import java.util.zip.CRC32
17
+ import kotlinx.coroutines.CoroutineScope
18
+ import kotlinx.coroutines.Dispatchers
19
+ import kotlinx.coroutines.launch
20
+ import kotlinx.coroutines.withContext
21
+
22
+ class AudioStudioModule : Module(), EventSender {
23
+ companion object {
24
+ private const val CLASS_NAME = "AudioStudioModule"
25
+ }
26
+
27
+ private lateinit var audioRecorderManager: AudioRecorderManager
28
+ private lateinit var audioProcessor: AudioProcessor
29
+ private lateinit var audioDeviceManager: AudioDeviceManager
30
+ private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
31
+ private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
32
+ private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
33
+ private var enableDeviceDetection: Boolean = false // Default to false until we check manifest
34
+ private val coroutineScope = CoroutineScope(Dispatchers.Main)
35
+
36
+ private val audioFileHandler by lazy {
37
+ AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
38
+ }
39
+
40
+ private val audioTrimmer by lazy {
41
+ AudioTrimmer(
42
+ appContext.reactContext ?: throw IllegalStateException("React context not available"),
43
+ audioFileHandler
44
+ )
45
+ }
46
+
47
+ @RequiresApi(Build.VERSION_CODES.R)
48
+ override fun definition() = ModuleDefinition {
49
+ // The module will be accessible from `requireNativeModule('AudioStudio')` in JavaScript.
50
+ Name("AudioStudio")
51
+
52
+ // Check permissions declared in the manifest
53
+ try {
54
+ val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
55
+ val packageInfo = context.packageManager.getPackageInfo(
56
+ context.packageName,
57
+ PackageManager.GET_PERMISSIONS
58
+ )
59
+
60
+ // Check if READ_PHONE_STATE is in the requested permissions
61
+ enablePhoneStateHandling = packageInfo.requestedPermissions?.contains(Manifest.permission.READ_PHONE_STATE) ?: false
62
+
63
+ // Check if POST_NOTIFICATIONS is in the requested permissions
64
+ enableNotificationHandling = packageInfo.requestedPermissions?.contains(Manifest.permission.POST_NOTIFICATIONS) ?: false
65
+
66
+ // Check if background audio is enabled by looking for FOREGROUND_SERVICE_MICROPHONE permission
67
+ enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) ?: false
68
+
69
+ // Check if device detection is enabled by looking for BLUETOOTH_CONNECT permission
70
+ enableDeviceDetection = packageInfo.requestedPermissions?.contains(Manifest.permission.BLUETOOTH_CONNECT) ?: false
71
+
72
+ LogUtils.d(CLASS_NAME, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
73
+ LogUtils.d(CLASS_NAME, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
74
+ LogUtils.d(CLASS_NAME, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
75
+ LogUtils.d(CLASS_NAME, "Device detection ${if (enableDeviceDetection) "enabled" else "disabled"} based on manifest permissions")
76
+ } catch (e: Exception) {
77
+ LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
78
+ enablePhoneStateHandling = false
79
+ enableNotificationHandling = false
80
+ enableBackgroundAudio = false
81
+ enableDeviceDetection = false
82
+ }
83
+
84
+ Events(
85
+ Constants.AUDIO_EVENT_NAME,
86
+ Constants.AUDIO_ANALYSIS_EVENT_NAME,
87
+ Constants.RECORDING_INTERRUPTED_EVENT_NAME,
88
+ Constants.TRIM_PROGRESS_EVENT,
89
+ Constants.DEVICE_CHANGED_EVENT // Add device changed event name
90
+ )
91
+
92
+ // Initialize Managers
93
+ initializeManager()
94
+
95
+ // Add a convenience function to check for foreground service permission separately
96
+ fun isForegroundServiceMicRequired(): Boolean {
97
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
98
+ }
99
+
100
+ // Helper function to check if device detection is enabled
101
+ fun isDeviceDetectionEnabled(): Boolean {
102
+ return enableDeviceDetection
103
+ }
104
+
105
+ // Add device-related functions to the module
106
+
107
+ // Gets available audio input devices with an optional refresh parameter
108
+ AsyncFunction("getAvailableInputDevices") { options: Map<String, Any>?, promise: Promise ->
109
+ try {
110
+ LogUtils.d(CLASS_NAME, "getAvailableInputDevices called. Refresh: ${options?.get("refresh") ?: false}")
111
+
112
+ // Check if refresh is requested
113
+ if (options?.get("refresh") as? Boolean == true) {
114
+ audioDeviceManager.forceRefreshAudioDevices()
115
+ }
116
+
117
+ // Get the list of devices
118
+ audioDeviceManager.getAvailableInputDevices(promise)
119
+ } catch (e: Exception) {
120
+ LogUtils.e(CLASS_NAME, "Error getting available input devices: ${e.message}", e)
121
+ promise.reject("DEVICE_ERROR", "Failed to get available audio devices: ${e.message}", e)
122
+ }
123
+ }
124
+
125
+ // Gets the currently selected audio input device
126
+ AsyncFunction("getCurrentInputDevice") { promise: Promise ->
127
+ try {
128
+ LogUtils.d(CLASS_NAME, "getCurrentInputDevice called")
129
+ audioDeviceManager.getCurrentInputDevice(promise)
130
+ } catch (e: Exception) {
131
+ LogUtils.e(CLASS_NAME, "Error getting current input device: ${e.message}", e)
132
+ promise.reject("DEVICE_ERROR", "Failed to get current audio device: ${e.message}", e)
133
+ }
134
+ }
135
+
136
+ // Selects a specific audio input device for recording
137
+ AsyncFunction("selectInputDevice") { deviceId: String, promise: Promise ->
138
+ try {
139
+ LogUtils.d(CLASS_NAME, "selectInputDevice called with ID: $deviceId")
140
+ audioDeviceManager.selectInputDevice(deviceId, promise)
141
+
142
+ // Update recording if in progress
143
+ if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
144
+ LogUtils.d(CLASS_NAME, "selectInputDevice: Notifying recorder of device change")
145
+ audioRecorderManager.handleDeviceChange()
146
+ }
147
+ } catch (e: Exception) {
148
+ LogUtils.e(CLASS_NAME, "Error selecting input device: ${e.message}", e)
149
+ promise.reject("DEVICE_ERROR", "Failed to select audio device: ${e.message}", e)
150
+ }
151
+ }
152
+
153
+ // Resets to the default audio input device
154
+ AsyncFunction("resetToDefaultDevice") { promise: Promise ->
155
+ try {
156
+ LogUtils.d(CLASS_NAME, "resetToDefaultDevice called")
157
+ audioDeviceManager.resetToDefaultDevice { success, error ->
158
+ if (success) {
159
+ // Update recording if in progress
160
+ if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
161
+ LogUtils.d(CLASS_NAME, "resetToDefaultDevice: Notifying recorder of device change")
162
+ audioRecorderManager.handleDeviceChange()
163
+ }
164
+ promise.resolve(true)
165
+ } else {
166
+ LogUtils.e(CLASS_NAME, "Failed to reset to default device: ${error?.message}")
167
+ promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${error?.message}", error)
168
+ }
169
+ }
170
+ } catch (e: Exception) {
171
+ LogUtils.e(CLASS_NAME, "Error resetting to default device: ${e.message}", e)
172
+ promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${e.message}", e)
173
+ }
174
+ }
175
+
176
+ // Refreshes the audio devices list
177
+ Function("refreshAudioDevices") {
178
+ LogUtils.d(CLASS_NAME, "refreshAudioDevices called")
179
+ val success = audioDeviceManager.forceRefreshAudioDevices()
180
+ return@Function mapOf("success" to success)
181
+ }
182
+
183
+
184
+
185
+ AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
186
+ try {
187
+ // If notifications are requested but permission not in manifest, modify options
188
+ if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
189
+ val modifiedOptions = options.toMutableMap()
190
+ modifiedOptions["showNotification"] = false
191
+ LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
192
+
193
+ if (audioRecorderManager.prepareRecording(modifiedOptions)) {
194
+ promise.resolve(true)
195
+ } else {
196
+ promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
197
+ }
198
+ } else {
199
+ if (audioRecorderManager.prepareRecording(options)) {
200
+ promise.resolve(true)
201
+ } else {
202
+ promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
203
+ }
204
+ }
205
+ } catch (e: Exception) {
206
+ LogUtils.e(CLASS_NAME, "Error preparing recording", e)
207
+ promise.reject("PREPARE_ERROR", "Failed to prepare recording: ${e.message}", e)
208
+ }
209
+ }
210
+
211
+ AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
212
+ // If notifications are requested but permission not in manifest, modify options
213
+ if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
214
+ val modifiedOptions = options.toMutableMap()
215
+ modifiedOptions["showNotification"] = false
216
+ LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
217
+ audioRecorderManager.startRecording(modifiedOptions, promise)
218
+ } else {
219
+ audioRecorderManager.startRecording(options, promise)
220
+ }
221
+ }
222
+
223
+ Function("clearAudioFiles") {
224
+ audioRecorderManager.clearAudioStorage()
225
+ }
226
+
227
+ Function("status") {
228
+ return@Function audioRecorderManager.getStatus()
229
+ }
230
+
231
+ AsyncFunction("listAudioFiles") { promise: Promise ->
232
+ audioRecorderManager.listAudioFiles(promise)
233
+ }
234
+
235
+ AsyncFunction("pauseRecording") { promise: Promise ->
236
+ audioRecorderManager.pauseRecording(promise)
237
+ }
238
+
239
+ AsyncFunction("resumeRecording") { promise: Promise ->
240
+ LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
241
+ try {
242
+ audioRecorderManager.resumeRecording(object : Promise {
243
+ override fun resolve(value: Any?) {
244
+ LogUtils.d(CLASS_NAME, "⏺️ resumeRecording completed successfully")
245
+ promise.resolve(value)
246
+ }
247
+ override fun reject(code: String, message: String?, cause: Throwable?) {
248
+ LogUtils.e(CLASS_NAME, "⏺️ resumeRecording failed: $code - $message", cause)
249
+ promise.reject(code, message, cause)
250
+ }
251
+ })
252
+ } catch (e: Exception) {
253
+ LogUtils.e(CLASS_NAME, "⏺️ Exception when calling resumeRecording: ${e.message}", e)
254
+ promise.reject("RESUME_ERROR", "Failed to resume recording: ${e.message}", e)
255
+ }
256
+ }
257
+
258
+ AsyncFunction("stopRecording") { promise: Promise ->
259
+ audioRecorderManager.stopRecording(promise)
260
+ }
261
+
262
+ AsyncFunction("requestPermissionsAsync") { promise: Promise ->
263
+ try {
264
+ val permissions = mutableListOf(
265
+ Manifest.permission.RECORD_AUDIO
266
+ )
267
+
268
+ // Only add phone state permission if enabled
269
+ if (enablePhoneStateHandling) {
270
+ permissions.add(Manifest.permission.READ_PHONE_STATE)
271
+ }
272
+
273
+ // Add foreground service permission for Android 14+ only if background audio is enabled
274
+ if (isForegroundServiceMicRequired()) {
275
+ LogUtils.d(CLASS_NAME, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
276
+ permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
277
+ }
278
+
279
+ // Add device detection permissions if device detection is enabled
280
+ if (isDeviceDetectionEnabled()) {
281
+ // BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
282
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
283
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
284
+ LogUtils.d(CLASS_NAME, "Adding BLUETOOTH_CONNECT permission request for device detection")
285
+ }
286
+ }
287
+
288
+ LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
289
+ Permissions.askForPermissionsWithPermissionsManager(
290
+ appContext.permissions,
291
+ promise,
292
+ *permissions.toTypedArray()
293
+ )
294
+ } catch (e: Exception) {
295
+ LogUtils.e(CLASS_NAME, "Error requesting permissions", e)
296
+ promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
297
+ }
298
+ }
299
+
300
+ AsyncFunction("getPermissionsAsync") { promise: Promise ->
301
+ val permissions = mutableListOf(
302
+ Manifest.permission.RECORD_AUDIO
303
+ )
304
+
305
+ // Only add phone state permission if enabled
306
+ if (enablePhoneStateHandling) {
307
+ permissions.add(Manifest.permission.READ_PHONE_STATE)
308
+ }
309
+
310
+ // Only check foreground service permission when background audio is enabled
311
+ if (isForegroundServiceMicRequired()) {
312
+ permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
313
+ }
314
+
315
+ // Add device detection permissions if enabled
316
+ if (isDeviceDetectionEnabled()) {
317
+ // BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
318
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
319
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
320
+ }
321
+ }
322
+
323
+ Permissions.getPermissionsWithPermissionsManager(
324
+ appContext.permissions,
325
+ promise,
326
+ *permissions.toTypedArray()
327
+ )
328
+ }
329
+
330
+ AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
331
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enableNotificationHandling) {
332
+ // Only request notification permissions if enabled in manifest
333
+ Permissions.askForPermissionsWithPermissionsManager(
334
+ appContext.permissions,
335
+ promise,
336
+ Manifest.permission.POST_NOTIFICATIONS
337
+ )
338
+ } else {
339
+ // Either notifications not required or running on Android < 13
340
+ promise.resolve(
341
+ bundleOf(
342
+ "status" to "granted",
343
+ "expires" to "never",
344
+ "granted" to true
345
+ )
346
+ )
347
+ }
348
+ }
349
+
350
+ AsyncFunction("getNotificationPermissionsAsync") { promise: Promise ->
351
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enableNotificationHandling) {
352
+ // Only check notification permissions if enabled in manifest
353
+ Permissions.getPermissionsWithPermissionsManager(
354
+ appContext.permissions,
355
+ promise,
356
+ Manifest.permission.POST_NOTIFICATIONS
357
+ )
358
+ } else {
359
+ // Either notifications not required or running on Android < 13
360
+ promise.resolve(
361
+ bundleOf(
362
+ "status" to "granted",
363
+ "expires" to "never",
364
+ "granted" to true
365
+ )
366
+ )
367
+ }
368
+ }
369
+
370
+ AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
371
+ try {
372
+ val fileUri = options["fileUri"] as? String ?: run {
373
+ promise.reject("INVALID_URI", "fileUri is required", null)
374
+ return@AsyncFunction
375
+ }
376
+
377
+ LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
378
+ LogUtils.d(CLASS_NAME, "Full options: $options")
379
+
380
+ val mode = options["mode"] as? String ?: "single"
381
+ val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
382
+ val endTimeMs = (options["endTimeMs"] as? Number)?.toLong()
383
+
384
+ @Suppress("UNCHECKED_CAST")
385
+ val ranges = options["ranges"] as? List<Map<String, Long>>
386
+
387
+ val outputFileName = options["outputFileName"] as? String
388
+
389
+ @Suppress("UNCHECKED_CAST")
390
+ var outputFormatMap = options["outputFormat"] as? Map<String, Any>
391
+
392
+ // Validate output format if provided
393
+ if (outputFormatMap != null) {
394
+ val format = outputFormatMap["format"] as? String
395
+ if (format != null && format != "wav" && format != "aac" && format != "opus") {
396
+ LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
397
+ // Create a new map with the corrected format
398
+ val newOutputFormat = HashMap<String, Any>(outputFormatMap)
399
+ newOutputFormat["format"] = "aac"
400
+ outputFormatMap = newOutputFormat
401
+ }
402
+ }
403
+
404
+ LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
405
+
406
+ // Create progress listener
407
+ val progressListener = object : AudioTrimmer.ProgressListener {
408
+ override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
409
+ sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
410
+ "progress" to progress,
411
+ "bytesProcessed" to bytesProcessed,
412
+ "totalBytes" to totalBytes
413
+ ))
414
+ }
415
+ }
416
+
417
+ // Record start time
418
+ val startTime = System.currentTimeMillis()
419
+
420
+ // Perform the trim operation
421
+ val result = audioTrimmer.trimAudio(
422
+ fileUri = fileUri,
423
+ mode = mode,
424
+ startTimeMs = startTimeMs,
425
+ endTimeMs = endTimeMs,
426
+ ranges = ranges,
427
+ outputFileName = outputFileName,
428
+ outputFormat = outputFormatMap,
429
+ progressListener = progressListener
430
+ )
431
+
432
+ // Calculate processing time
433
+ val processingTimeMs = System.currentTimeMillis() - startTime
434
+
435
+ // Add processing time to result
436
+ val resultWithProcessingTime = result.toMutableMap()
437
+ resultWithProcessingTime["processingInfo"] = mapOf(
438
+ "durationMs" to processingTimeMs
439
+ )
440
+
441
+ LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
442
+ promise.resolve(resultWithProcessingTime)
443
+ } catch (e: Exception) {
444
+ LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
445
+ promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
446
+ }
447
+ }
448
+
449
+ AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
450
+ try {
451
+ // Log all incoming options for debugging
452
+ LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
453
+
454
+ // Extract required parameters with detailed logging
455
+ val fileUri = options["fileUri"] as? String
456
+ LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
457
+ if (fileUri == null) {
458
+ LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
459
+ throw IllegalArgumentException("fileUri is required")
460
+ }
461
+
462
+ val windowSizeMs = options["windowSizeMs"] as? Double
463
+ LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
464
+ if (windowSizeMs == null) {
465
+ LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
466
+ throw IllegalArgumentException("windowSizeMs is required")
467
+ }
468
+
469
+ val hopLengthMs = options["hopLengthMs"] as? Double
470
+ LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
471
+ if (hopLengthMs == null) {
472
+ LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
473
+ throw IllegalArgumentException("hopLengthMs is required")
474
+ }
475
+
476
+ // Handle nMels which might come as Double from JavaScript
477
+ val nMelsValue = options["nMels"]
478
+ LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
479
+
480
+ val nMels = when (nMelsValue) {
481
+ is Int -> nMelsValue
482
+ is Double -> nMelsValue.toInt()
483
+ is Number -> nMelsValue.toInt()
484
+ else -> {
485
+ LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
486
+ throw IllegalArgumentException("nMels is required and must be a number")
487
+ }
488
+ }
489
+
490
+ LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
491
+
492
+ // Extract optional parameters with defaults
493
+ val fMin = options["fMin"] as? Double ?: 0.0
494
+ val fMax = options["fMax"] as? Double
495
+ val windowType = options["windowType"] as? String ?: "hann"
496
+ val normalize = options["normalize"] as? Boolean ?: false
497
+ val logScale = options["logScale"] as? Boolean ?: true
498
+
499
+ // Fix the conversion from Number to Long to preserve decimal values
500
+ val startTimeMsNumber = options["startTimeMs"] as? Number
501
+ val endTimeMsNumber = options["endTimeMs"] as? Number
502
+ val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
503
+ val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
504
+
505
+ LogUtils.d(CLASS_NAME, """
506
+ Optional parameters:
507
+ - fMin: $fMin
508
+ - fMax: $fMax
509
+ - windowType: $windowType
510
+ - normalize: $normalize
511
+ - logScale: $logScale
512
+ - startTimeMs: $startTimeMs (original: $startTimeMsNumber)
513
+ - endTimeMs: $endTimeMs (original: $endTimeMsNumber)
514
+ """.trimIndent())
515
+
516
+ // Handle decoding options
517
+ val decodingOptions = options["decodingOptions"] as? Map<String, Any>
518
+ LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
519
+
520
+ val config = decodingOptions?.let {
521
+ val targetSampleRateValue = it["targetSampleRate"]
522
+ val targetSampleRate = when (targetSampleRateValue) {
523
+ is Int -> targetSampleRateValue
524
+ is Double -> targetSampleRateValue.toInt()
525
+ is Number -> targetSampleRateValue.toInt()
526
+ else -> null
527
+ }
528
+
529
+ val targetChannelsValue = it["targetChannels"]
530
+ val targetChannels = when (targetChannelsValue) {
531
+ is Int -> targetChannelsValue
532
+ is Double -> targetChannelsValue.toInt()
533
+ is Number -> targetChannelsValue.toInt()
534
+ else -> 1
535
+ }
536
+
537
+ val targetBitDepthValue = it["targetBitDepth"]
538
+ val targetBitDepth = when (targetBitDepthValue) {
539
+ is Int -> targetBitDepthValue
540
+ is Double -> targetBitDepthValue.toInt()
541
+ is Number -> targetBitDepthValue.toInt()
542
+ else -> 16
543
+ }
544
+
545
+ val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
546
+
547
+ DecodingConfig(
548
+ targetSampleRate = targetSampleRate,
549
+ targetChannels = targetChannels,
550
+ targetBitDepth = targetBitDepth,
551
+ normalizeAudio = normalizeAudio
552
+ ).also { config ->
553
+ LogUtils.d(CLASS_NAME, """
554
+ Using decoding config:
555
+ - targetSampleRate: ${config.targetSampleRate ?: "original"}
556
+ - targetChannels: ${config.targetChannels ?: "original"}
557
+ - targetBitDepth: ${config.targetBitDepth}
558
+ - normalizeAudio: ${config.normalizeAudio}
559
+ """.trimIndent())
560
+ }
561
+ } ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
562
+ LogUtils.d(CLASS_NAME, "Using default decoding config")
563
+ }
564
+
565
+ // Check if the audio data is too short
566
+ if (startTimeMs != null && endTimeMs != null) {
567
+ val durationMs = endTimeMs - startTimeMs
568
+ LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
569
+ if (durationMs < 25) { // 25ms is minimum for a single window
570
+ LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
571
+ throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
572
+ }
573
+ }
574
+
575
+ // Load audio data with optional time range
576
+ LogUtils.d(CLASS_NAME, "Loading audio data...")
577
+ val audioData = when {
578
+ startTimeMs != null && endTimeMs != null -> {
579
+ LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
580
+ audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
581
+ }
582
+ else -> {
583
+ LogUtils.d(CLASS_NAME, "Loading entire audio file")
584
+ audioProcessor.loadAudioFromAnyFormat(fileUri, config)
585
+ }
586
+ }
587
+
588
+ if (audioData == null) {
589
+ LogUtils.e(CLASS_NAME, "Failed to load audio data")
590
+ throw IllegalStateException("Failed to load audio data")
591
+ }
592
+
593
+ LogUtils.d(CLASS_NAME, """
594
+ Audio data loaded successfully:
595
+ - data size: ${audioData.data.size} bytes
596
+ - sampleRate: ${audioData.sampleRate}
597
+ - channels: ${audioData.channels}
598
+ - bitDepth: ${audioData.bitDepth}
599
+ - durationMs: ${audioData.durationMs}
600
+ """.trimIndent())
601
+
602
+ // Validate that we have enough audio data for processing
603
+ if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
604
+ LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
605
+ throw IllegalArgumentException(
606
+ "Audio data is too short for spectrogram analysis. " +
607
+ "Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
608
+ )
609
+ }
610
+
611
+ // Compute mel-spectrogram
612
+ LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
613
+ val spectrogramData = audioProcessor.extractMelSpectrogram(
614
+ audioData = audioData,
615
+ windowSizeMs = windowSizeMs.toFloat(),
616
+ hopLengthMs = hopLengthMs.toFloat(),
617
+ nMels = nMels,
618
+ fMin = fMin.toFloat(),
619
+ fMax = fMax?.toFloat() ?: (audioData.sampleRate.toFloat() / 2),
620
+ normalize = normalize,
621
+ logScaling = logScale,
622
+ windowType = windowType
623
+ )
624
+
625
+ LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
626
+
627
+ // Convert to map for React Native
628
+ val result = mapOf(
629
+ "spectrogram" to spectrogramData.spectrogram.map { it.toList() },
630
+ "sampleRate" to audioData.sampleRate,
631
+ "nMels" to nMels,
632
+ "timeSteps" to spectrogramData.spectrogram.size,
633
+ "durationMs" to audioData.durationMs
634
+ )
635
+
636
+ LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
637
+ promise.resolve(result)
638
+ } catch (e: Exception) {
639
+ LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
640
+ LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
641
+ promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
642
+ }
643
+ }
644
+
645
+ OnDestroy {
646
+ AudioRecorderManager.destroy()
647
+ }
648
+
649
+ // Add a new function to check if recording is actually running
650
+ AsyncFunction("checkRecordingStatus") { promise: Promise ->
651
+ val isServiceRunning = AudioRecordingService.isServiceRunning()
652
+
653
+ val status = audioRecorderManager.getStatus()
654
+
655
+ // If service is running but isRecording is false, we need to cleanup
656
+ if (isServiceRunning && !status.getBoolean("isRecording")) {
657
+ audioRecorderManager.cleanup()
658
+ AudioRecordingService.stopService(appContext.reactContext!!)
659
+ }
660
+
661
+ promise.resolve(status)
662
+ }
663
+
664
+
665
+ AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
666
+ try {
667
+ val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
668
+
669
+ // Get time or byte range options
670
+ val startTimeMs = options["startTimeMs"] as? Number
671
+ val endTimeMs = options["endTimeMs"] as? Number
672
+ val position = options["position"] as? Number
673
+ val length = options["length"] as? Number
674
+ val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
675
+
676
+ // Validate ranges - can have time range OR byte range OR no range
677
+ val hasTimeRange = startTimeMs != null && endTimeMs != null
678
+ val hasByteRange = position != null && length != null
679
+
680
+ // Only throw if both ranges are provided
681
+ if (hasTimeRange && hasByteRange) {
682
+ throw IllegalArgumentException("Cannot specify both time range and byte range")
683
+ }
684
+
685
+ // Get decoding options with default configuration
686
+ val defaultConfig = DecodingConfig(
687
+ targetSampleRate = null,
688
+ targetChannels = 1, // Default to mono
689
+ targetBitDepth = 16,
690
+ normalizeAudio = false
691
+ )
692
+
693
+ val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
694
+ DecodingConfig(
695
+ targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
696
+ targetChannels = decodingOptionsMap["targetChannels"] as? Int,
697
+ targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
698
+ normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
699
+ )
700
+ } ?: defaultConfig
701
+
702
+ // Load audio data based on range type (or full file if no range specified)
703
+ val audioData = when {
704
+ hasByteRange -> {
705
+ val format = audioProcessor.getAudioFormat(fileUri)
706
+ ?: throw IllegalArgumentException("Could not determine audio format")
707
+
708
+ // Calculate time range from byte position
709
+ val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
710
+ val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
711
+ val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
712
+
713
+ LogUtils.d(CLASS_NAME, "Loading audio with byte range: position=$position, length=$length")
714
+
715
+ audioProcessor.loadAudioRange(
716
+ fileUri = fileUri,
717
+ startTimeMs = effectiveStartTimeMs,
718
+ endTimeMs = effectiveEndTimeMs,
719
+ config = config
720
+ )
721
+ }
722
+ hasTimeRange -> {
723
+ LogUtils.d(CLASS_NAME, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
724
+
725
+ audioProcessor.loadAudioRange(
726
+ fileUri = fileUri,
727
+ startTimeMs = startTimeMs!!.toLong(),
728
+ endTimeMs = endTimeMs!!.toLong(),
729
+ config = config
730
+ )
731
+ }
732
+ else -> {
733
+ LogUtils.d(CLASS_NAME, "Loading entire audio file")
734
+ audioProcessor.loadAudioFromAnyFormat(fileUri, config)
735
+ }
736
+ } ?: throw IllegalStateException("Failed to load audio data")
737
+
738
+ val featuresMap = options["features"] as? Map<*, *>
739
+ val features = Features.parseFeatureOptions(featuresMap)
740
+
741
+ val recordingConfig = RecordingConfig(
742
+ sampleRate = audioData.sampleRate,
743
+ channels = audioData.channels,
744
+ encoding = when (audioData.bitDepth) {
745
+ 8 -> "pcm_8bit"
746
+ 16 -> "pcm_16bit"
747
+ 32 -> "pcm_32bit"
748
+ else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
749
+ },
750
+ segmentDurationMs = segmentDurationMs,
751
+ features = features
752
+ )
753
+
754
+ LogUtils.d(CLASS_NAME, "extractAudioAnalysis: $recordingConfig")
755
+ audioProcessor.resetCumulativeAmplitudeRange()
756
+
757
+ val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
758
+ promise.resolve(analysisData.toDictionary())
759
+ } catch (e: Exception) {
760
+ LogUtils.e(CLASS_NAME, "Failed to extract audio analysis: ${e.message}", e)
761
+ promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
762
+ }
763
+ }
764
+
765
+ AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
766
+ try {
767
+ val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
768
+ val startTimeMs = options["startTimeMs"] as? Number
769
+ val endTimeMs = options["endTimeMs"] as? Number
770
+ val position = options["position"] as? Number
771
+ val length = options["length"] as? Number
772
+
773
+ // Validate that we have either time range or byte range, but not both and not neither
774
+ val hasTimeRange = startTimeMs != null && endTimeMs != null
775
+ val hasByteRange = position != null && length != null
776
+
777
+ if (!hasTimeRange && !hasByteRange) {
778
+ throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
779
+ }
780
+ if (hasTimeRange && hasByteRange) {
781
+ throw IllegalArgumentException("Cannot specify both time range and byte range")
782
+ }
783
+
784
+ // Get decoding options
785
+ val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
786
+ val decodingConfig = if (decodingOptionsMap != null) {
787
+ DecodingConfig(
788
+ targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
789
+ targetChannels = decodingOptionsMap["targetChannels"] as? Int,
790
+ targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
791
+ normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
792
+ ).also {
793
+ LogUtils.d(CLASS_NAME, """
794
+ Using decoding config:
795
+ - targetSampleRate: ${it.targetSampleRate ?: "original"}
796
+ - targetChannels: ${it.targetChannels ?: "original"}
797
+ - targetBitDepth: ${it.targetBitDepth}
798
+ - normalizeAudio: ${it.normalizeAudio}
799
+ """.trimIndent())
800
+ }
801
+ } else null
802
+
803
+ val audioData = if (hasByteRange) {
804
+ val format = audioProcessor.getAudioFormat(fileUri)
805
+ ?: throw IllegalArgumentException("Could not determine audio format")
806
+
807
+ // Calculate time range from byte position
808
+ val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
809
+ val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
810
+ val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
811
+
812
+ LogUtils.d(CLASS_NAME, """
813
+ Converting byte range to time range:
814
+ - position: $position bytes
815
+ - length: $length bytes
816
+ - bytesPerSecond: $bytesPerSecond
817
+ - effectiveStartTimeMs: $effectiveStartTimeMs
818
+ - effectiveEndTimeMs: $effectiveEndTimeMs
819
+ """.trimIndent())
820
+
821
+ audioProcessor.loadAudioRange(
822
+ fileUri = fileUri,
823
+ startTimeMs = effectiveStartTimeMs,
824
+ endTimeMs = effectiveEndTimeMs,
825
+ config = decodingConfig
826
+ )
827
+ } else {
828
+ // Must be time range due to earlier validation
829
+ LogUtils.d(CLASS_NAME, """
830
+ Using time range:
831
+ - startTimeMs: $startTimeMs
832
+ - endTimeMs: $endTimeMs
833
+ """.trimIndent())
834
+
835
+ audioProcessor.loadAudioRange(
836
+ fileUri = fileUri,
837
+ startTimeMs = startTimeMs!!.toLong(),
838
+ endTimeMs = endTimeMs!!.toLong(),
839
+ config = decodingConfig
840
+ )
841
+ } ?: throw IllegalStateException("Failed to load audio data")
842
+
843
+ LogUtils.d(CLASS_NAME, """
844
+ Audio data loaded successfully:
845
+ - data size: ${audioData.data.size} bytes
846
+ - sampleRate: ${audioData.sampleRate}
847
+ - channels: ${audioData.channels}
848
+ - bitDepth: ${audioData.bitDepth}
849
+ - durationMs: ${audioData.durationMs}
850
+ """.trimIndent())
851
+
852
+ val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
853
+ val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
854
+ val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
855
+ val bytesPerSample = audioData.bitDepth / 8
856
+ val samples = audioData.data.size / (bytesPerSample * audioData.channels)
857
+
858
+ // Create the result map
859
+ val resultMap = mutableMapOf<String, Any>()
860
+
861
+ // Add WAV header if requested
862
+ if (includeWavHeader) {
863
+ // Use ByteArrayOutputStream to write the WAV header and data
864
+ val outputStream = java.io.ByteArrayOutputStream()
865
+ val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
866
+
867
+ // Write the WAV header
868
+ audioFileHandler.writeWavHeader(
869
+ outputStream,
870
+ audioData.sampleRate,
871
+ audioData.channels,
872
+ audioData.bitDepth
873
+ )
874
+
875
+ // Write the PCM data
876
+ outputStream.write(audioData.data)
877
+
878
+ // Get the complete WAV data
879
+ val wavData = outputStream.toByteArray()
880
+
881
+ resultMap["pcmData"] = wavData
882
+ resultMap["hasWavHeader"] = true
883
+
884
+ LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
885
+ } else {
886
+ resultMap["pcmData"] = audioData.data
887
+ resultMap["hasWavHeader"] = false
888
+ }
889
+
890
+ // Add the rest of the data
891
+ resultMap.putAll(mapOf(
892
+ "sampleRate" to audioData.sampleRate,
893
+ "channels" to audioData.channels,
894
+ "bitDepth" to audioData.bitDepth,
895
+ "durationMs" to audioData.durationMs,
896
+ "format" to "pcm_${audioData.bitDepth}bit",
897
+ "samples" to samples
898
+ ))
899
+
900
+ // Add checksum if requested
901
+ if (options["computeChecksum"] == true) {
902
+ val crc32 = CRC32()
903
+ crc32.update(audioData.data)
904
+ resultMap["checksum"] = crc32.value.toInt()
905
+
906
+ LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
907
+ }
908
+
909
+ if (includeNormalizedData) {
910
+ val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
911
+ audioData.data,
912
+ "pcm_${audioData.bitDepth}bit"
913
+ )
914
+ resultMap["normalizedData"] = float32Data
915
+ }
916
+
917
+ if (includeBase64Data) {
918
+ // Convert the PCM data to a base64 string
919
+ val base64Data = android.util.Base64.encodeToString(
920
+ audioData.data,
921
+ android.util.Base64.NO_WRAP
922
+ )
923
+ resultMap["base64Data"] = base64Data
924
+ }
925
+
926
+ promise.resolve(resultMap)
927
+ } catch (e: Exception) {
928
+ LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
929
+ LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
930
+ promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
931
+ }
932
+ }
933
+ }
934
+
935
+ private fun initializeManager() {
936
+ val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
937
+ val filesDir = context.filesDir
938
+ val permissionUtils = PermissionUtils(context)
939
+ val audioDataEncoder = AudioDataEncoder()
940
+
941
+ // Initialize AudioDeviceManager
942
+ LogUtils.d(CLASS_NAME, "🔧 Initializing AudioDeviceManager...")
943
+ LogUtils.d(CLASS_NAME, "🔧 Device detection enabled: $enableDeviceDetection")
944
+ audioDeviceManager = AudioDeviceManager(context, enableDeviceDetection)
945
+ LogUtils.d(CLASS_NAME, "🔧 AudioDeviceManager initialized")
946
+
947
+ // Initialize AudioRecorderManager with AudioDeviceManager integration
948
+ audioRecorderManager = AudioRecorderManager.initialize(
949
+ context,
950
+ filesDir,
951
+ permissionUtils,
952
+ audioDataEncoder,
953
+ this,
954
+ enablePhoneStateHandling,
955
+ enableBackgroundAudio
956
+ )
957
+
958
+ // Set up the delegate for the AudioDeviceManager
959
+ audioDeviceManager.delegate = object : AudioDeviceManagerDelegate {
960
+ override fun onDeviceDisconnected(deviceId: String) {
961
+ LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
962
+ // Handle device disconnection
963
+ coroutineScope.launch {
964
+ try {
965
+ // If recording is active, handle the disconnection based on the recording config
966
+ if (audioRecorderManager.isRecording) {
967
+ handleDeviceDisconnection(deviceId)
968
+ }
969
+
970
+ // Notify JS about the disconnection
971
+ sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
972
+ "type" to "deviceDisconnected",
973
+ "deviceId" to deviceId
974
+ ))
975
+ } catch (e: Exception) {
976
+ LogUtils.e(CLASS_NAME, "📱 Error handling device disconnection: ${e.message}", e)
977
+ }
978
+ }
979
+ }
980
+ }
981
+
982
+ // Set up connection callback
983
+ audioDeviceManager.onDeviceConnected = { deviceId ->
984
+ LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
985
+ // Notify JS about the connection
986
+ sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
987
+ "type" to "deviceConnected",
988
+ "deviceId" to deviceId
989
+ ))
990
+ }
991
+
992
+ // Set up disconnection callback
993
+ audioDeviceManager.onDeviceDisconnected = { deviceId ->
994
+ LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
995
+ // Notify JS about the disconnection
996
+ sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
997
+ "type" to "deviceDisconnected",
998
+ "deviceId" to deviceId
999
+ ))
1000
+ }
1001
+
1002
+ audioProcessor = AudioProcessor(filesDir)
1003
+ }
1004
+
1005
+ /**
1006
+ * Handles audio device disconnection based on the recording configuration
1007
+ */
1008
+ private suspend fun handleDeviceDisconnection(deviceId: String) {
1009
+ LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection called for device: $deviceId")
1010
+ // Get disconnection behavior from recorder config
1011
+ val behavior = audioRecorderManager.getDeviceDisconnectionBehavior()
1012
+ LogUtils.d(CLASS_NAME, "📱 Device disconnection behavior configured as: $behavior")
1013
+
1014
+ when (behavior) {
1015
+ "fallback" -> {
1016
+ LogUtils.d(CLASS_NAME, "📱 Using fallback behavior, getting default device")
1017
+ // Get default device
1018
+ val defaultDevice = withContext(Dispatchers.IO) {
1019
+ audioDeviceManager.getDefaultInputDevice()
1020
+ }
1021
+
1022
+ if (defaultDevice != null) {
1023
+ LogUtils.d(CLASS_NAME, "📱 Falling back to default device: ${defaultDevice["name"]}")
1024
+
1025
+ // Select default device
1026
+ val deviceId = defaultDevice["id"] as String
1027
+ LogUtils.d(CLASS_NAME, "📱 Attempting to select default device: $deviceId")
1028
+ val success = audioDeviceManager.selectDevice(deviceId)
1029
+
1030
+ if (success) {
1031
+ LogUtils.d(CLASS_NAME, "📱 Successfully selected default device, notifying AudioRecorderManager")
1032
+ // Notify AudioRecorderManager to update its recording source
1033
+ audioRecorderManager.handleDeviceChange()
1034
+
1035
+ // Notify JS about fallback
1036
+ LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
1037
+ sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1038
+ "reason" to "deviceFallback",
1039
+ "isPaused" to false,
1040
+ "deviceId" to deviceId
1041
+ ))
1042
+ } else {
1043
+ LogUtils.e(CLASS_NAME, "📱 Failed to select default device, pausing recording")
1044
+
1045
+ // Fall back to pause if we can't select the default device
1046
+ audioRecorderManager.pauseRecording(object : Promise {
1047
+ override fun resolve(value: Any?) {
1048
+ LogUtils.d(CLASS_NAME, "📱 Recording successfully paused, notifying AudioRecorderManager")
1049
+ // Notify AudioRecorderManager to handle device change while paused
1050
+ audioRecorderManager.handleDeviceChange()
1051
+
1052
+ sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1053
+ "reason" to "deviceSwitchFailed",
1054
+ "isPaused" to true
1055
+ ))
1056
+ }
1057
+ override fun reject(code: String, message: String?, cause: Throwable?) {
1058
+ LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1059
+ }
1060
+ })
1061
+ }
1062
+ } else {
1063
+ LogUtils.e(CLASS_NAME, "📱 No default device found, pausing recording")
1064
+
1065
+ // Fall back to pause if we can't find a default device
1066
+ audioRecorderManager.pauseRecording(object : Promise {
1067
+ override fun resolve(value: Any?) {
1068
+ LogUtils.d(CLASS_NAME, "📱 Recording successfully paused when no default device found")
1069
+ // Notify AudioRecorderManager to handle device change while paused
1070
+ audioRecorderManager.handleDeviceChange()
1071
+
1072
+ sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1073
+ "reason" to "deviceDisconnected",
1074
+ "isPaused" to true
1075
+ ))
1076
+ }
1077
+ override fun reject(code: String, message: String?, cause: Throwable?) {
1078
+ LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1079
+ }
1080
+ })
1081
+ }
1082
+ }
1083
+
1084
+ else -> { // Default to pause behavior
1085
+ LogUtils.d(CLASS_NAME, "📱 Using pause behavior for device disconnection")
1086
+
1087
+ // Pause recording
1088
+ audioRecorderManager.pauseRecording(object : Promise {
1089
+ override fun resolve(value: Any?) {
1090
+ LogUtils.d(CLASS_NAME, "📱 Recording successfully paused after device disconnection")
1091
+ // Notify AudioRecorderManager to handle device change while paused
1092
+ audioRecorderManager.handleDeviceChange()
1093
+
1094
+ sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1095
+ "reason" to "deviceDisconnected",
1096
+ "isPaused" to true
1097
+ ))
1098
+ }
1099
+ override fun reject(code: String, message: String?, cause: Throwable?) {
1100
+ LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
1101
+ }
1102
+ })
1103
+ }
1104
+ }
1105
+ LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection completed")
1106
+ }
1107
+
1108
+ override fun sendExpoEvent(eventName: String, params: Bundle) {
1109
+ LogUtils.d(CLASS_NAME, "Sending event: $eventName")
1110
+ this@AudioStudioModule.sendEvent(eventName, params)
1111
+ }
1112
+ }