@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,670 @@
1
+ //
2
+ // AudioDeviceManager.swift
3
+ // Pods
4
+ //
5
+ // Created by Arthur on 4/29/25.
6
+ //
7
+
8
+ import Foundation
9
+ import AVFoundation
10
+ import ExpoModulesCore
11
+
12
+ // MARK: - Delegate Protocol
13
+ protocol AudioDeviceManagerDelegate: AnyObject {
14
+ func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String)
15
+ // Future delegate methods can be added here
16
+ }
17
+
18
+ /// Manages audio device detection, selection and capabilities
19
+ class AudioDeviceManager {
20
+ // MARK: - Properties
21
+ weak var delegate: AudioDeviceManagerDelegate? // Add delegate property
22
+
23
+ // MARK: - Device Type Constants
24
+
25
+ // Constants for device types - standardized across platforms
26
+ private let deviceTypeBuiltinMic = "builtin_mic"
27
+ private let deviceTypeBluetooth = "bluetooth"
28
+ private let deviceTypeUSB = "usb"
29
+ private let deviceTypeWiredHeadset = "wired_headset"
30
+ private let deviceTypeWiredHeadphones = "wired_headphones"
31
+ private let deviceTypeSpeaker = "speaker"
32
+ private let deviceTypeUnknown = "unknown"
33
+
34
+ // Flag to prevent infinite loops
35
+ private static var isAudioSessionPrepared = false
36
+ private static var lastPreparationTime: TimeInterval = 0
37
+
38
+ // Observer handle
39
+ private var routeChangeObserver: Any?
40
+
41
+ // MARK: - Initialization and Deinitialization
42
+ init() {
43
+ // Start monitoring route changes on initialization
44
+ startMonitoringDeviceChanges()
45
+ }
46
+
47
+ deinit {
48
+ // Stop monitoring when the instance is deallocated
49
+ stopMonitoringDeviceChanges()
50
+ }
51
+
52
+ // MARK: - Public Methods
53
+
54
+ /// Maps AVAudioSession port types to standardized device types
55
+ func mapDeviceType(_ portType: AVAudioSession.Port) -> String {
56
+ Logger.debug("AudioDeviceManager", "Mapping device type for port: \(portType.rawValue)")
57
+ switch portType {
58
+ case .builtInMic:
59
+ return deviceTypeBuiltinMic
60
+ case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE:
61
+ return deviceTypeBluetooth
62
+ case .headphones:
63
+ return deviceTypeWiredHeadphones
64
+ case .headsetMic:
65
+ return deviceTypeWiredHeadset
66
+ case .usbAudio:
67
+ return deviceTypeUSB
68
+ case .builtInSpeaker:
69
+ return deviceTypeSpeaker
70
+ default:
71
+ return deviceTypeUnknown
72
+ }
73
+ }
74
+
75
+ /// Prepares the audio session to detect all available devices, including Bluetooth
76
+ private func prepareAudioSession(force: Bool = false) -> Bool {
77
+ // Skip preparation if already prepared and not forcing
78
+ let now = Date().timeIntervalSince1970
79
+ let timeSinceLastPreparation = now - AudioDeviceManager.lastPreparationTime
80
+
81
+ if AudioDeviceManager.isAudioSessionPrepared && !force && timeSinceLastPreparation < 5.0 {
82
+ Logger.debug("AudioDeviceManager", "Audio session already prepared, skipping")
83
+ return true
84
+ }
85
+
86
+ Logger.debug("AudioDeviceManager", "Preparing audio session for device detection")
87
+ do {
88
+ let session = AVAudioSession.sharedInstance()
89
+
90
+ // Preserve existing session configuration if already set up for recording
91
+ if session.category == .playAndRecord {
92
+ Logger.debug("AudioDeviceManager", "Session already .playAndRecord, preserving categoryOptions")
93
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
94
+ } else {
95
+ // Configure with options needed for Bluetooth detection
96
+ try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
97
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
98
+ }
99
+
100
+ // Give the system a moment to detect Bluetooth devices if needed
101
+ // Minimal delay that still allows devices to be detected
102
+ Thread.sleep(forTimeInterval: 0.1)
103
+
104
+ // Mark as prepared
105
+ AudioDeviceManager.isAudioSessionPrepared = true
106
+ AudioDeviceManager.lastPreparationTime = now
107
+
108
+ Logger.debug("AudioDeviceManager", "Audio session prepared for device detection")
109
+ return true
110
+ } catch {
111
+ Logger.debug("AudioDeviceManager", "Failed to prepare audio session: \(error.localizedDescription)")
112
+ return false
113
+ }
114
+ }
115
+
116
+ /// Force a refresh of the audio session preparation
117
+ public func forceRefreshAudioSession() -> Bool {
118
+ // Only allow force refresh once every second to prevent excessive refreshes
119
+ let now = Date().timeIntervalSince1970
120
+ let timeSinceLastPreparation = now - AudioDeviceManager.lastPreparationTime
121
+
122
+ if timeSinceLastPreparation < 1.0 {
123
+ Logger.debug("AudioDeviceManager", "Skipping force refresh - too soon since last preparation (\(timeSinceLastPreparation) seconds)")
124
+ return false
125
+ }
126
+
127
+ return prepareAudioSession(force: true)
128
+ }
129
+
130
+ /// Gets capabilities for an audio input device
131
+ func getDeviceCapabilities(_ port: AVAudioSessionPortDescription) -> [String: Any] {
132
+ Logger.debug("AudioDeviceManager", "Getting capabilities for device: \(port.portName) (ID: \(port.uid))")
133
+ let session = AVAudioSession.sharedInstance()
134
+
135
+ // Test standard sample rates for support
136
+ let sampleRates = [8000, 16000, 22050, 44100, 48000, 96000].filter { rate in
137
+ let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: Double(rate), channels: 1, interleaved: false)
138
+ return session.isInputAvailable && format != nil
139
+ }
140
+
141
+ return [
142
+ "sampleRates": sampleRates,
143
+ "channelCounts": [1, 2], // Most iOS devices support mono and stereo
144
+ "bitDepths": [16, 24], // Common bit depths on iOS
145
+ "hasEchoCancellation": true, // iOS doesn't expose this per-device, set to true as it's generally available
146
+ "hasNoiseSuppression": true, // iOS doesn't expose this per-device, set to true as it's generally available
147
+ "hasAutomaticGainControl": true // iOS doesn't expose this per-device, set to true as it's generally available
148
+ ]
149
+ }
150
+
151
+ /// Gets a list of available audio input devices
152
+ func getAvailableInputDevices(promise: Promise) {
153
+ Logger.debug("AudioDeviceManager", "Getting available input devices")
154
+
155
+ // Prepare audio session if needed
156
+ let prepared = prepareAudioSession()
157
+ if !prepared {
158
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device list may be incomplete")
159
+ }
160
+
161
+ do {
162
+ let session = AVAudioSession.sharedInstance()
163
+
164
+ // We should have already activated the session in prepareAudioSession
165
+ // But ensure it's active just in case
166
+ try session.setActive(true)
167
+
168
+ let currentPreferredInput = session.preferredInput
169
+
170
+ var devices = [[String: Any]]()
171
+
172
+ // First add current route devices as they're definitely available
173
+ for input in session.currentRoute.inputs {
174
+ let deviceType = mapDeviceType(input.portType)
175
+ let isDefault = currentPreferredInput == nil ?
176
+ (input.portType == .builtInMic) : // Default is usually built-in mic
177
+ (input.uid == currentPreferredInput?.uid)
178
+
179
+ let deviceId = normalizeBluetoothDeviceId(input.uid)
180
+
181
+ Logger.debug("AudioDeviceManager", "Current route device: \(input.portName) (type: \(deviceType), ID: \(deviceId))")
182
+
183
+ devices.append([
184
+ "id": deviceId,
185
+ "name": input.portName,
186
+ "type": deviceType,
187
+ "isDefault": isDefault,
188
+ "capabilities": getDeviceCapabilities(input),
189
+ "isAvailable": true,
190
+ "source": "currentRoute"
191
+ ])
192
+ }
193
+
194
+ // Then add from availableInputs
195
+ if let availableInputs = session.availableInputs {
196
+ for port in availableInputs {
197
+ let deviceType = mapDeviceType(port.portType)
198
+ let isDefault = currentPreferredInput == nil ?
199
+ (port.portType == .builtInMic) : // Default is usually built-in mic
200
+ (port.uid == currentPreferredInput?.uid)
201
+
202
+ let deviceId = normalizeBluetoothDeviceId(port.uid)
203
+
204
+ // Skip if already in our list
205
+ if !devices.contains(where: { ($0["id"] as? String) == deviceId }) {
206
+ Logger.debug("AudioDeviceManager", "Available input: \(port.portName) (type: \(deviceType), ID: \(deviceId))")
207
+
208
+ devices.append([
209
+ "id": deviceId,
210
+ "name": port.portName,
211
+ "type": deviceType,
212
+ "isDefault": isDefault,
213
+ "capabilities": getDeviceCapabilities(port),
214
+ "isAvailable": true,
215
+ "source": "availableInputs"
216
+ ])
217
+ }
218
+ }
219
+ }
220
+
221
+ Logger.debug("AudioDeviceManager", "Found \(devices.count) available input devices")
222
+ promise.resolve(devices)
223
+ } catch {
224
+ Logger.debug("AudioDeviceManager", "Error getting available input devices: \(error.localizedDescription)")
225
+ promise.reject("DEVICE_DETECTION_ERROR", "Failed to get available audio devices: \(error.localizedDescription)")
226
+ }
227
+ }
228
+
229
+ /// Gets the currently selected audio input device
230
+ func getCurrentInputDevice(promise: Promise) {
231
+ Logger.debug("AudioDeviceManager", "Getting current input device")
232
+
233
+ // Prepare audio session if needed
234
+ let prepared = prepareAudioSession()
235
+ if !prepared {
236
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, current device may not be correctly detected")
237
+ }
238
+
239
+ do {
240
+ let session = AVAudioSession.sharedInstance()
241
+
242
+ // We should have already activated the session in prepareAudioSession
243
+ // But ensure it's active just in case
244
+ try session.setActive(true)
245
+
246
+ // Check current route first
247
+ if let currentPort = session.currentRoute.inputs.first {
248
+ let deviceType = mapDeviceType(currentPort.portType)
249
+ let isDefault = session.preferredInput == nil || session.preferredInput?.portType == currentPort.portType
250
+ let deviceId = normalizeBluetoothDeviceId(currentPort.uid)
251
+
252
+ Logger.debug("AudioDeviceManager", "Current input device: \(currentPort.portName) (ID: \(deviceId), type: \(deviceType))")
253
+
254
+ let device: [String: Any] = [
255
+ "id": deviceId,
256
+ "name": currentPort.portName,
257
+ "type": deviceType,
258
+ "isDefault": isDefault,
259
+ "capabilities": getDeviceCapabilities(currentPort),
260
+ "isAvailable": true,
261
+ "source": "currentRoute"
262
+ ]
263
+
264
+ promise.resolve(device)
265
+ return
266
+ }
267
+
268
+ // Fallback to preferred input
269
+ if let preferredInput = session.preferredInput {
270
+ let deviceType = mapDeviceType(preferredInput.portType)
271
+ let deviceId = normalizeBluetoothDeviceId(preferredInput.uid)
272
+
273
+ Logger.debug("AudioDeviceManager", "Current input from preferred: \(preferredInput.portName) (ID: \(deviceId), type: \(deviceType))")
274
+
275
+ let device: [String: Any] = [
276
+ "id": deviceId,
277
+ "name": preferredInput.portName,
278
+ "type": deviceType,
279
+ "isDefault": true,
280
+ "capabilities": getDeviceCapabilities(preferredInput),
281
+ "isAvailable": true,
282
+ "source": "preferredInput"
283
+ ]
284
+
285
+ promise.resolve(device)
286
+ return
287
+ }
288
+
289
+ // No input device is currently selected
290
+ Logger.debug("AudioDeviceManager", "No current input device found")
291
+ promise.resolve(nil)
292
+ } catch {
293
+ Logger.debug("AudioDeviceManager", "Error getting current input device: \(error.localizedDescription)")
294
+ promise.reject("DEVICE_DETECTION_ERROR", "Failed to get current audio device: \(error.localizedDescription)")
295
+ }
296
+ }
297
+
298
+ /// Gets the default audio input device (usually built-in mic)
299
+ /// This is an async version useful for fallback logic.
300
+ func getDefaultInputDevice() async -> AudioDevice? {
301
+ Logger.debug("AudioDeviceManager", "Getting default input device")
302
+
303
+ let prepared = prepareAudioSession()
304
+ if !prepared {
305
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, default device detection may be inaccurate")
306
+ }
307
+
308
+ let session = AVAudioSession.sharedInstance()
309
+ do {
310
+ try session.setActive(true) // Ensure session is active
311
+
312
+ // Find the built-in microphone port, which is typically the default fallback
313
+ if let defaultPort = session.availableInputs?.first(where: { $0.portType == .builtInMic }) {
314
+ let deviceType = mapDeviceType(defaultPort.portType)
315
+ let deviceId = normalizeBluetoothDeviceId(defaultPort.uid)
316
+ let capabilities = getDeviceCapabilities(defaultPort)
317
+
318
+ Logger.debug("AudioDeviceManager", "Found default device: \(defaultPort.portName) (ID: \(deviceId), Type: \(deviceType))")
319
+
320
+ // Convert capabilities dictionary to Capabilities struct/object if needed
321
+ let audioCapabilities = AudioDeviceCapabilities(
322
+ sampleRates: capabilities["sampleRates"] as? [Int] ?? [],
323
+ channelCounts: capabilities["channelCounts"] as? [Int] ?? [],
324
+ bitDepths: capabilities["bitDepths"] as? [Int] ?? []
325
+ // Add boolean flags if available in your dictionary
326
+ )
327
+
328
+ return AudioDevice(
329
+ id: deviceId,
330
+ name: defaultPort.portName,
331
+ type: deviceType,
332
+ isDefault: true, // Assume it's the default we're looking for
333
+ capabilities: audioCapabilities,
334
+ isAvailable: true // It's available if found here
335
+ )
336
+ } else {
337
+ Logger.debug("AudioDeviceManager", "Could not find built-in mic as default device.")
338
+ return nil
339
+ }
340
+ } catch {
341
+ Logger.debug("AudioDeviceManager", "Error getting default input device: \(error)")
342
+ return nil
343
+ }
344
+ }
345
+
346
+ /// Selects a specific audio input device for recording
347
+ func selectInputDevice(_ deviceId: String, promise: Promise) {
348
+ Logger.debug("AudioDeviceManager", "Attempting to select input device with ID: \(deviceId)")
349
+
350
+ // Prepare audio session - use force: true for device selection to ensure we get the latest devices
351
+ let prepared = prepareAudioSession(force: true)
352
+ if !prepared {
353
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device selection may not work correctly")
354
+ }
355
+
356
+ do {
357
+ let session = AVAudioSession.sharedInstance()
358
+
359
+ // Ensure the session is active
360
+ try session.setActive(true)
361
+
362
+ // For Bluetooth devices, normalize and match by prefix
363
+ let normalizedRequestedId = normalizeBluetoothDeviceId(deviceId)
364
+ let isBluetoothDevice = deviceId.contains(":")
365
+
366
+ Logger.debug("AudioDeviceManager", "Selecting \(isBluetoothDevice ? "Bluetooth" : "non-Bluetooth") device with normalized ID: \(normalizedRequestedId)")
367
+
368
+ // Find the device with the specified ID
369
+ let selectedPort: AVAudioSessionPortDescription?
370
+
371
+ if isBluetoothDevice {
372
+ // For Bluetooth devices, match by normalized ID
373
+ selectedPort = session.availableInputs?.first { port in
374
+ let portNormalizedId = normalizeBluetoothDeviceId(port.uid)
375
+ let matches = portNormalizedId == normalizedRequestedId
376
+ Logger.debug("AudioDeviceManager", "Checking device \(port.portName) (ID: \(port.uid), Normalized: \(portNormalizedId)) - Matches: \(matches)")
377
+ return matches
378
+ }
379
+ } else {
380
+ // For non-Bluetooth devices, direct match
381
+ selectedPort = session.availableInputs?.first { port in
382
+ let matches = port.uid == deviceId
383
+ Logger.debug("AudioDeviceManager", "Checking device \(port.portName) (ID: \(port.uid)) - Matches: \(matches)")
384
+ return matches
385
+ }
386
+ }
387
+
388
+ guard let selectedPort = selectedPort else {
389
+ Logger.debug("AudioDeviceManager", "Device not found with ID \(deviceId)")
390
+
391
+ // Log all available devices to help debugging
392
+ if let availableInputs = session.availableInputs {
393
+ Logger.debug("AudioDeviceManager", "Available devices:")
394
+ for (index, device) in availableInputs.enumerated() {
395
+ Logger.debug("AudioDeviceManager", "\(index+1). \(device.portName) (ID: \(device.uid), Normalized: \(normalizeBluetoothDeviceId(device.uid)))")
396
+ }
397
+ } else {
398
+ Logger.debug("AudioDeviceManager", "No available devices found")
399
+ }
400
+
401
+ promise.reject("DEVICE_NOT_FOUND", "The selected audio device is not available")
402
+ return
403
+ }
404
+
405
+ // Set the preferred input device
406
+ Logger.debug("AudioDeviceManager", "Setting preferred input to: \(selectedPort.portName) (ID: \(selectedPort.uid))")
407
+ try session.setPreferredInput(selectedPort)
408
+
409
+ // Verify selection
410
+ if let currentInput = session.currentRoute.inputs.first {
411
+ let succeeded = (currentInput.uid == selectedPort.uid ||
412
+ normalizeBluetoothDeviceId(currentInput.uid) == normalizeBluetoothDeviceId(selectedPort.uid))
413
+ Logger.debug("AudioDeviceManager", "Device selection \(succeeded ? "succeeded" : "failed") - Current device: \(currentInput.portName) (ID: \(currentInput.uid))")
414
+ }
415
+
416
+ Logger.debug("AudioDeviceManager", "Device selected successfully")
417
+ promise.resolve(true)
418
+ } catch {
419
+ Logger.debug("AudioDeviceManager", "Failed to select device: \(error.localizedDescription)")
420
+ promise.reject("DEVICE_SELECTION_FAILED", "Failed to select audio device: \(error.localizedDescription)")
421
+ }
422
+ }
423
+
424
+ /// Selects a specific audio input device asynchronously (useful for internal calls)
425
+ func selectDevice(_ deviceId: String) async -> Bool {
426
+ Logger.debug("AudioDeviceManager", "Attempting to select input device with ID: \(deviceId) (async)")
427
+
428
+ let prepared = prepareAudioSession(force: true)
429
+ if !prepared {
430
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device selection may not work correctly")
431
+ return false
432
+ }
433
+
434
+ do {
435
+ let session = AVAudioSession.sharedInstance()
436
+ try session.setActive(true)
437
+
438
+ let normalizedRequestedId = normalizeBluetoothDeviceId(deviceId)
439
+ let isBluetoothDevice = deviceId.contains(":")
440
+
441
+ Logger.debug("AudioDeviceManager", "Selecting \(isBluetoothDevice ? "Bluetooth" : "non-Bluetooth") device with normalized ID: \(normalizedRequestedId)")
442
+
443
+ let selectedPort: AVAudioSessionPortDescription?
444
+ if isBluetoothDevice {
445
+ selectedPort = session.availableInputs?.first { port in
446
+ normalizeBluetoothDeviceId(port.uid) == normalizedRequestedId
447
+ }
448
+ } else {
449
+ selectedPort = session.availableInputs?.first { $0.uid == deviceId }
450
+ }
451
+
452
+ guard let portToSet = selectedPort else {
453
+ Logger.debug("AudioDeviceManager", "Device not found with ID \(deviceId) for async selection")
454
+ return false
455
+ }
456
+
457
+ Logger.debug("AudioDeviceManager", "Setting preferred input to: \(portToSet.portName) (ID: \(portToSet.uid)) (async)")
458
+ try session.setPreferredInput(portToSet)
459
+ // Add a small delay hoping the system applies the change before potential next operations
460
+ try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
461
+
462
+ // Optional: Verify selection succeeded (might be less reliable immediately after setting)
463
+ if let currentInput = session.currentRoute.inputs.first {
464
+ let succeeded = (currentInput.uid == portToSet.uid || normalizeBluetoothDeviceId(currentInput.uid) == normalizedRequestedId)
465
+ Logger.debug("AudioDeviceManager", "Async selection verification: \(succeeded ? "succeeded" : "failed")")
466
+ return succeeded
467
+ } else {
468
+ // If no current input after setting, assume failure
469
+ return false
470
+ }
471
+
472
+ } catch {
473
+ Logger.debug("AudioDeviceManager", "Failed to select device asynchronously: \(error.localizedDescription)")
474
+ return false
475
+ }
476
+ }
477
+
478
+ /// Determines if a device is still available
479
+ func isDeviceAvailable(_ deviceId: String) -> Bool {
480
+ Logger.debug("AudioDeviceManager", "Checking availability for device ID: \(deviceId)")
481
+
482
+ // Prepare audio session if needed
483
+ let prepared = prepareAudioSession()
484
+ if !prepared {
485
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device availability check may not be accurate")
486
+ }
487
+
488
+ let session = AVAudioSession.sharedInstance()
489
+
490
+ // Handle Bluetooth devices with multiple profiles (SCO, A2DP, etc.)
491
+ let isBluetoothDevice = deviceId.contains(":") // Most Bluetooth devices have MAC addresses with colons
492
+
493
+ if isBluetoothDevice {
494
+ // For Bluetooth devices, check if any device with the same MAC address prefix is available
495
+ let baseDeviceId = deviceId.split(separator: "-").first ?? Substring(deviceId)
496
+
497
+ // Log all available inputs for debugging
498
+ Logger.debug("AudioDeviceManager", "Available devices to check against:")
499
+ if let availableInputs = session.availableInputs {
500
+ for (index, device) in availableInputs.enumerated() {
501
+ let normalizedId = normalizeBluetoothDeviceId(device.uid)
502
+ let matches = device.uid.starts(with: String(baseDeviceId))
503
+ Logger.debug("AudioDeviceManager", "\(index+1). \(device.portName) (ID: \(device.uid), Normalized: \(normalizedId)) - Matches: \(matches)")
504
+ }
505
+ } else {
506
+ Logger.debug("AudioDeviceManager", "No available devices found")
507
+ }
508
+
509
+ // Also check current route
510
+ for (index, input) in session.currentRoute.inputs.enumerated() {
511
+ let normalizedId = normalizeBluetoothDeviceId(input.uid)
512
+ let matches = input.uid.starts(with: String(baseDeviceId))
513
+ Logger.debug("AudioDeviceManager", "Current route input \(index+1): \(input.portName) (ID: \(input.uid), Normalized: \(normalizedId)) - Matches: \(matches)")
514
+ }
515
+
516
+ let result = session.availableInputs?.contains { $0.uid.starts(with: String(baseDeviceId)) } ?? false
517
+ Logger.debug("AudioDeviceManager", "Bluetooth device \(deviceId) with base ID \(baseDeviceId) available: \(result)")
518
+ return result
519
+ } else {
520
+ // Standard device ID check for non-Bluetooth devices
521
+ return session.availableInputs?.contains { $0.uid == deviceId } ?? false
522
+ }
523
+ }
524
+
525
+ /// Resets the selected device to system default (usually built-in mic)
526
+ /// - Parameter completion: Callback with success (Bool) and optional error
527
+ func resetToDefaultDevice(completion: @escaping (Bool, Error?) -> Void) {
528
+ Logger.debug("AudioDeviceManager", "Attempting to reset to default input device")
529
+
530
+ // Prepare audio session if needed
531
+ let prepared = prepareAudioSession()
532
+ if !prepared {
533
+ Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device reset may not work correctly")
534
+ }
535
+
536
+ do {
537
+ let session = AVAudioSession.sharedInstance()
538
+
539
+ // Log current device before reset
540
+ if let currentDevice = session.currentRoute.inputs.first {
541
+ Logger.debug("AudioDeviceManager", "Current device before reset: \(currentDevice.portName) (ID: \(currentDevice.uid))")
542
+ } else {
543
+ Logger.debug("AudioDeviceManager", "No current device before reset")
544
+ }
545
+
546
+ // Setting preferred input to nil lets the system choose the default
547
+ try session.setPreferredInput(nil)
548
+
549
+ // Log the device after reset
550
+ if let newDevice = session.currentRoute.inputs.first {
551
+ Logger.debug("AudioDeviceManager", "Reset to default device: \(newDevice.portName) (ID: \(newDevice.uid))")
552
+
553
+ // Check if it's actually the built-in mic (which is the typical default)
554
+ let isBuiltIn = newDevice.portType == .builtInMic
555
+ Logger.debug("AudioDeviceManager", "Reset device is built-in mic: \(isBuiltIn)")
556
+ } else {
557
+ Logger.debug("AudioDeviceManager", "No device found after reset")
558
+ }
559
+
560
+ completion(true, nil)
561
+ } catch {
562
+ Logger.debug("AudioDeviceManager", "Failed to reset to default device: \(error.localizedDescription)")
563
+ completion(false, error)
564
+ }
565
+ }
566
+
567
+ /// Starts monitoring device connection/disconnection events
568
+ private func startMonitoringDeviceChanges() {
569
+ // Ensure we don't add multiple observers
570
+ stopMonitoringDeviceChanges()
571
+
572
+ Logger.debug("AudioDeviceManager", "Starting device change monitoring")
573
+ routeChangeObserver = NotificationCenter.default.addObserver(
574
+ forName: AVAudioSession.routeChangeNotification,
575
+ object: nil,
576
+ queue: .main // Process on main queue to avoid threading issues with delegate calls
577
+ ) { [weak self] notification in
578
+ self?.handleRouteChange(notification)
579
+ }
580
+ }
581
+
582
+ /// Stops monitoring device changes
583
+ private func stopMonitoringDeviceChanges() {
584
+ if let observer = routeChangeObserver {
585
+ Logger.debug("AudioDeviceManager", "Stopping device change monitoring")
586
+ NotificationCenter.default.removeObserver(observer)
587
+ routeChangeObserver = nil
588
+ }
589
+ }
590
+
591
+ /// Handles route change notifications to detect device connections and disconnections
592
+ @objc private func handleRouteChange(_ notification: Notification) {
593
+ guard let userInfo = notification.userInfo,
594
+ let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
595
+ let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
596
+ return
597
+ }
598
+
599
+ Logger.debug("AudioDeviceManager", "Route change detected, reason: \(reason.rawValue)")
600
+
601
+ // Only proceed if a device was potentially removed or added or the route changed significantly
602
+ guard reason == .oldDeviceUnavailable || reason == .newDeviceAvailable || reason == .override || reason == .routeConfigurationChange else {
603
+ Logger.debug("AudioDeviceManager", "Ignoring route change reason: \(reason.rawValue)")
604
+ return
605
+ }
606
+
607
+ // Get the *previous* route description
608
+ guard let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription else {
609
+ Logger.debug("AudioDeviceManager", "No previous route info found for device change check.")
610
+ return
611
+ }
612
+
613
+ // Get the *current* available input devices
614
+ let currentInputs = AVAudioSession.sharedInstance().availableInputs ?? []
615
+ let currentInputIds = Set(currentInputs.map { normalizeBluetoothDeviceId($0.uid) })
616
+ let previousInputIds = Set(previousRoute.inputs.map { normalizeBluetoothDeviceId($0.uid) })
617
+
618
+ // Check for DISCONNECTED devices (were in previous route but not in current available)
619
+ for previousInputPort in previousRoute.inputs {
620
+ let normalizedPreviousId = normalizeBluetoothDeviceId(previousInputPort.uid)
621
+ if !currentInputIds.contains(normalizedPreviousId) {
622
+ Logger.debug("AudioDeviceManager", "Detected disconnection of device: \(previousInputPort.portName) (Normalized ID: \(normalizedPreviousId))")
623
+ // Keep existing disconnection delegate method unchanged
624
+ delegate?.audioDeviceManager(self, didDetectDisconnectionOfDevice: normalizedPreviousId)
625
+ }
626
+ }
627
+
628
+ // Check for CONNECTED devices (are in current available but were not in previous route)
629
+ for currentInput in currentInputs {
630
+ let normalizedCurrentId = normalizeBluetoothDeviceId(currentInput.uid)
631
+ if !previousInputIds.contains(normalizedCurrentId) {
632
+ Logger.debug("AudioDeviceManager", "Detected connection of device: \(currentInput.portName) (Normalized ID: \(normalizedCurrentId))")
633
+ // Emit connection event via notification
634
+ NotificationCenter.default.post(
635
+ name: NSNotification.Name("DeviceConnected"),
636
+ object: nil,
637
+ userInfo: ["deviceId": normalizedCurrentId]
638
+ )
639
+ }
640
+ }
641
+ }
642
+
643
+ /// Normalizes Bluetooth device IDs by removing profile suffixes
644
+ public func normalizeBluetoothDeviceId(_ deviceId: String) -> String {
645
+ // For Bluetooth devices with MAC addresses and profile suffixes (like -tsco)
646
+ if deviceId.contains(":") && deviceId.contains("-") {
647
+ // Split by the hyphen and take the first part (the MAC address)
648
+ return deviceId.split(separator: "-").first.map(String.init) ?? deviceId
649
+ }
650
+ return deviceId
651
+ }
652
+ }
653
+
654
+ // Add structure for AudioDeviceCapabilities if not defined elsewhere
655
+ struct AudioDeviceCapabilities {
656
+ let sampleRates: [Int]
657
+ let channelCounts: [Int]
658
+ let bitDepths: [Int]
659
+ // Add boolean flags if needed
660
+ }
661
+
662
+ // Add structure for AudioDevice if not defined elsewhere
663
+ struct AudioDevice {
664
+ let id: String
665
+ let name: String
666
+ let type: String
667
+ let isDefault: Bool
668
+ let capabilities: AudioDeviceCapabilities
669
+ let isAvailable: Bool
670
+ }