@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,1741 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import android.Manifest
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothProfile
7
+ import android.content.BroadcastReceiver
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.IntentFilter
11
+ import android.content.pm.PackageManager
12
+ import android.media.AudioDeviceInfo
13
+ import android.media.AudioFormat
14
+ import android.media.AudioManager
15
+ import android.media.AudioRecord
16
+ import android.media.MediaRecorder
17
+ import android.os.Build
18
+ import android.hardware.usb.UsbManager
19
+ import android.util.Log
20
+ import androidx.annotation.RequiresApi
21
+ import expo.modules.kotlin.Promise
22
+ import net.siteed.audiostudio.LogUtils
23
+ import kotlinx.coroutines.CoroutineScope
24
+ import kotlinx.coroutines.Dispatchers
25
+ import kotlinx.coroutines.launch
26
+ import kotlinx.coroutines.delay
27
+
28
+ /**
29
+ * Constants not available in all Android versions
30
+ */
31
+ private const val ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED"
32
+
33
+ /**
34
+ * Interface for handling audio device disconnection events
35
+ */
36
+ interface AudioDeviceManagerDelegate {
37
+ fun onDeviceDisconnected(deviceId: String)
38
+ }
39
+
40
+ /**
41
+ * Manages audio device detection, selection and capabilities for Android
42
+ */
43
+ class AudioDeviceManager(
44
+ private val context: Context,
45
+ private val enableDeviceDetection: Boolean = true
46
+ ) {
47
+
48
+ companion object {
49
+ private const val TAG = "AudioDeviceManager"
50
+ private const val CLASS_NAME = "AudioDeviceManager" // Add class name constant for logging
51
+
52
+ // Device type constants - standardized across platforms
53
+ const val DEVICE_TYPE_BUILTIN_MIC = "builtin_mic"
54
+ const val DEVICE_TYPE_BLUETOOTH = "bluetooth"
55
+ const val DEVICE_TYPE_USB = "usb"
56
+ const val DEVICE_TYPE_WIRED_HEADSET = "wired_headset"
57
+ const val DEVICE_TYPE_WIRED_HEADPHONES = "wired_headphones"
58
+ const val DEVICE_TYPE_SPEAKER = "speaker"
59
+ const val DEVICE_TYPE_UNKNOWN = "unknown"
60
+
61
+ // Common sample rates most devices support
62
+ private val COMMON_SAMPLE_RATES = listOf(8000, 11025, 16000, 22050, 32000, 44100, 48000)
63
+
64
+ // Common channel configurations most devices support
65
+ private val COMMON_CHANNEL_COUNTS = listOf(1, 2)
66
+ }
67
+
68
+ // Delegate for handling device disconnection
69
+ var delegate: AudioDeviceManagerDelegate? = null
70
+
71
+ // Simple callback for device connections
72
+ var onDeviceConnected: ((String) -> Unit)? = null
73
+
74
+ // Simple callback for device disconnections
75
+ var onDeviceDisconnected: ((String) -> Unit)? = null
76
+
77
+ // Audio manager for accessing device information
78
+ private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
79
+
80
+ // Last selected device ID for tracking changes
81
+ private var lastSelectedDeviceId: String? = null
82
+
83
+ // BroadcastReceiver for device connection/disconnection
84
+ private var deviceReceiver: BroadcastReceiver? = null
85
+
86
+ // Coroutine scope for async operations
87
+ private val coroutineScope = CoroutineScope(Dispatchers.Main)
88
+
89
+ init {
90
+ // Start monitoring device changes only if BT_CONNECT permission is available (#294)
91
+ if (enableDeviceDetection) {
92
+ startMonitoringDeviceChanges()
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Gets all available audio input devices
98
+ */
99
+ fun getAvailableInputDevices(promise: Promise) {
100
+ LogUtils.d(TAG, "Getting available input devices")
101
+
102
+ val devices = mutableListOf<Map<String, Any>>()
103
+ val currentInput = getCurrentInputDeviceInternal()
104
+
105
+ // Device map for smart deduplication
106
+ // We won't deduplicate devices with different capabilities
107
+ // This ensures we preserve devices that represent different recording profiles
108
+ val deviceMap = mutableMapOf<String, MutableList<Map<String, Any>>>()
109
+
110
+ // Get all audio devices
111
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
112
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
113
+
114
+ for (device in audioDevices) {
115
+ if (device.type == AudioDeviceInfo.TYPE_UNKNOWN) {
116
+ continue
117
+ }
118
+
119
+ val deviceId = device.id.toString()
120
+ val isDefault = currentInput?.get("id") == deviceId
121
+ val deviceType = mapDeviceType(device)
122
+ val deviceName = getDeviceName(device)
123
+
124
+ val capabilities = getDeviceCapabilities(device)
125
+
126
+ LogUtils.d(TAG, "Raw device found: ${deviceName} (ID: ${deviceId}, type: ${deviceType})")
127
+
128
+ val deviceInfo = mapOf(
129
+ "id" to deviceId,
130
+ "name" to deviceName,
131
+ "type" to deviceType,
132
+ "isDefault" to isDefault,
133
+ "capabilities" to capabilities,
134
+ "isAvailable" to true
135
+ )
136
+
137
+ // Group devices by name for potential deduplication
138
+ val key = deviceName
139
+ if (!deviceMap.containsKey(key)) {
140
+ deviceMap[key] = mutableListOf()
141
+ }
142
+ deviceMap[key]?.add(deviceInfo)
143
+ }
144
+
145
+ // Deduplicate while preserving devices with different capabilities
146
+ deviceMap.forEach { (name, deviceList) ->
147
+ if (deviceList.size > 1) {
148
+ LogUtils.d(TAG, "Found ${deviceList.size} devices with name: $name - checking for duplicates")
149
+
150
+ // First check if we have a default device in this group - it gets priority
151
+ val defaultDevice = deviceList.find { it["isDefault"] == true }
152
+ if (defaultDevice != null) {
153
+ // Always keep the default device
154
+ LogUtils.d(TAG, "Keeping default device with ID: ${defaultDevice["id"]}")
155
+ devices.add(defaultDevice)
156
+
157
+ // Now process the others
158
+ val remainingDevices = deviceList.filter { it["id"] != defaultDevice["id"] }
159
+
160
+ // Create groups based on unique capabilities
161
+ val capabilityGroups = mutableMapOf<String, MutableList<Map<String, Any>>>()
162
+
163
+ for (device in remainingDevices) {
164
+ val capabilities = device["capabilities"] as Map<String, Any>
165
+ val capabilityHash = generateCapabilityHash(capabilities)
166
+
167
+ if (!capabilityGroups.containsKey(capabilityHash)) {
168
+ capabilityGroups[capabilityHash] = mutableListOf()
169
+ }
170
+ capabilityGroups[capabilityHash]?.add(device)
171
+ }
172
+
173
+ // Now keep one device from each capability group
174
+ capabilityGroups.forEach { (capHash, devicesWithSameCapabilities) ->
175
+ val selectedDevice = devicesWithSameCapabilities.first()
176
+ LogUtils.d(TAG, "Adding device ${selectedDevice["id"]} with unique capabilities: $capHash")
177
+ devices.add(selectedDevice)
178
+
179
+ // Log if we're dropping duplicate devices
180
+ if (devicesWithSameCapabilities.size > 1) {
181
+ val droppedIds = devicesWithSameCapabilities.drop(1).map { it["id"] }
182
+ LogUtils.d(TAG, "Dropping ${devicesWithSameCapabilities.size - 1} duplicate devices with IDs: $droppedIds")
183
+ }
184
+ }
185
+ } else {
186
+ // No default device, so just deduplicate by capabilities
187
+ val capabilityGroups = mutableMapOf<String, MutableList<Map<String, Any>>>()
188
+
189
+ for (device in deviceList) {
190
+ val capabilities = device["capabilities"] as Map<String, Any>
191
+ val capabilityHash = generateCapabilityHash(capabilities)
192
+
193
+ if (!capabilityGroups.containsKey(capabilityHash)) {
194
+ capabilityGroups[capabilityHash] = mutableListOf()
195
+ }
196
+ capabilityGroups[capabilityHash]?.add(device)
197
+ }
198
+
199
+ // Keep one device from each capability group
200
+ capabilityGroups.forEach { (capHash, devicesWithSameCapabilities) ->
201
+ val selectedDevice = devicesWithSameCapabilities.first()
202
+ LogUtils.d(TAG, "Adding device ${selectedDevice["id"]} with unique capabilities: $capHash")
203
+ devices.add(selectedDevice)
204
+
205
+ // Log if we're dropping duplicate devices
206
+ if (devicesWithSameCapabilities.size > 1) {
207
+ val droppedIds = devicesWithSameCapabilities.drop(1).map { it["id"] }
208
+ LogUtils.d(TAG, "Dropping ${devicesWithSameCapabilities.size - 1} duplicate devices with IDs: $droppedIds")
209
+ }
210
+ }
211
+ }
212
+ } else {
213
+ // Only one device with this name, just add it
214
+ devices.add(deviceList.first())
215
+ }
216
+ }
217
+
218
+ } else {
219
+ // Fallback for older Android versions - add at least the default device
220
+ val isHeadsetConnected = audioManager.isWiredHeadsetOn || isBluetoothHeadsetConnected()
221
+
222
+ // Add default device (built-in mic)
223
+ devices.add(mapOf(
224
+ "id" to "0", // Default device ID
225
+ "name" to "Built-in Microphone",
226
+ "type" to DEVICE_TYPE_BUILTIN_MIC,
227
+ "isDefault" to !isHeadsetConnected,
228
+ "capabilities" to getDefaultCapabilities(),
229
+ "isAvailable" to true
230
+ ))
231
+
232
+ // Add headset device if connected
233
+ if (audioManager.isWiredHeadsetOn) {
234
+ devices.add(mapOf(
235
+ "id" to "1",
236
+ "name" to "Wired Headset",
237
+ "type" to DEVICE_TYPE_WIRED_HEADSET,
238
+ "isDefault" to true,
239
+ "capabilities" to getDefaultCapabilities(),
240
+ "isAvailable" to true
241
+ ))
242
+ }
243
+
244
+ // Add Bluetooth device if connected
245
+ if (isBluetoothHeadsetConnected()) {
246
+ devices.add(mapOf(
247
+ "id" to "2",
248
+ "name" to "Bluetooth Headset",
249
+ "type" to DEVICE_TYPE_BLUETOOTH,
250
+ "isDefault" to true,
251
+ "capabilities" to getDefaultCapabilities(),
252
+ "isAvailable" to true
253
+ ))
254
+ }
255
+ }
256
+
257
+ LogUtils.d(TAG, "Found ${devices.size} input devices after deduplication")
258
+ promise.resolve(devices)
259
+ }
260
+
261
+ /**
262
+ * Generate a string hash representing device capabilities for comparison
263
+ */
264
+ private fun generateCapabilityHash(capabilities: Map<String, Any>): String {
265
+ val sampleRates = capabilities["sampleRates"]?.toString() ?: ""
266
+ val channelCounts = capabilities["channelCounts"]?.toString() ?: ""
267
+ val bitDepths = capabilities["bitDepths"]?.toString() ?: ""
268
+ val hasEchoCancellation = capabilities["hasEchoCancellation"]?.toString() ?: "false"
269
+ val hasNoiseSuppression = capabilities["hasNoiseSuppression"]?.toString() ?: "false"
270
+ val hasAutomaticGainControl = capabilities["hasAutomaticGainControl"]?.toString() ?: "false"
271
+
272
+ return "$sampleRates|$channelCounts|$bitDepths|$hasEchoCancellation|$hasNoiseSuppression|$hasAutomaticGainControl"
273
+ }
274
+
275
+ /**
276
+ * Gets the currently selected input device
277
+ */
278
+ fun getCurrentInputDevice(promise: Promise) {
279
+ val device = getCurrentInputDeviceInternal()
280
+ promise.resolve(device)
281
+ }
282
+
283
+ /**
284
+ * Gets the default input device (built-in mic usually)
285
+ */
286
+ suspend fun getDefaultInputDevice(): Map<String, Any>? {
287
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
288
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
289
+ // Find built-in microphone which is the typical default
290
+ val defaultDevice = audioDevices.firstOrNull {
291
+ it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC
292
+ }
293
+
294
+ if (defaultDevice != null) {
295
+ val deviceType = mapDeviceType(defaultDevice)
296
+ val deviceName = getDeviceName(defaultDevice)
297
+
298
+ LogUtils.d(TAG, "Found default device: $deviceName (ID: ${defaultDevice.id}, Type: $deviceType)")
299
+
300
+ return mapOf(
301
+ "id" to defaultDevice.id.toString(),
302
+ "name" to deviceName,
303
+ "type" to deviceType,
304
+ "isDefault" to true,
305
+ "capabilities" to getDeviceCapabilities(defaultDevice),
306
+ "isAvailable" to true
307
+ )
308
+ }
309
+ }
310
+
311
+ // Fallback for older Android or if no built-in mic found
312
+ return mapOf(
313
+ "id" to "0",
314
+ "name" to "Built-in Microphone",
315
+ "type" to DEVICE_TYPE_BUILTIN_MIC,
316
+ "isDefault" to true,
317
+ "capabilities" to getDefaultCapabilities(),
318
+ "isAvailable" to true
319
+ )
320
+ }
321
+
322
+ /**
323
+ * Gets the currently active input device (internal implementation)
324
+ */
325
+ private fun getCurrentInputDeviceInternal(): Map<String, Any>? {
326
+ // On Android, we need to check the current routing
327
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
328
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
329
+
330
+ // Determine current input device based on the communication device
331
+ // or audio routing
332
+ // For API level 31+, we can use getCommunicationDevice() directly
333
+ var currentDevice: AudioDeviceInfo? = null
334
+
335
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
336
+ currentDevice = audioManager.communicationDevice?.takeIf {
337
+ it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER &&
338
+ it.isSource
339
+ }
340
+ }
341
+
342
+ // If no communication device found, check other indicators
343
+ if (currentDevice == null) {
344
+ // Check if we have a Bluetooth SCO device active
345
+ if (audioManager.isBluetoothScoOn) {
346
+ currentDevice = audioDevices.firstOrNull {
347
+ it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
348
+ }
349
+ }
350
+
351
+ // Check if wired headset is connected
352
+ if (currentDevice == null && audioManager.isWiredHeadsetOn) {
353
+ currentDevice = audioDevices.firstOrNull {
354
+ it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET
355
+ }
356
+ }
357
+
358
+ // Default to built-in mic if nothing else is found
359
+ if (currentDevice == null) {
360
+ currentDevice = audioDevices.firstOrNull {
361
+ it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC
362
+ }
363
+ }
364
+ }
365
+
366
+ if (currentDevice != null) {
367
+ val deviceId = currentDevice.id.toString()
368
+ val deviceType = mapDeviceType(currentDevice)
369
+ val deviceName = getDeviceName(currentDevice)
370
+
371
+ LogUtils.d(TAG, "Current input device: $deviceName (ID: $deviceId, type: $deviceType)")
372
+
373
+ return mapOf(
374
+ "id" to deviceId,
375
+ "name" to deviceName,
376
+ "type" to deviceType,
377
+ "isDefault" to (deviceType == DEVICE_TYPE_BUILTIN_MIC),
378
+ "capabilities" to getDeviceCapabilities(currentDevice),
379
+ "isAvailable" to true
380
+ )
381
+ }
382
+ } else {
383
+ // For older Android versions, determine based on flags
384
+ if (isBluetoothHeadsetConnected()) {
385
+ return mapOf(
386
+ "id" to "2",
387
+ "name" to "Bluetooth Headset",
388
+ "type" to DEVICE_TYPE_BLUETOOTH,
389
+ "isDefault" to true,
390
+ "capabilities" to getDefaultCapabilities(),
391
+ "isAvailable" to true
392
+ )
393
+ } else if (audioManager.isWiredHeadsetOn) {
394
+ return mapOf(
395
+ "id" to "1",
396
+ "name" to "Wired Headset",
397
+ "type" to DEVICE_TYPE_WIRED_HEADSET,
398
+ "isDefault" to true,
399
+ "capabilities" to getDefaultCapabilities(),
400
+ "isAvailable" to true
401
+ )
402
+ } else {
403
+ // Default to built-in mic
404
+ return mapOf(
405
+ "id" to "0",
406
+ "name" to "Built-in Microphone",
407
+ "type" to DEVICE_TYPE_BUILTIN_MIC,
408
+ "isDefault" to true,
409
+ "capabilities" to getDefaultCapabilities(),
410
+ "isAvailable" to true
411
+ )
412
+ }
413
+ }
414
+
415
+ return null
416
+ }
417
+
418
+ /**
419
+ * Selects a specific audio input device for recording
420
+ */
421
+ fun selectInputDevice(deviceId: String, promise: Promise) {
422
+ LogUtils.d(TAG, "Selecting input device with ID: $deviceId")
423
+
424
+ // Store the selected device ID for tracking
425
+ lastSelectedDeviceId = deviceId
426
+
427
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
428
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
429
+ val selectedDevice = audioDevices.firstOrNull { it.id.toString() == deviceId }
430
+
431
+ if (selectedDevice == null) {
432
+ LogUtils.e(TAG, "Device not found with ID $deviceId")
433
+ promise.reject("DEVICE_NOT_FOUND", "The selected audio device is not available", null)
434
+ return
435
+ }
436
+
437
+ // Handle device selection based on type
438
+ when (selectedDevice.type) {
439
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
440
+ // For Bluetooth SCO devices, start SCO connection
441
+ if (!audioManager.isBluetoothScoOn) {
442
+ audioManager.startBluetoothSco()
443
+ audioManager.isBluetoothScoOn = true
444
+ }
445
+
446
+ // On Android S (API 31) and above, we can set communication device directly
447
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
448
+ try {
449
+ val success = audioManager.setCommunicationDevice(selectedDevice)
450
+ if (!success) {
451
+ LogUtils.w(TAG, "Failed to set communication device for Bluetooth SCO")
452
+ }
453
+ } catch (e: Exception) {
454
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
455
+ }
456
+ }
457
+ }
458
+
459
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> {
460
+ // For wired headsets, just need to ensure SCO is off
461
+ if (audioManager.isBluetoothScoOn) {
462
+ audioManager.stopBluetoothSco()
463
+ audioManager.isBluetoothScoOn = false
464
+ }
465
+
466
+ // On Android S (API 31) and above, we can set communication device directly
467
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
468
+ try {
469
+ val success = audioManager.setCommunicationDevice(selectedDevice)
470
+ if (!success) {
471
+ LogUtils.w(TAG, "Failed to set communication device for wired headset")
472
+ }
473
+ } catch (e: Exception) {
474
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
475
+ }
476
+ }
477
+ }
478
+
479
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> {
480
+ // For built-in mic, ensure SCO is off
481
+ if (audioManager.isBluetoothScoOn) {
482
+ audioManager.stopBluetoothSco()
483
+ audioManager.isBluetoothScoOn = false
484
+ }
485
+
486
+ // On Android S (API 31) and above, we can set communication device directly
487
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
488
+ try {
489
+ val success = audioManager.setCommunicationDevice(selectedDevice)
490
+ if (!success) {
491
+ LogUtils.w(TAG, "Failed to set communication device for built-in mic")
492
+ }
493
+ } catch (e: Exception) {
494
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
495
+ }
496
+ }
497
+ }
498
+
499
+ // Handle other device types as needed
500
+ else -> {
501
+ // For other device types, ensure SCO is off
502
+ if (audioManager.isBluetoothScoOn) {
503
+ audioManager.stopBluetoothSco()
504
+ audioManager.isBluetoothScoOn = false
505
+ }
506
+
507
+ // On Android S (API 31) and above, we can set communication device directly
508
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
509
+ try {
510
+ val success = audioManager.setCommunicationDevice(selectedDevice)
511
+ if (!success) {
512
+ LogUtils.w(TAG, "Failed to set communication device for device type: ${selectedDevice.type}")
513
+ }
514
+ } catch (e: Exception) {
515
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ LogUtils.d(TAG, "Successfully selected device: ${getDeviceName(selectedDevice)}")
522
+ promise.resolve(true)
523
+ } else {
524
+ // For older Android versions, handle based on device ID
525
+ when (deviceId) {
526
+ "0" -> { // Built-in mic
527
+ if (audioManager.isBluetoothScoOn) {
528
+ audioManager.stopBluetoothSco()
529
+ audioManager.isBluetoothScoOn = false
530
+ }
531
+ LogUtils.d(TAG, "Selected built-in microphone")
532
+ promise.resolve(true)
533
+ }
534
+ "1" -> { // Wired headset
535
+ if (audioManager.isWiredHeadsetOn) {
536
+ if (audioManager.isBluetoothScoOn) {
537
+ audioManager.stopBluetoothSco()
538
+ audioManager.isBluetoothScoOn = false
539
+ }
540
+ LogUtils.d(TAG, "Selected wired headset")
541
+ promise.resolve(true)
542
+ } else {
543
+ LogUtils.e(TAG, "Wired headset is not connected")
544
+ promise.reject("DEVICE_NOT_AVAILABLE", "Wired headset is not connected", null)
545
+ }
546
+ }
547
+ "2" -> { // Bluetooth headset
548
+ if (isBluetoothHeadsetConnected()) {
549
+ audioManager.startBluetoothSco()
550
+ audioManager.isBluetoothScoOn = true
551
+ LogUtils.d(TAG, "Selected Bluetooth headset")
552
+ promise.resolve(true)
553
+ } else {
554
+ LogUtils.e(TAG, "Bluetooth headset is not connected")
555
+ promise.reject("DEVICE_NOT_AVAILABLE", "Bluetooth headset is not connected", null)
556
+ }
557
+ }
558
+ else -> {
559
+ LogUtils.e(TAG, "Unknown device ID: $deviceId")
560
+ promise.reject("DEVICE_NOT_FOUND", "The selected audio device is not available", null)
561
+ }
562
+ }
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Selects a specific audio input device asynchronously (for internal use)
568
+ */
569
+ suspend fun selectDevice(deviceId: String): Boolean {
570
+ LogUtils.d(TAG, "Asynchronously selecting device with ID: $deviceId")
571
+ lastSelectedDeviceId = deviceId
572
+
573
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
574
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
575
+ val selectedDevice = audioDevices.firstOrNull { it.id.toString() == deviceId }
576
+
577
+ if (selectedDevice == null) {
578
+ LogUtils.e(TAG, "Device not found with ID $deviceId for async selection")
579
+ return false
580
+ }
581
+
582
+ // Handle device selection based on type
583
+ when (selectedDevice.type) {
584
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
585
+ // For Bluetooth SCO devices, start SCO connection
586
+ if (!audioManager.isBluetoothScoOn) {
587
+ audioManager.startBluetoothSco()
588
+ audioManager.isBluetoothScoOn = true
589
+ }
590
+
591
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
592
+ try {
593
+ val success = audioManager.setCommunicationDevice(selectedDevice)
594
+ LogUtils.d(TAG, "Setting communication device for Bluetooth SCO: $success")
595
+ // Return true even if setCommunicationDevice fails
596
+ return true
597
+ } catch (e: Exception) {
598
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
599
+ // Return true anyway to allow fallback to continue
600
+ return true
601
+ }
602
+ }
603
+ return true
604
+ }
605
+
606
+ else -> {
607
+ // For other device types
608
+ if (audioManager.isBluetoothScoOn) {
609
+ audioManager.stopBluetoothSco()
610
+ audioManager.isBluetoothScoOn = false
611
+ }
612
+
613
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
614
+ try {
615
+ val success = audioManager.setCommunicationDevice(selectedDevice)
616
+ LogUtils.d(TAG, "Setting communication device for other device type: $success")
617
+ // Return true even if setCommunicationDevice fails
618
+ return true
619
+ } catch (e: Exception) {
620
+ LogUtils.e(TAG, "Error setting communication device: ${e.message}")
621
+ // Return true anyway to allow fallback to continue
622
+ return true
623
+ }
624
+ }
625
+ return true
626
+ }
627
+ }
628
+ } else {
629
+ // For older Android versions
630
+ when (deviceId) {
631
+ "0" -> { // Built-in mic
632
+ if (audioManager.isBluetoothScoOn) {
633
+ audioManager.stopBluetoothSco()
634
+ audioManager.isBluetoothScoOn = false
635
+ }
636
+ return true
637
+ }
638
+ "1" -> { // Wired headset
639
+ if (audioManager.isWiredHeadsetOn) {
640
+ if (audioManager.isBluetoothScoOn) {
641
+ audioManager.stopBluetoothSco()
642
+ audioManager.isBluetoothScoOn = false
643
+ }
644
+ return true
645
+ }
646
+ return false
647
+ }
648
+ "2" -> { // Bluetooth headset
649
+ if (isBluetoothHeadsetConnected()) {
650
+ audioManager.startBluetoothSco()
651
+ audioManager.isBluetoothScoOn = true
652
+ return true
653
+ }
654
+ return false
655
+ }
656
+ else -> return false
657
+ }
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Resets to the default audio input device (usually built-in mic)
663
+ */
664
+ fun resetToDefaultDevice(callback: (Boolean, Exception?) -> Unit) {
665
+ LogUtils.d(TAG, "Resetting to default input device")
666
+
667
+ try {
668
+ // Stop Bluetooth SCO if active
669
+ if (audioManager.isBluetoothScoOn) {
670
+ audioManager.stopBluetoothSco()
671
+ audioManager.isBluetoothScoOn = false
672
+ }
673
+
674
+ // For Android S and above, reset communication device
675
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
676
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
677
+ val builtInMic = audioDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC }
678
+
679
+ if (builtInMic != null) {
680
+ try {
681
+ val success = audioManager.setCommunicationDevice(builtInMic)
682
+ if (!success) {
683
+ LogUtils.w(TAG, "Failed to reset to default device")
684
+ }
685
+ } catch (e: Exception) {
686
+ LogUtils.e(TAG, "Error resetting to default device: ${e.message}")
687
+ callback(false, e)
688
+ return
689
+ }
690
+ }
691
+ }
692
+
693
+ // Clear last selected device
694
+ lastSelectedDeviceId = null
695
+
696
+ // Get the device after reset
697
+ val currentDevice = getCurrentInputDeviceInternal()
698
+ if (currentDevice != null) {
699
+ LogUtils.d(TAG, "Reset to default device: ${currentDevice["name"]}")
700
+ } else {
701
+ LogUtils.d(TAG, "No device detected after reset")
702
+ }
703
+
704
+ callback(true, null)
705
+ } catch (e: Exception) {
706
+ LogUtils.e(TAG, "Failed to reset to default device: ${e.message}")
707
+ callback(false, e)
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Force refreshes the audio session to update device list
713
+ */
714
+ fun forceRefreshAudioDevices(): Boolean {
715
+ LogUtils.d(TAG, "Forcing refresh of audio devices")
716
+
717
+ // Not much to do on Android since devices are enumerated on-demand
718
+ // but we can check if SCO is consistent with device state
719
+ try {
720
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
721
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
722
+ val hasBluetoothSco = audioDevices.any { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
723
+
724
+ // If we have a Bluetooth SCO device but SCO isn't started, start it
725
+ if (hasBluetoothSco && !audioManager.isBluetoothScoOn && lastSelectedDeviceId != null) {
726
+ val device = audioDevices.firstOrNull { it.id.toString() == lastSelectedDeviceId }
727
+ if (device?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
728
+ audioManager.startBluetoothSco()
729
+ audioManager.isBluetoothScoOn = true
730
+ }
731
+ }
732
+ }
733
+
734
+ return true
735
+ } catch (e: Exception) {
736
+ LogUtils.e(TAG, "Error refreshing audio devices: ${e.message}")
737
+ return false
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Maps Android's AudioDeviceInfo type to our standardized device type
743
+ */
744
+ @RequiresApi(Build.VERSION_CODES.M)
745
+ private fun mapDeviceType(device: AudioDeviceInfo): String {
746
+ return when (device.type) {
747
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> DEVICE_TYPE_BUILTIN_MIC
748
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> DEVICE_TYPE_BLUETOOTH
749
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> DEVICE_TYPE_WIRED_HEADSET
750
+ AudioDeviceInfo.TYPE_USB_DEVICE,
751
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
752
+ AudioDeviceInfo.TYPE_USB_HEADSET -> DEVICE_TYPE_USB
753
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> DEVICE_TYPE_WIRED_HEADPHONES
754
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> DEVICE_TYPE_SPEAKER
755
+ else -> DEVICE_TYPE_UNKNOWN
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Gets a human-readable name for the device with detailed capability information
761
+ */
762
+ @RequiresApi(Build.VERSION_CODES.M)
763
+ private fun getDeviceName(device: AudioDeviceInfo): String {
764
+ // Get product name if available
765
+ val productName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
766
+ device.productName?.toString()
767
+ } else null
768
+
769
+ // Get base name
770
+ val baseName = if (productName.isNullOrBlank()) {
771
+ when (device.type) {
772
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Built-in Microphone"
773
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth Headset"
774
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
775
+ AudioDeviceInfo.TYPE_USB_DEVICE -> "USB Audio Device"
776
+ AudioDeviceInfo.TYPE_USB_ACCESSORY -> "USB Audio Accessory"
777
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
778
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Wired Headphones"
779
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Built-in Speaker"
780
+ else -> "Audio Device"
781
+ }
782
+ } else {
783
+ productName
784
+ }
785
+
786
+ // Get capability details for naming
787
+ val maxSampleRate = device.sampleRates?.maxOrNull() ?: 0
788
+ val channelCount = device.channelCounts?.maxOrNull() ?: 1
789
+
790
+ // Create a descriptive suffix based on detailed capabilities
791
+ val typeDescription = if (device.type == AudioDeviceInfo.TYPE_UNKNOWN) {
792
+ when {
793
+ maxSampleRate >= 44100 && channelCount > 1 -> "External"
794
+ maxSampleRate >= 44100 -> "Line-in"
795
+ else -> "Unknown Type"
796
+ }
797
+ } else {
798
+ when (device.type) {
799
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Internal"
800
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
801
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired"
802
+ AudioDeviceInfo.TYPE_USB_DEVICE,
803
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
804
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB"
805
+ else -> ""
806
+ }
807
+ }
808
+
809
+ // Create full capability description
810
+ val capabilityDesc = when {
811
+ // Stereo high sample rate
812
+ maxSampleRate >= 48000 && channelCount >= 2 -> {
813
+ if (device.type == AudioDeviceInfo.TYPE_UNKNOWN) {
814
+ "HD Audio $typeDescription #${device.id}"
815
+ } else {
816
+ "HD Audio $typeDescription"
817
+ }
818
+ }
819
+
820
+ // Stereo
821
+ channelCount > 1 -> {
822
+ if (maxSampleRate >= 44100) {
823
+ "Stereo $typeDescription ${maxSampleRate/1000}kHz"
824
+ } else {
825
+ "Stereo $typeDescription"
826
+ }
827
+ }
828
+
829
+ // High sample rate mono
830
+ maxSampleRate >= 44100 -> "High Quality $typeDescription"
831
+
832
+ // Basic
833
+ else -> "$typeDescription #${device.id}"
834
+ }
835
+
836
+ return "$baseName ($capabilityDesc)".trim()
837
+ }
838
+
839
+ /**
840
+ * Gets just the base device name without capability information
841
+ */
842
+ @RequiresApi(Build.VERSION_CODES.M)
843
+ private fun getBaseDeviceName(device: AudioDeviceInfo): String {
844
+ val productName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
845
+ device.productName?.toString()
846
+ } else null
847
+
848
+ return if (productName.isNullOrBlank()) {
849
+ when (device.type) {
850
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Built-in Microphone"
851
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth Headset"
852
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
853
+ AudioDeviceInfo.TYPE_USB_DEVICE -> "USB Audio Device"
854
+ AudioDeviceInfo.TYPE_USB_ACCESSORY -> "USB Audio Accessory"
855
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
856
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Wired Headphones"
857
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Built-in Speaker"
858
+ else -> "Audio Device"
859
+ }
860
+ } else {
861
+ productName
862
+ }
863
+ }
864
+
865
+ /**
866
+ * Gets capabilities for an audio input device
867
+ * Enhanced to provide comprehensive capabilities even if not reported by the API
868
+ */
869
+ @RequiresApi(Build.VERSION_CODES.M)
870
+ private fun getDeviceCapabilities(device: AudioDeviceInfo): Map<String, Any> {
871
+ // Get reported sample rates or use common ones if not available
872
+ val reportedSampleRates = device.sampleRates?.toList()
873
+
874
+ // Use reported sample rates but ensure common ones are included as most devices
875
+ // actually support these even if not explicitly reported
876
+ val sampleRates = if (reportedSampleRates.isNullOrEmpty()) {
877
+ COMMON_SAMPLE_RATES
878
+ } else {
879
+ // Create a set of all sample rates - both reported and common ones
880
+ val combinedRates = reportedSampleRates.toMutableSet()
881
+
882
+ // For built-in and common devices, add standard rates that are typically supported
883
+ if (device.type == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
884
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
885
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
886
+ combinedRates.addAll(COMMON_SAMPLE_RATES)
887
+ }
888
+
889
+ // Convert back to list and sort
890
+ combinedRates.toList().sorted()
891
+ }
892
+
893
+ // Get reported channel counts or use common ones
894
+ val reportedChannelCounts = device.channelCounts?.toList()
895
+
896
+ // Ensure mono and stereo are included as they're widely supported
897
+ val channelCounts = if (reportedChannelCounts.isNullOrEmpty()) {
898
+ COMMON_CHANNEL_COUNTS
899
+ } else {
900
+ val combinedCounts = reportedChannelCounts.toMutableSet()
901
+
902
+ // Most devices support at least mono recording
903
+ if (device.type == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
904
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
905
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
906
+ combinedCounts.addAll(COMMON_CHANNEL_COUNTS)
907
+ }
908
+
909
+ combinedCounts.toList().sorted()
910
+ }
911
+
912
+ // Test if common configurations are actually supported
913
+ val verifiedSampleRates = verifyAudioConfigurations(device.id, channelCounts.firstOrNull() ?: 1, sampleRates)
914
+
915
+ // Android doesn't provide bit depth info, so we use common values
916
+ val bitDepths = listOf(16, 24)
917
+
918
+ return mapOf(
919
+ "sampleRates" to verifiedSampleRates,
920
+ "channelCounts" to channelCounts,
921
+ "bitDepths" to bitDepths,
922
+ "hasEchoCancellation" to true, // Android generally has AEC
923
+ "hasNoiseSuppression" to true, // Android generally has noise suppression
924
+ "hasAutomaticGainControl" to true // Android generally has AGC
925
+ )
926
+ }
927
+
928
+ /**
929
+ * Verify which sample rates are actually supported by attempting to create an AudioRecord
930
+ * This helps catch cases where the API reports capabilities incorrectly
931
+ */
932
+ private fun verifyAudioConfigurations(deviceId: Int, channels: Int, sampleRates: List<Int>): List<Int> {
933
+ if (!permissionGranted()) {
934
+ return sampleRates // Can't verify without permission, return as-is
935
+ }
936
+
937
+ val supportedSampleRates = mutableListOf<Int>()
938
+ val channelConfig = if (channels == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO
939
+
940
+ // Always include these standard rates that are almost universally supported
941
+ val standardRates = listOf(16000, 44100, 48000)
942
+
943
+ LogUtils.d(TAG, "Verifying audio configurations for device ${deviceId} with ${sampleRates.size} sample rates")
944
+
945
+ for (sampleRate in sampleRates.distinct()) {
946
+ try {
947
+ val minBufferSize = AudioRecord.getMinBufferSize(
948
+ sampleRate,
949
+ channelConfig,
950
+ AudioFormat.ENCODING_PCM_16BIT
951
+ )
952
+
953
+ // Skip if invalid buffer size
954
+ if (minBufferSize <= 0) {
955
+ // But keep standard rates anyway as they usually work
956
+ if (sampleRate in standardRates) {
957
+ supportedSampleRates.add(sampleRate)
958
+ LogUtils.d(TAG, "⚠️ Adding standard rate ${sampleRate}Hz despite test failure")
959
+ }
960
+ continue
961
+ }
962
+
963
+ // Try to create an AudioRecord with this configuration
964
+ var audioRecord: AudioRecord? = null
965
+ try {
966
+ audioRecord = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
967
+ // Use the specific device if on newer Android
968
+ AudioRecord.Builder()
969
+ .setAudioSource(MediaRecorder.AudioSource.MIC)
970
+ .setAudioFormat(
971
+ AudioFormat.Builder()
972
+ .setSampleRate(sampleRate)
973
+ .setChannelMask(channelConfig)
974
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
975
+ .build()
976
+ )
977
+ .setBufferSizeInBytes(minBufferSize)
978
+ .build()
979
+ } else {
980
+ AudioRecord(
981
+ MediaRecorder.AudioSource.MIC,
982
+ sampleRate,
983
+ channelConfig,
984
+ AudioFormat.ENCODING_PCM_16BIT,
985
+ minBufferSize
986
+ )
987
+ }
988
+
989
+ if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
990
+ supportedSampleRates.add(sampleRate)
991
+ LogUtils.d(TAG, "✅ Sample rate ${sampleRate}Hz verified as supported")
992
+ } else if (sampleRate in standardRates) {
993
+ // Include standard rates even if initialization failed
994
+ // as they typically work during actual recording
995
+ supportedSampleRates.add(sampleRate)
996
+ LogUtils.d(TAG, "⚠️ Adding standard rate ${sampleRate}Hz despite test failure")
997
+ }
998
+ } finally {
999
+ audioRecord?.release()
1000
+ }
1001
+ } catch (e: Exception) {
1002
+ LogUtils.d(TAG, "Sample rate $sampleRate not supported: ${e.message}")
1003
+
1004
+ // Include standard rates even if test failed
1005
+ if (sampleRate in standardRates) {
1006
+ supportedSampleRates.add(sampleRate)
1007
+ LogUtils.d(TAG, "⚠️ Adding standard rate ${sampleRate}Hz despite test failure")
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ // Ensure we have at least the standard rates
1013
+ if (supportedSampleRates.isEmpty()) {
1014
+ supportedSampleRates.addAll(standardRates)
1015
+ }
1016
+
1017
+ return supportedSampleRates.sorted()
1018
+ }
1019
+
1020
+ /**
1021
+ * Check if recording permission is granted
1022
+ */
1023
+ private fun permissionGranted(): Boolean {
1024
+ return context.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) ==
1025
+ android.content.pm.PackageManager.PERMISSION_GRANTED
1026
+ }
1027
+
1028
+ /**
1029
+ * Default capabilities for older Android versions
1030
+ */
1031
+ private fun getDefaultCapabilities(): Map<String, Any> {
1032
+ return mapOf(
1033
+ "sampleRates" to COMMON_SAMPLE_RATES,
1034
+ "channelCounts" to COMMON_CHANNEL_COUNTS,
1035
+ "bitDepths" to listOf(16, 24),
1036
+ "hasEchoCancellation" to true,
1037
+ "hasNoiseSuppression" to true,
1038
+ "hasAutomaticGainControl" to true
1039
+ )
1040
+ }
1041
+
1042
+ /**
1043
+ * Checks if a Bluetooth headset is connected
1044
+ */
1045
+ private fun isBluetoothHeadsetConnected(): Boolean {
1046
+ try {
1047
+ // BLUETOOTH_CONNECT permission is required on API 31+ for Bluetooth API access.
1048
+ // Without it, BluetoothAdapter calls throw SecurityException.
1049
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1050
+ if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT)
1051
+ != PackageManager.PERMISSION_GRANTED) {
1052
+ return false
1053
+ }
1054
+ }
1055
+
1056
+ val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return false
1057
+ if (!bluetoothAdapter.isEnabled) {
1058
+ return false
1059
+ }
1060
+
1061
+ // For newer Android versions, check communication device directly
1062
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1063
+ val commsDevice = audioManager.communicationDevice
1064
+ if (commsDevice != null && commsDevice.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
1065
+ return true
1066
+ }
1067
+ }
1068
+
1069
+ // Check if Bluetooth SCO is enabled (active call)
1070
+ if (audioManager.isBluetoothScoOn) {
1071
+ return true
1072
+ }
1073
+
1074
+ // Check legacy API
1075
+ val bluetoothProfile = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1076
+ bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)
1077
+ } else {
1078
+ @Suppress("DEPRECATION")
1079
+ bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)
1080
+ }
1081
+
1082
+ return bluetoothProfile == BluetoothProfile.STATE_CONNECTED
1083
+ } catch (e: Exception) {
1084
+ LogUtils.e(CLASS_NAME, "Error checking Bluetooth headset connection: ${e.message}", e)
1085
+ return false
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Checks if a device is still available
1091
+ */
1092
+ fun isDeviceAvailable(deviceId: String): Boolean {
1093
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1094
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1095
+ return audioDevices.any { it.id.toString() == deviceId }
1096
+ } else {
1097
+ // For older Android versions, check based on device ID
1098
+ return when (deviceId) {
1099
+ "0" -> true // Built-in mic is always available
1100
+ "1" -> audioManager.isWiredHeadsetOn // Wired headset
1101
+ "2" -> isBluetoothHeadsetConnected() // Bluetooth headset
1102
+ else -> false
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * Starts monitoring device connection/disconnection events
1109
+ */
1110
+ private fun startMonitoringDeviceChanges() {
1111
+ if (deviceReceiver != null) {
1112
+ return // Already monitoring
1113
+ }
1114
+
1115
+ try {
1116
+ val filter = IntentFilter().apply {
1117
+ // Wired headset events
1118
+ addAction(AudioManager.ACTION_HEADSET_PLUG)
1119
+
1120
+ // Bluetooth device events
1121
+ addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
1122
+ addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
1123
+
1124
+ // Audio routing change events - critical for detecting device disconnection during recording
1125
+ addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
1126
+
1127
+ // Bluetooth connection state changes
1128
+ addAction(ACTION_CONNECTION_STATE_CHANGED)
1129
+
1130
+ // USB device events - to detect USB audio devices
1131
+ addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
1132
+ addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
1133
+
1134
+ // For Android 8+ we need to look for USB device permission events
1135
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1136
+ addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)
1137
+ addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED)
1138
+ }
1139
+
1140
+ // Bluetooth SCO state changes - to detect when microphone becomes available
1141
+ addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
1142
+ }
1143
+
1144
+ deviceReceiver = object : BroadcastReceiver() {
1145
+ override fun onReceive(context: Context, intent: Intent) {
1146
+ val action = intent.action
1147
+ LogUtils.d(CLASS_NAME, "Device connectivity changed: $action")
1148
+
1149
+ // Log current audio state for debugging
1150
+ logAudioState()
1151
+
1152
+ // Determine which device was affected
1153
+ var deviceId: String? = null
1154
+ var deviceName: String? = null
1155
+ var deviceType: String? = null
1156
+
1157
+ when (action) {
1158
+ AudioManager.ACTION_HEADSET_PLUG -> {
1159
+ val state = intent.getIntExtra("state", 0)
1160
+ val microphone = intent.getIntExtra("microphone", 0)
1161
+ val name = intent.getStringExtra("name") ?: "Wired Headset"
1162
+
1163
+ if (state == 0) { // Unplugged
1164
+ // Legacy device ID for pre-M Android
1165
+ deviceId = "1"
1166
+ deviceName = name
1167
+ deviceType = DEVICE_TYPE_WIRED_HEADSET
1168
+
1169
+ LogUtils.d(CLASS_NAME, "Wired headset unplugged: $name")
1170
+
1171
+ // For M+ we can get the actual device ID
1172
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1173
+ // Look up the actual device ID we were using
1174
+ if (lastSelectedDeviceId != null) {
1175
+ val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1176
+ if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
1177
+ lastDeviceInfo?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
1178
+ deviceId = lastSelectedDeviceId
1179
+ }
1180
+ }
1181
+ }
1182
+ } else if (state == 1) { // Plugged in
1183
+ LogUtils.d(CLASS_NAME, "Wired headset connected: $name")
1184
+
1185
+ // For M+ find the actual new device
1186
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1187
+ // Find the newly connected wired device
1188
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1189
+ val wiredDevice = audioDevices.firstOrNull {
1190
+ (it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
1191
+ it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) &&
1192
+ getDeviceName(it).contains(name, ignoreCase = true)
1193
+ }
1194
+
1195
+ if (wiredDevice != null) {
1196
+ val connectedDeviceId = wiredDevice.id.toString()
1197
+ LogUtils.d(CLASS_NAME, "Found connected wired device: $name (ID: $connectedDeviceId)")
1198
+ handleDeviceConnection(connectedDeviceId)
1199
+ }
1200
+ } else {
1201
+ // Legacy handling for older Android
1202
+ handleDeviceConnection("1")
1203
+ }
1204
+ }
1205
+ }
1206
+
1207
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
1208
+ LogUtils.d(CLASS_NAME, "Audio becoming noisy - potential device disconnect")
1209
+
1210
+ // This could be any type of device disconnect, so we need to check
1211
+ // what device was actually removed by comparing current devices with our last device
1212
+ if (lastSelectedDeviceId != null) {
1213
+ // First check if the device is simply unavailable
1214
+ if (!isDeviceAvailable(lastSelectedDeviceId!!)) {
1215
+ deviceId = lastSelectedDeviceId
1216
+ LogUtils.d(CLASS_NAME, "Detected device disconnection via AUDIO_BECOMING_NOISY: $deviceId")
1217
+ }
1218
+ // If device seems available but routing changed, also consider it disconnected
1219
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1220
+ val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1221
+
1222
+ // Get current routing
1223
+ val currentDevice = getCurrentInputDeviceInternal()
1224
+
1225
+ // If current input device is different from the last selected, consider it disconnected
1226
+ if (lastDeviceInfo != null && currentDevice != null &&
1227
+ currentDevice["id"] != lastSelectedDeviceId) {
1228
+ deviceId = lastSelectedDeviceId
1229
+ LogUtils.d(CLASS_NAME, "Routing changed from ${lastDeviceInfo.id} to ${currentDevice["id"]}")
1230
+ }
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
1236
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1237
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1238
+ } else {
1239
+ @Suppress("DEPRECATION")
1240
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1241
+ }
1242
+
1243
+ if (device != null) {
1244
+ LogUtils.d(CLASS_NAME, "Bluetooth device connected: ${device.name}")
1245
+
1246
+ // For M+ find the actual new device
1247
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1248
+ val actualDevice = findBluetoothDevice(device)
1249
+ if (actualDevice != null) {
1250
+ val connectedDeviceId = actualDevice.id.toString()
1251
+ LogUtils.d(CLASS_NAME, "Found connected Bluetooth device: ${device.name} (ID: $connectedDeviceId)")
1252
+ handleDeviceConnection(connectedDeviceId)
1253
+ } else {
1254
+ LogUtils.d(CLASS_NAME, "Bluetooth device ${device.name} connected but not found in audio device list - attempting to activate SCO")
1255
+
1256
+ // Try to activate Bluetooth SCO to make microphone available
1257
+ if (!audioManager.isBluetoothScoOn) {
1258
+ LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
1259
+ audioManager.startBluetoothSco()
1260
+
1261
+ // Give SCO time to activate, then check again
1262
+ coroutineScope.launch {
1263
+ delay(2000) // Wait 2 seconds
1264
+
1265
+ val scoDevice = findBluetoothDevice(device)
1266
+ if (scoDevice != null) {
1267
+ val activatedDeviceId = scoDevice.id.toString()
1268
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
1269
+ handleDeviceConnection(activatedDeviceId)
1270
+ } else {
1271
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
1272
+ // Send generic connection event anyway
1273
+ handleDeviceConnection("bluetooth:${device.address}")
1274
+ }
1275
+ }
1276
+ } else {
1277
+ // SCO already on, send generic event
1278
+ handleDeviceConnection("bluetooth:${device.address}")
1279
+ }
1280
+ }
1281
+ } else {
1282
+ // Legacy handling for older Android
1283
+ handleDeviceConnection("2")
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
1289
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1290
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1291
+ } else {
1292
+ @Suppress("DEPRECATION")
1293
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1294
+ }
1295
+
1296
+ if (device != null) {
1297
+ deviceId = "2" // Legacy ID for bluetooth headset
1298
+ deviceName = device.name
1299
+ deviceType = DEVICE_TYPE_BLUETOOTH
1300
+
1301
+ // For M+ get the actual device ID
1302
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1303
+ val actualDevice = findBluetoothDevice(device)
1304
+ if (actualDevice != null) {
1305
+ deviceId = actualDevice.id.toString()
1306
+ }
1307
+ }
1308
+
1309
+ LogUtils.d(CLASS_NAME, "Bluetooth device disconnected: ${device.name}, using ID: $deviceId")
1310
+ }
1311
+ }
1312
+
1313
+ ACTION_CONNECTION_STATE_CHANGED -> {
1314
+ val state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)
1315
+ logAudioState()
1316
+
1317
+ when (state) {
1318
+ BluetoothAdapter.STATE_CONNECTED -> {
1319
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1320
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1321
+ } else {
1322
+ @Suppress("DEPRECATION")
1323
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1324
+ }
1325
+
1326
+ if (device != null) {
1327
+ LogUtils.d(CLASS_NAME, "Bluetooth profile connected: ${device.name}")
1328
+
1329
+ // For M+ find the actual new device
1330
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1331
+ val actualDevice = findBluetoothDevice(device)
1332
+ if (actualDevice != null) {
1333
+ val connectedDeviceId = actualDevice.id.toString()
1334
+ LogUtils.d(CLASS_NAME, "Found connected Bluetooth profile device: ${device.name} (ID: $connectedDeviceId)")
1335
+ handleDeviceConnection(connectedDeviceId)
1336
+ } else {
1337
+ LogUtils.d(CLASS_NAME, "Bluetooth profile ${device.name} connected but not found in audio device list - attempting to activate SCO")
1338
+
1339
+ // Try to activate Bluetooth SCO to make microphone available
1340
+ if (!audioManager.isBluetoothScoOn) {
1341
+ LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
1342
+ audioManager.startBluetoothSco()
1343
+
1344
+ // Give SCO time to activate, then check again
1345
+ coroutineScope.launch {
1346
+ delay(2000) // Wait 2 seconds
1347
+
1348
+ val scoDevice = findBluetoothDevice(device)
1349
+ if (scoDevice != null) {
1350
+ val activatedDeviceId = scoDevice.id.toString()
1351
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
1352
+ handleDeviceConnection(activatedDeviceId)
1353
+ } else {
1354
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
1355
+ // Send generic connection event anyway
1356
+ handleDeviceConnection("bluetooth:${device.address}")
1357
+ }
1358
+ }
1359
+ } else {
1360
+ // SCO already on, send generic event
1361
+ handleDeviceConnection("bluetooth:${device.address}")
1362
+ }
1363
+ }
1364
+ } else {
1365
+ // Legacy handling for older Android
1366
+ handleDeviceConnection("2")
1367
+ }
1368
+ }
1369
+ }
1370
+
1371
+ BluetoothAdapter.STATE_DISCONNECTED -> {
1372
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1373
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1374
+ } else {
1375
+ @Suppress("DEPRECATION")
1376
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1377
+ }
1378
+
1379
+ if (device != null) {
1380
+ deviceId = "2" // Legacy ID for bluetooth
1381
+ deviceName = device.name
1382
+ deviceType = DEVICE_TYPE_BLUETOOTH
1383
+
1384
+ // For M+ get the actual ID
1385
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1386
+ val actualDevice = findBluetoothDevice(device)
1387
+ if (actualDevice != null) {
1388
+ deviceId = actualDevice.id.toString()
1389
+ }
1390
+ }
1391
+
1392
+ LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected: ${device.name}, using ID: $deviceId")
1393
+ }
1394
+ // No device info, check if our last device was bluetooth
1395
+ else if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1396
+ val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1397
+
1398
+ if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
1399
+ deviceId = lastSelectedDeviceId
1400
+ LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected, using last selected device ID: $deviceId")
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ }
1406
+
1407
+ UsbManager.ACTION_USB_DEVICE_ATTACHED, UsbManager.ACTION_USB_ACCESSORY_ATTACHED -> {
1408
+ LogUtils.d(CLASS_NAME, "USB device attached")
1409
+
1410
+ // For M+ find newly connected USB audio devices
1411
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1412
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1413
+ val usbDevice = audioDevices.firstOrNull {
1414
+ it.type == AudioDeviceInfo.TYPE_USB_DEVICE ||
1415
+ it.type == AudioDeviceInfo.TYPE_USB_HEADSET ||
1416
+ it.type == AudioDeviceInfo.TYPE_USB_ACCESSORY
1417
+ }
1418
+
1419
+ if (usbDevice != null) {
1420
+ val connectedDeviceId = usbDevice.id.toString()
1421
+ val deviceName = getDeviceName(usbDevice)
1422
+ LogUtils.d(CLASS_NAME, "Found connected USB audio device: $deviceName (ID: $connectedDeviceId)")
1423
+ handleDeviceConnection(connectedDeviceId)
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ UsbManager.ACTION_USB_DEVICE_DETACHED, UsbManager.ACTION_USB_ACCESSORY_DETACHED -> {
1429
+ LogUtils.d(CLASS_NAME, "USB device detached")
1430
+
1431
+ // Check if our last selected device was USB
1432
+ if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1433
+ val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1434
+
1435
+ if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_USB_DEVICE ||
1436
+ lastDeviceInfo?.type == AudioDeviceInfo.TYPE_USB_HEADSET ||
1437
+ lastDeviceInfo?.type == AudioDeviceInfo.TYPE_USB_ACCESSORY) {
1438
+ deviceId = lastSelectedDeviceId
1439
+ deviceType = DEVICE_TYPE_USB
1440
+ deviceName = getDeviceName(lastDeviceInfo)
1441
+ LogUtils.d(CLASS_NAME, "USB audio device disconnected: $deviceName")
1442
+ }
1443
+ }
1444
+ }
1445
+
1446
+ AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
1447
+ val scoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
1448
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO state changed: $scoState")
1449
+
1450
+ when (scoState) {
1451
+ AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
1452
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO connected - microphone available")
1453
+
1454
+ // Check if any new Bluetooth SCO devices appeared
1455
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1456
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1457
+ val bluetoothScoDevices = audioDevices.filter {
1458
+ it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
1459
+ }
1460
+
1461
+ if (bluetoothScoDevices.isNotEmpty()) {
1462
+ // Found Bluetooth SCO device(s), notify about the first one
1463
+ val scoDevice = bluetoothScoDevices.first()
1464
+ val scoDeviceId = scoDevice.id.toString()
1465
+ val scoDeviceName = getDeviceName(scoDevice)
1466
+ LogUtils.d(CLASS_NAME, "Found Bluetooth SCO device: $scoDeviceName (ID: $scoDeviceId)")
1467
+ handleDeviceConnection(scoDeviceId)
1468
+ }
1469
+ }
1470
+ }
1471
+
1472
+ AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
1473
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO disconnected - microphone no longer available")
1474
+ // Note: Device disconnection will be handled by other broadcasts
1475
+ }
1476
+
1477
+ AudioManager.SCO_AUDIO_STATE_CONNECTING -> {
1478
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO connecting...")
1479
+ }
1480
+
1481
+ else -> {
1482
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO state: $scoState")
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+
1488
+ // Handle any device disconnection - send events to React Native for device list updates
1489
+ if (deviceId != null) {
1490
+ LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId (selected: ${deviceId == lastSelectedDeviceId})")
1491
+ if (deviceName != null) {
1492
+ LogUtils.d(CLASS_NAME, "Device name: $deviceName, type: $deviceType")
1493
+ }
1494
+
1495
+ // Log the disconnection for debugging
1496
+ logDeviceDisconnection(deviceId, action ?: "unknown")
1497
+
1498
+ // Send device disconnection event to React Native (for UI updates)
1499
+ handleDeviceDisconnectionEvent(deviceId)
1500
+
1501
+ // If this was the currently selected device, also notify delegate for recording interruption
1502
+ if (deviceId == lastSelectedDeviceId) {
1503
+ LogUtils.d(CLASS_NAME, "Currently selected device disconnected - notifying delegate: $deviceId")
1504
+ // Launch a coroutine to call the suspend function
1505
+ coroutineScope.launch {
1506
+ try {
1507
+ handleDeviceDisconnection(deviceId)
1508
+ } catch (e: Exception) {
1509
+ LogUtils.e(CLASS_NAME, "Error handling device disconnection: ${e.message}", e)
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+
1515
+ // Force refresh the device list
1516
+ forceRefreshAudioDevices()
1517
+ }
1518
+ }
1519
+
1520
+ context.registerReceiver(deviceReceiver, filter)
1521
+ LogUtils.d(CLASS_NAME, "Started monitoring device changes")
1522
+ } catch (e: Exception) {
1523
+ LogUtils.e(CLASS_NAME, "Error starting device monitoring: ${e.message}", e)
1524
+ }
1525
+ }
1526
+
1527
+ /**
1528
+ * Helper method to find a device by ID
1529
+ */
1530
+ @RequiresApi(Build.VERSION_CODES.M)
1531
+ private fun findDeviceById(deviceId: String): AudioDeviceInfo? {
1532
+ return audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1533
+ .firstOrNull { it.id.toString() == deviceId }
1534
+ }
1535
+
1536
+ /**
1537
+ * Finds a BluetoothDevice in the list of audio devices
1538
+ */
1539
+ @RequiresApi(Build.VERSION_CODES.M)
1540
+ private fun findBluetoothDevice(device: BluetoothDevice): AudioDeviceInfo? {
1541
+ try {
1542
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1543
+ val bluetoothDevices = audioDevices.filter {
1544
+ it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
1545
+ it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
1546
+ }
1547
+
1548
+ // First try to match by address
1549
+ val foundByAddress = bluetoothDevices.firstOrNull {
1550
+ it.address == device.address
1551
+ }
1552
+
1553
+ if (foundByAddress != null) {
1554
+ return foundByAddress
1555
+ }
1556
+
1557
+ // If no match by address, try by name
1558
+ val deviceName = device.name
1559
+ if (deviceName != null) {
1560
+ return bluetoothDevices.firstOrNull {
1561
+ getDeviceName(it).contains(deviceName, ignoreCase = true)
1562
+ }
1563
+ }
1564
+
1565
+ return null
1566
+ } catch (e: Exception) {
1567
+ LogUtils.e(CLASS_NAME, "Error finding Bluetooth device: ${e.message}", e)
1568
+ return null
1569
+ }
1570
+ }
1571
+
1572
+ /**
1573
+ * Cleanup resources
1574
+ */
1575
+ fun cleanup() {
1576
+ // Stop monitoring device changes
1577
+ stopMonitoringDeviceChanges()
1578
+
1579
+ // Stop Bluetooth SCO if active
1580
+ if (audioManager.isBluetoothScoOn) {
1581
+ audioManager.stopBluetoothSco()
1582
+ audioManager.isBluetoothScoOn = false
1583
+ }
1584
+ }
1585
+
1586
+ /**
1587
+ * Stops monitoring device connection/disconnection events
1588
+ */
1589
+ private fun stopMonitoringDeviceChanges() {
1590
+ if (deviceReceiver != null) {
1591
+ try {
1592
+ context.unregisterReceiver(deviceReceiver)
1593
+ deviceReceiver = null
1594
+ LogUtils.d(CLASS_NAME, "Stopped monitoring device changes")
1595
+ } catch (e: Exception) {
1596
+ LogUtils.e(CLASS_NAME, "Error stopping device monitoring: ${e.message}", e)
1597
+ }
1598
+ }
1599
+ }
1600
+
1601
+ /**
1602
+ * Log current audio state for debugging
1603
+ */
1604
+ fun logAudioState() {
1605
+ try {
1606
+ LogUtils.d(CLASS_NAME, "--- Current Audio State ---")
1607
+ LogUtils.d(CLASS_NAME, "BluetoothScoOn: ${audioManager.isBluetoothScoOn}")
1608
+ LogUtils.d(CLASS_NAME, "WiredHeadsetOn: ${audioManager.isWiredHeadsetOn}")
1609
+ LogUtils.d(CLASS_NAME, "BluetoothHeadsetConnected: ${isBluetoothHeadsetConnected()}")
1610
+ LogUtils.d(CLASS_NAME, "LastSelectedDeviceId: $lastSelectedDeviceId")
1611
+
1612
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1613
+ val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1614
+ LogUtils.d(CLASS_NAME, "Available input devices: ${devices.size}")
1615
+
1616
+ var usbDevicesCount = 0
1617
+ var bluetoothDevicesCount = 0
1618
+ var wiredDevicesCount = 0
1619
+
1620
+ devices.forEachIndexed { index, device ->
1621
+ val deviceName = getDeviceName(device)
1622
+ val deviceType = mapDeviceType(device)
1623
+ val isSource = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) device.isSource else true
1624
+
1625
+ when (device.type) {
1626
+ AudioDeviceInfo.TYPE_USB_DEVICE,
1627
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
1628
+ AudioDeviceInfo.TYPE_USB_HEADSET -> usbDevicesCount++
1629
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
1630
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> bluetoothDevicesCount++
1631
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
1632
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> wiredDevicesCount++
1633
+ }
1634
+
1635
+ LogUtils.d(CLASS_NAME, "Device $index: $deviceName (ID: ${device.id}, Type: $deviceType, IsSource: $isSource)")
1636
+
1637
+ // Log address if available (helps track bluetooth devices)
1638
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1639
+ val address = device.address
1640
+ if (address != null) {
1641
+ LogUtils.d(CLASS_NAME, " Address: ${address}")
1642
+ }
1643
+ }
1644
+
1645
+ // For M+, log detailed capabilities
1646
+ try {
1647
+ val sampleRates = device.sampleRates?.joinToString(", ") ?: "Unknown"
1648
+ val channelCounts = device.channelCounts?.joinToString(", ") ?: "Unknown"
1649
+ LogUtils.d(CLASS_NAME, " Capabilities: SampleRates=[$sampleRates], Channels=[$channelCounts]")
1650
+ } catch (e: Exception) {
1651
+ LogUtils.d(CLASS_NAME, " Error getting capabilities: ${e.message}")
1652
+ }
1653
+ }
1654
+
1655
+ LogUtils.d(CLASS_NAME, "Device Counts: USB=$usbDevicesCount, Bluetooth=$bluetoothDevicesCount, Wired=$wiredDevicesCount")
1656
+
1657
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1658
+ val commsDevice = audioManager.communicationDevice
1659
+ if (commsDevice != null) {
1660
+ LogUtils.d(CLASS_NAME, "Communication device: ${getDeviceName(commsDevice)} (ID: ${commsDevice.id})")
1661
+ } else {
1662
+ LogUtils.d(CLASS_NAME, "No communication device set")
1663
+ }
1664
+ }
1665
+ }
1666
+
1667
+ // Log audio system properties
1668
+ val mode = when (audioManager.mode) {
1669
+ AudioManager.MODE_NORMAL -> "NORMAL"
1670
+ AudioManager.MODE_RINGTONE -> "RINGTONE"
1671
+ AudioManager.MODE_IN_CALL -> "IN_CALL"
1672
+ AudioManager.MODE_IN_COMMUNICATION -> "IN_COMMUNICATION"
1673
+ else -> "UNKNOWN(${audioManager.mode})"
1674
+ }
1675
+
1676
+ LogUtils.d(CLASS_NAME, "AudioManager Mode: $mode")
1677
+ LogUtils.d(CLASS_NAME, "-------------------------")
1678
+ } catch (e: Exception) {
1679
+ LogUtils.e(CLASS_NAME, "Error logging audio state: ${e.message}", e)
1680
+ }
1681
+ }
1682
+
1683
+ /**
1684
+ * Log a device disconnection event for better debugging
1685
+ */
1686
+ private fun logDeviceDisconnection(deviceId: String, reason: String) {
1687
+ LogUtils.d(CLASS_NAME, "=== DEVICE DISCONNECTION ===")
1688
+ LogUtils.d(CLASS_NAME, "Device ID: $deviceId")
1689
+ LogUtils.d(CLASS_NAME, "Reason: $reason")
1690
+ LogUtils.d(CLASS_NAME, "Last Selected Device ID: $lastSelectedDeviceId")
1691
+
1692
+ // Get device info if possible
1693
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1694
+ val device = findDeviceById(deviceId)
1695
+ if (device != null) {
1696
+ LogUtils.d(CLASS_NAME, "Disconnected device: ${getDeviceName(device)}")
1697
+ LogUtils.d(CLASS_NAME, "Device type: ${mapDeviceType(device)} (raw type: ${device.type})")
1698
+ } else {
1699
+ LogUtils.d(CLASS_NAME, "Device ID $deviceId no longer found in device list")
1700
+ }
1701
+ }
1702
+
1703
+ // Check current device after disconnection
1704
+ val currentDevice = getCurrentInputDeviceInternal()
1705
+ if (currentDevice != null) {
1706
+ LogUtils.d(CLASS_NAME, "Current device after disconnection: ${currentDevice["name"]} (ID: ${currentDevice["id"]})")
1707
+ } else {
1708
+ LogUtils.d(CLASS_NAME, "No current device found after disconnection")
1709
+ }
1710
+
1711
+ logAudioState()
1712
+ LogUtils.d(CLASS_NAME, "==========================")
1713
+ }
1714
+
1715
+ /**
1716
+ * Handles audio device disconnection based on the recording configuration
1717
+ */
1718
+ private suspend fun handleDeviceDisconnection(deviceId: String) {
1719
+ // Always pause on device disconnection - simpler approach
1720
+ LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId. Pausing recording.")
1721
+ delegate?.onDeviceDisconnected(deviceId)
1722
+ }
1723
+
1724
+ /**
1725
+ * Handles audio device connection
1726
+ */
1727
+ private fun handleDeviceConnection(deviceId: String) {
1728
+ LogUtils.d(CLASS_NAME, "Device connected: $deviceId")
1729
+ onDeviceConnected?.invoke(deviceId)
1730
+ }
1731
+
1732
+ /**
1733
+ * Handles audio device disconnection (for React Native events)
1734
+ */
1735
+ private fun handleDeviceDisconnectionEvent(deviceId: String) {
1736
+ LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId")
1737
+ onDeviceDisconnected?.invoke(deviceId)
1738
+ }
1739
+
1740
+
1741
+ }