@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,800 @@
1
+ import { EventEmitter } from 'expo-modules-core'
2
+ import { Platform } from 'react-native'
3
+
4
+ import {
5
+ AudioDevice,
6
+ AudioDeviceCapabilities,
7
+ DeviceDisconnectionBehavior,
8
+ ConsoleLike,
9
+ } from './AudioStudio.types'
10
+ import AudioStudioModule from './AudioStudioModule'
11
+
12
+ // Default device fallback for web and unsupported platforms
13
+ const DEFAULT_DEVICE: AudioDevice = {
14
+ id: 'default',
15
+ name: 'Default Microphone',
16
+ type: 'builtin_mic',
17
+ isDefault: true,
18
+ isAvailable: true,
19
+ capabilities: {
20
+ sampleRates: [16000, 44100, 48000],
21
+ channelCounts: [1, 2],
22
+ bitDepths: [16, 24, 32],
23
+ hasEchoCancellation: true,
24
+ hasNoiseSuppression: true,
25
+ hasAutomaticGainControl: true,
26
+ },
27
+ }
28
+
29
+ // Helper function to map raw object to AudioDevice interface
30
+ // This handles potential inconsistencies from the native module
31
+ function mapRawDeviceToAudioDevice(rawDevice: any): AudioDevice {
32
+ const capabilities = rawDevice.capabilities || {}
33
+ return {
34
+ id: rawDevice.id || 'unknown',
35
+ name: rawDevice.name || 'Unknown Device',
36
+ type: rawDevice.type || 'unknown',
37
+ isDefault: rawDevice.isDefault || false,
38
+ isAvailable:
39
+ rawDevice.isAvailable !== undefined ? rawDevice.isAvailable : true, // Default to true if undefined
40
+ capabilities: {
41
+ sampleRates: capabilities.sampleRates || [16000, 44100, 48000], // Provide defaults
42
+ channelCounts: capabilities.channelCounts || [1, 2],
43
+ bitDepths: capabilities.bitDepths || [16, 24, 32],
44
+ hasEchoCancellation: capabilities.hasEchoCancellation,
45
+ hasNoiseSuppression: capabilities.hasNoiseSuppression,
46
+ hasAutomaticGainControl: capabilities.hasAutomaticGainControl,
47
+ },
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Class that provides a cross-platform API for managing audio input devices
53
+ *
54
+ * EVENT API SPECIFICATION:
55
+ * ========================
56
+ *
57
+ * Device Events (deviceChangedEvent):
58
+ * ```
59
+ * {
60
+ * type: "deviceConnected" | "deviceDisconnected",
61
+ * deviceId: string
62
+ * }
63
+ * ```
64
+ *
65
+ * Recording Interruption Events (recordingInterruptedEvent):
66
+ * ```
67
+ * {
68
+ * reason: "userPaused" | "userResumed" | "audioFocusLoss" | "audioFocusGain" |
69
+ * "deviceFallback" | "deviceSwitchFailed" | "phoneCall" | "phoneCallEnded",
70
+ * isPaused: boolean,
71
+ * timestamp: number
72
+ * }
73
+ * ```
74
+ *
75
+ * NOTE: Device events use "type" field, interruption events use "reason" field.
76
+ * This is intentional to distinguish between different event categories.
77
+ */
78
+ export class AudioDeviceManager {
79
+ private eventEmitter: InstanceType<typeof EventEmitter>
80
+ private currentDeviceId: string | null = null
81
+ private availableDevices: AudioDevice[] = []
82
+ private deviceChangeListeners: Set<(devices: AudioDevice[]) => void> =
83
+ new Set()
84
+ private webDeviceChangeHandler?: () => void
85
+ private lastRefreshTime: number = 0
86
+ private refreshInProgress: boolean = false
87
+ private refreshDebounceMs: number = 500 // Minimum 500ms between refreshes
88
+ private logger?: ConsoleLike
89
+
90
+ // Track temporarily disconnected devices
91
+ private temporarilyDisconnectedDevices: Set<string> = new Set()
92
+ private disconnectionTimeouts: Map<string, ReturnType<typeof setTimeout>> =
93
+ new Map()
94
+ private readonly DISCONNECTION_TIMEOUT_MS = 5000 // 5 seconds
95
+
96
+ constructor(options?: { logger?: ConsoleLike }) {
97
+ this.eventEmitter = new EventEmitter(AudioStudioModule)
98
+ this.logger = options?.logger
99
+
100
+ // Set up device event listeners for all platforms immediately
101
+ this.setupDeviceEventListeners()
102
+ }
103
+
104
+ /**
105
+ * Set up device event listeners for the current platform
106
+ */
107
+ private setupDeviceEventListeners(): void {
108
+ if (Platform.OS === 'web') {
109
+ this.setupWebDeviceChangeListener()
110
+ } else {
111
+ this.setupNativeDeviceEventListener()
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Set up native device event listener for iOS/Android
117
+ */
118
+ private setupNativeDeviceEventListener(): void {
119
+ // Store the last event type to avoid duplicates
120
+ let lastEventType: string | null = null
121
+ let lastEventTime = 0
122
+
123
+ this.eventEmitter.addListener('deviceChangedEvent', (event: any) => {
124
+ // Skip processing duplicate events that occur too close together
125
+ const now = Date.now()
126
+ const isSimilarEvent =
127
+ lastEventType === event.type &&
128
+ now - lastEventTime < this.refreshDebounceMs
129
+
130
+ if (isSimilarEvent) {
131
+ this.logger?.debug(
132
+ `Skipping similar device event (${event.type}) received too soon`
133
+ )
134
+ return
135
+ }
136
+
137
+ // Update the last event tracking
138
+ lastEventType = event.type
139
+ lastEventTime = now
140
+
141
+ // Only refresh on meaningful events
142
+ if (
143
+ event.type === 'deviceConnected' ||
144
+ event.type === 'deviceDisconnected' ||
145
+ event.type === 'routeChanged'
146
+ ) {
147
+ this.logger?.debug(`Processing device event: ${event.type}`)
148
+ // Force refresh for device events to ensure fresh data
149
+ this.forceRefreshDevices()
150
+ }
151
+ })
152
+ this.logger?.debug('Native device event listener set up')
153
+ }
154
+
155
+ /**
156
+ * Initialize the device manager with a logger
157
+ * @param logger A logger instance that implements the ConsoleLike interface
158
+ * @returns The manager instance for chaining
159
+ */
160
+ initWithLogger(logger: ConsoleLike): AudioDeviceManager {
161
+ this.setLogger(logger)
162
+ return this
163
+ }
164
+
165
+ /**
166
+ * Set the logger instance
167
+ * @param logger A logger instance that implements the ConsoleLike interface
168
+ */
169
+ setLogger(logger: ConsoleLike) {
170
+ this.logger = logger
171
+ }
172
+
173
+ /**
174
+ * Initialize or reinitialize device detection
175
+ * Useful for restarting device detection if initial setup failed
176
+ */
177
+ initializeDeviceDetection(): void {
178
+ this.logger?.debug('Initializing device detection...')
179
+
180
+ // Clean up existing listeners first
181
+ if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
182
+ if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
183
+ navigator.mediaDevices.removeEventListener(
184
+ 'devicechange',
185
+ this.webDeviceChangeHandler
186
+ )
187
+ }
188
+ this.webDeviceChangeHandler = undefined
189
+ }
190
+
191
+ // Re-setup device event listeners
192
+ this.setupDeviceEventListeners()
193
+ }
194
+
195
+ /**
196
+ * Get the current logger instance
197
+ * @returns The logger instance or undefined if not set
198
+ */
199
+ getLogger(): ConsoleLike | undefined {
200
+ return this.logger
201
+ }
202
+
203
+ /**
204
+ * Get all available audio input devices
205
+ * @param options Optional settings to force refresh the device list. Can include a refresh flag.
206
+ * @returns Promise resolving to an array of audio devices conforming to AudioDevice interface
207
+ */
208
+ async getAvailableDevices(options?: {
209
+ refresh?: boolean
210
+ }): Promise<AudioDevice[]> {
211
+ try {
212
+ if (Platform.OS === 'web') {
213
+ this.availableDevices = await this.getWebAudioDevices()
214
+ } else if (AudioStudioModule.getAvailableInputDevices) {
215
+ // Expecting an array of raw device objects from native
216
+ const rawDevices: any[] =
217
+ await AudioStudioModule.getAvailableInputDevices(options)
218
+ // Map raw objects to the AudioDevice interface
219
+ this.availableDevices = rawDevices.map(
220
+ mapRawDeviceToAudioDevice
221
+ )
222
+ } else {
223
+ // Fallback for unsupported platforms
224
+ this.availableDevices = [DEFAULT_DEVICE]
225
+ }
226
+ return this.availableDevices
227
+ } catch (error) {
228
+ this.logger?.error('Failed to get available devices:', error)
229
+ this.availableDevices = [DEFAULT_DEVICE] // Ensure state is updated on error
230
+ return this.availableDevices
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Get the currently selected audio input device
236
+ * @returns Promise resolving to the current device (conforming to AudioDevice) or null
237
+ */
238
+ async getCurrentDevice(): Promise<AudioDevice | null> {
239
+ try {
240
+ if (Platform.OS === 'web') {
241
+ if (!this.currentDeviceId) {
242
+ // On web, return the typed default device if nothing is selected
243
+ return DEFAULT_DEVICE
244
+ }
245
+ // Refresh web devices to ensure the current one is up-to-date
246
+ const webDevices = await this.getWebAudioDevices()
247
+ return (
248
+ webDevices.find((d) => d.id === this.currentDeviceId) ||
249
+ DEFAULT_DEVICE // Fallback to default if current ID not found
250
+ )
251
+ } else if (AudioStudioModule.getCurrentInputDevice) {
252
+ // Expecting a single raw device object or null from native
253
+ const rawDevice: any | null =
254
+ await AudioStudioModule.getCurrentInputDevice()
255
+ // Map to AudioDevice interface if not null
256
+ return rawDevice ? mapRawDeviceToAudioDevice(rawDevice) : null
257
+ } else {
258
+ // Fallback for unsupported platforms
259
+ return DEFAULT_DEVICE
260
+ }
261
+ } catch (error) {
262
+ this.logger?.error('Failed to get current device:', error)
263
+ return DEFAULT_DEVICE // Return default on error
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Select a specific audio input device for recording
269
+ * @param deviceId The ID of the device to select
270
+ * @returns Promise resolving to a boolean indicating success
271
+ */
272
+ async selectDevice(deviceId: string): Promise<boolean> {
273
+ try {
274
+ let success = false
275
+ if (Platform.OS === 'web') {
276
+ // Check if the device exists before setting it
277
+ const devices = await this.getWebAudioDevices()
278
+ if (devices.some((d) => d.id === deviceId)) {
279
+ this.currentDeviceId = deviceId
280
+ success = true
281
+ } else {
282
+ this.logger?.warn(
283
+ `Web: Device with ID ${deviceId} not found.`
284
+ )
285
+ success = false
286
+ }
287
+ } else if (AudioStudioModule.selectInputDevice) {
288
+ success = await AudioStudioModule.selectInputDevice(deviceId)
289
+ if (success) {
290
+ this.currentDeviceId = deviceId
291
+ }
292
+ }
293
+ // Refresh devices after selection attempt to update state
294
+ await this.refreshDevices()
295
+ return success
296
+ } catch (error) {
297
+ this.logger?.error('Failed to select device:', error)
298
+ await this.refreshDevices() // Refresh even on error
299
+ return false
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Reset to the default audio input device
305
+ * @returns Promise resolving to a boolean indicating success
306
+ */
307
+ async resetToDefaultDevice(): Promise<boolean> {
308
+ try {
309
+ let success = false
310
+ if (Platform.OS === 'web') {
311
+ this.currentDeviceId = 'default'
312
+ success = true
313
+ } else if (AudioStudioModule.resetToDefaultDevice) {
314
+ success = await AudioStudioModule.resetToDefaultDevice()
315
+ if (success) {
316
+ this.currentDeviceId = null
317
+ }
318
+ }
319
+ // Refresh devices after reset attempt
320
+ await this.refreshDevices()
321
+ return success
322
+ } catch (error) {
323
+ this.logger?.error('Failed to reset to default device:', error)
324
+ await this.refreshDevices() // Refresh even on error
325
+ return false
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Register a listener for device changes
331
+ * @param listener Function to call when devices change (receives AudioDevice[])
332
+ * @returns Function to remove the listener
333
+ */
334
+ addDeviceChangeListener(
335
+ listener: (devices: AudioDevice[]) => void
336
+ ): () => void {
337
+ this.deviceChangeListeners.add(listener)
338
+
339
+ // Immediately call listener with current devices if available
340
+ if (this.availableDevices.length > 0) {
341
+ listener([...this.availableDevices])
342
+ }
343
+
344
+ // Return a function to remove the listener
345
+ return () => {
346
+ this.deviceChangeListeners.delete(listener)
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Mark a device as temporarily disconnected (for UI filtering)
352
+ * @param deviceId The ID of the device that was disconnected
353
+ * @param notify Whether to notify listeners immediately (default: true)
354
+ */
355
+ markDeviceAsDisconnected(deviceId: string, notify: boolean = true): void {
356
+ this.logger?.debug(
357
+ `Marking device ${deviceId} as temporarily disconnected`
358
+ )
359
+
360
+ // Clear any existing timeout for this device
361
+ const existingTimeout = this.disconnectionTimeouts.get(deviceId)
362
+ if (existingTimeout) {
363
+ clearTimeout(existingTimeout)
364
+ }
365
+
366
+ // Add to disconnected set
367
+ this.temporarilyDisconnectedDevices.add(deviceId)
368
+
369
+ // Set timeout to remove from disconnected set
370
+ const timeout = setTimeout(() => {
371
+ this.logger?.debug(
372
+ `Reconnection timeout expired for device ${deviceId}`
373
+ )
374
+ this.temporarilyDisconnectedDevices.delete(deviceId)
375
+ this.disconnectionTimeouts.delete(deviceId)
376
+ // Refresh devices to show the device again if it's still available
377
+ this.forceRefreshDevices()
378
+ }, this.DISCONNECTION_TIMEOUT_MS)
379
+
380
+ this.disconnectionTimeouts.set(deviceId, timeout)
381
+
382
+ // Only notify listeners if requested
383
+ if (notify) {
384
+ this.notifyListeners()
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Mark a device as reconnected (remove from disconnected set)
390
+ * @param deviceId The ID of the device that was reconnected
391
+ */
392
+ markDeviceAsReconnected(deviceId: string): void {
393
+ this.logger?.debug(`Marking device ${deviceId} as reconnected`)
394
+
395
+ // Clear timeout and remove from disconnected set
396
+ const timeout = this.disconnectionTimeouts.get(deviceId)
397
+ if (timeout) {
398
+ clearTimeout(timeout)
399
+ this.disconnectionTimeouts.delete(deviceId)
400
+ }
401
+
402
+ this.temporarilyDisconnectedDevices.delete(deviceId)
403
+
404
+ // Notify listeners with updated device list
405
+ this.notifyListeners()
406
+ }
407
+
408
+ /**
409
+ * Get filtered device list (excluding temporarily disconnected devices)
410
+ * @returns Array of available devices excluding temporarily disconnected ones
411
+ */
412
+ private getFilteredDevices(): AudioDevice[] {
413
+ if (this.temporarilyDisconnectedDevices.size === 0) {
414
+ return [...this.availableDevices]
415
+ }
416
+
417
+ const filtered = this.availableDevices.filter(
418
+ (device) => !this.temporarilyDisconnectedDevices.has(device.id)
419
+ )
420
+
421
+ this.logger?.debug(
422
+ `Filtered ${this.availableDevices.length - filtered.length} temporarily disconnected devices. ` +
423
+ `Showing ${filtered.length} devices.`
424
+ )
425
+
426
+ return filtered
427
+ }
428
+
429
+ /**
430
+ * Get the raw device list (including temporarily disconnected devices)
431
+ * @returns Array of all available devices from native layer
432
+ */
433
+ getRawDevices(): AudioDevice[] {
434
+ return [...this.availableDevices]
435
+ }
436
+
437
+ /**
438
+ * Get the IDs of temporarily disconnected devices
439
+ * @returns Set of device IDs that are temporarily hidden from UI
440
+ */
441
+ getTemporarilyDisconnectedDeviceIds(): ReadonlySet<string> {
442
+ return new Set(this.temporarilyDisconnectedDevices)
443
+ }
444
+
445
+ /**
446
+ * Clean up timeouts and listeners (useful for testing or cleanup)
447
+ */
448
+ cleanup(): void {
449
+ // Clear all disconnection timeouts
450
+ this.disconnectionTimeouts.forEach((timeout) => clearTimeout(timeout))
451
+ this.disconnectionTimeouts.clear()
452
+ this.temporarilyDisconnectedDevices.clear()
453
+
454
+ // Clear device change listeners
455
+ this.deviceChangeListeners.clear()
456
+
457
+ // Clean up web device listener
458
+ if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
459
+ if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
460
+ navigator.mediaDevices.removeEventListener(
461
+ 'devicechange',
462
+ this.webDeviceChangeHandler
463
+ )
464
+ }
465
+ this.webDeviceChangeHandler = undefined
466
+ }
467
+
468
+ this.logger?.debug('AudioDeviceManager cleanup completed')
469
+ }
470
+
471
+ /**
472
+ * Force refresh devices without debouncing (for device events)
473
+ * @returns Promise resolving to the updated device list (AudioDevice[])
474
+ */
475
+ async forceRefreshDevices(): Promise<AudioDevice[]> {
476
+ this.logger?.debug('Force refreshing devices (bypassing debounce)...')
477
+ this.refreshInProgress = true
478
+ try {
479
+ // Force fetch the latest devices from native layer
480
+ const devices = await this.getAvailableDevices({ refresh: true })
481
+ // Update internal state
482
+ this.availableDevices = devices
483
+ // Notify listeners with fresh data
484
+ this.notifyListeners()
485
+ this.lastRefreshTime = Date.now()
486
+ return devices
487
+ } catch (error) {
488
+ this.logger?.error('Error during forceRefreshDevices:', error)
489
+ return this.availableDevices
490
+ } finally {
491
+ this.refreshInProgress = false
492
+ this.logger?.debug('Force refresh finished.')
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Refresh the list of available devices with debouncing and notify listeners.
498
+ * @returns Promise resolving to the updated device list (AudioDevice[])
499
+ */
500
+ async refreshDevices(): Promise<AudioDevice[]> {
501
+ const now = Date.now()
502
+
503
+ if (this.refreshInProgress) {
504
+ this.logger?.debug('Refresh already in progress, skipping')
505
+ return this.availableDevices
506
+ }
507
+
508
+ // Always allow refresh if forced by native event or longer than 2s debounce
509
+ const timeSinceLastRefresh = now - this.lastRefreshTime
510
+ const shouldDebounce =
511
+ timeSinceLastRefresh < this.refreshDebounceMs &&
512
+ timeSinceLastRefresh < 2000
513
+
514
+ if (shouldDebounce) {
515
+ this.logger?.debug(
516
+ `Refresh debounced, skipping (last refresh was ${timeSinceLastRefresh}ms ago)`
517
+ )
518
+ return this.availableDevices
519
+ }
520
+
521
+ this.logger?.debug('Refreshing devices...')
522
+ this.refreshInProgress = true
523
+ try {
524
+ // Fetch the latest devices; getAvailableDevices handles mapping now
525
+ const devices = await this.getAvailableDevices({ refresh: true })
526
+ // availableDevices state is updated within getAvailableDevices
527
+ this.notifyListeners() // Notify listeners with the updated list
528
+ this.lastRefreshTime = Date.now()
529
+ return devices // Return the fetched & mapped list
530
+ } catch (error) {
531
+ this.logger?.error('Error during refreshDevices:', error)
532
+ return this.availableDevices // Return potentially stale list on error
533
+ } finally {
534
+ this.refreshInProgress = false
535
+ this.logger?.debug('Refresh finished.')
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Get audio input devices using the Web Audio API
541
+ * @returns Promise resolving to an array of AudioDevice objects
542
+ */
543
+ private async getWebAudioDevices(): Promise<AudioDevice[]> {
544
+ if (
545
+ typeof navigator === 'undefined' ||
546
+ !navigator.mediaDevices ||
547
+ !navigator.mediaDevices.enumerateDevices
548
+ ) {
549
+ return [DEFAULT_DEVICE]
550
+ }
551
+
552
+ try {
553
+ const permissionStatus = await this.checkMicrophonePermission()
554
+
555
+ if (permissionStatus === 'denied') {
556
+ return [
557
+ {
558
+ ...DEFAULT_DEVICE,
559
+ name: 'Microphone Access Denied',
560
+ isAvailable: false,
561
+ },
562
+ ]
563
+ }
564
+
565
+ if (permissionStatus !== 'granted') {
566
+ try {
567
+ // Requesting stream often reveals device labels
568
+ await navigator.mediaDevices.getUserMedia({ audio: true })
569
+ } catch (error) {
570
+ this.logger?.warn(
571
+ 'Microphone permission request failed:',
572
+ error
573
+ )
574
+ return [
575
+ {
576
+ ...DEFAULT_DEVICE,
577
+ name: 'Microphone Access Required',
578
+ isAvailable: false,
579
+ },
580
+ ]
581
+ }
582
+ }
583
+
584
+ const devices = await navigator.mediaDevices.enumerateDevices()
585
+ const audioInputDevices = devices
586
+ .filter((device) => device.kind === 'audioinput')
587
+ .map((device) => this.mapWebDeviceToAudioDevice(device))
588
+
589
+ const hasUnlabeledDevices = audioInputDevices.some(
590
+ (device) =>
591
+ !device.name || device.name.startsWith('Microphone ')
592
+ )
593
+
594
+ let finalDevices = audioInputDevices
595
+ if (hasUnlabeledDevices && this.isSafariOrIOS()) {
596
+ finalDevices = this.enhanceDevicesForSafari(audioInputDevices)
597
+ }
598
+
599
+ if (finalDevices.length === 0) {
600
+ finalDevices = [DEFAULT_DEVICE]
601
+ }
602
+
603
+ this.availableDevices = finalDevices // Update internal state
604
+ return finalDevices
605
+ } catch (error) {
606
+ this.logger?.error('Failed to enumerate web audio devices:', error)
607
+ this.availableDevices = [DEFAULT_DEVICE] // Update state on error
608
+ return this.availableDevices
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Check the current microphone permission status
614
+ * @returns Permission state ('prompt', 'granted', or 'denied')
615
+ */
616
+ private async checkMicrophonePermission(): Promise<PermissionState> {
617
+ if (!navigator.permissions || !navigator.permissions.query) {
618
+ return 'prompt'
619
+ }
620
+ try {
621
+ const permissionStatus = await navigator.permissions.query({
622
+ name: 'microphone' as PermissionName,
623
+ })
624
+ permissionStatus.onchange = () => {
625
+ // Refresh devices when permission changes
626
+ this.refreshDevices()
627
+ }
628
+ return permissionStatus.state
629
+ } catch (error) {
630
+ this.logger?.warn('Permission query not supported:', error)
631
+ return 'prompt'
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Setup listener for device changes in web environment
637
+ */
638
+ private setupWebDeviceChangeListener(): void {
639
+ if (
640
+ typeof navigator === 'undefined' ||
641
+ !navigator.mediaDevices ||
642
+ this.webDeviceChangeHandler // Avoid adding multiple listeners
643
+ ) {
644
+ this.logger?.debug(
645
+ 'Web device change listener not available or already set up'
646
+ )
647
+ return
648
+ }
649
+
650
+ try {
651
+ this.webDeviceChangeHandler = () => {
652
+ this.logger?.debug(
653
+ 'Web device change detected, refreshing device list'
654
+ )
655
+ // Force refresh to get immediate updates
656
+ this.forceRefreshDevices()
657
+ }
658
+
659
+ navigator.mediaDevices.addEventListener(
660
+ 'devicechange',
661
+ this.webDeviceChangeHandler
662
+ )
663
+ this.logger?.debug('Web device change listener successfully set up')
664
+ } catch (error) {
665
+ this.logger?.warn(
666
+ 'Failed to set up web device change listener:',
667
+ error
668
+ )
669
+ this.webDeviceChangeHandler = undefined
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Check if the current browser is Safari or iOS WebKit
675
+ */
676
+ private isSafariOrIOS(): boolean {
677
+ if (typeof navigator === 'undefined') return false
678
+ const ua = navigator.userAgent
679
+ return (
680
+ /^((?!chrome|android).)*safari/i.test(ua) ||
681
+ /iPad|iPhone|iPod/.test(ua) ||
682
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
683
+ )
684
+ }
685
+
686
+ /**
687
+ * Create enhanced device information for Safari and privacy-restricted browsers
688
+ * @param devices Array of AudioDevice objects, potentially unlabeled
689
+ * @returns Array of enhanced AudioDevice objects
690
+ */
691
+ private enhanceDevicesForSafari(devices: AudioDevice[]): AudioDevice[] {
692
+ const defaultDevice = devices.find((d) => d.isDefault)
693
+
694
+ if (devices.length <= 1) {
695
+ // Return a typed default device
696
+ return [
697
+ {
698
+ id: defaultDevice?.id || 'default',
699
+ name: 'Microphone (Browser Managed)',
700
+ type: 'builtin_mic',
701
+ isDefault: true,
702
+ isAvailable: true,
703
+ capabilities:
704
+ defaultDevice?.capabilities ||
705
+ DEFAULT_DEVICE.capabilities,
706
+ },
707
+ ]
708
+ }
709
+
710
+ // Provide more descriptive names for unlabeled devices
711
+ return devices.map((device, index) => {
712
+ if (!device.name || device.name.startsWith('Microphone ')) {
713
+ const deviceTypes = [
714
+ 'Built-in Microphone',
715
+ 'External Microphone',
716
+ 'Headset Microphone',
717
+ ]
718
+ const typeName = deviceTypes[index % deviceTypes.length]
719
+ return {
720
+ ...device,
721
+ name: device.isDefault ? `${typeName} (Default)` : typeName,
722
+ }
723
+ }
724
+ return device
725
+ })
726
+ }
727
+
728
+ /**
729
+ * Map a Web MediaDeviceInfo to our AudioDevice format
730
+ * @param device The MediaDeviceInfo object from the browser
731
+ * @returns An object conforming to the AudioDevice interface
732
+ */
733
+ private mapWebDeviceToAudioDevice(device: MediaDeviceInfo): AudioDevice {
734
+ const isDefault = device.deviceId === 'default'
735
+ const deviceType = this.inferDeviceType(device.label || '')
736
+
737
+ // Provide reasonable default capabilities for web devices
738
+ const defaultWebCapabilities: AudioDeviceCapabilities = {
739
+ sampleRates: [16000, 44100, 48000],
740
+ channelCounts: [1, 2],
741
+ bitDepths: [16, 32], // Web Audio uses float32, common PCM might be 16/32
742
+ hasEchoCancellation: true, // Often handled by browser
743
+ hasNoiseSuppression: true, // Often handled by browser
744
+ hasAutomaticGainControl: true, // Often handled by browser
745
+ }
746
+
747
+ return {
748
+ id: device.deviceId,
749
+ name:
750
+ device.label || `Microphone ${device.deviceId.substring(0, 8)}`,
751
+ type: deviceType,
752
+ isDefault,
753
+ isAvailable: true, // Assume available if enumerated
754
+ capabilities: defaultWebCapabilities,
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Try to infer the device type from its name
760
+ * @param deviceName The label of the device
761
+ * @returns A string representing the inferred device type
762
+ */
763
+ private inferDeviceType(deviceName: string): string {
764
+ const name = deviceName.toLowerCase()
765
+ if (name.includes('bluetooth') || name.includes('airpods'))
766
+ return 'bluetooth'
767
+ if (name.includes('usb')) return 'usb'
768
+ if (name.includes('headphone') || name.includes('headset')) {
769
+ return name.includes('wired') ? 'wired_headset' : 'wired_headphones'
770
+ }
771
+ if (name.includes('speaker')) return 'speaker'
772
+ return 'builtin_mic' // Default assumption
773
+ }
774
+
775
+ /**
776
+ * Notify all registered listeners about device changes.
777
+ */
778
+ notifyListeners(): void {
779
+ // Pass a copy of the filtered devices array to listeners
780
+ const devicesCopy = this.getFilteredDevices()
781
+
782
+ this.logger?.debug(
783
+ `Notifying ${this.deviceChangeListeners.size} listeners with ${devicesCopy.length} devices ` +
784
+ `(${this.temporarilyDisconnectedDevices.size} temporarily hidden)`
785
+ )
786
+
787
+ this.deviceChangeListeners.forEach((listener) => {
788
+ try {
789
+ listener(devicesCopy)
790
+ } catch (error) {
791
+ this.logger?.error('Error in device change listener:', error)
792
+ }
793
+ })
794
+ }
795
+ }
796
+
797
+ // Create and export the singleton instance
798
+ export const audioDeviceManager = new AudioDeviceManager()
799
+
800
+ export { DeviceDisconnectionBehavior }