@siteed/expo-audio-studio 2.18.6 → 3.0.1

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 (329) hide show
  1. package/README.md +13 -297
  2. package/index.d.ts +1 -0
  3. package/index.js +1 -0
  4. package/package.json +7 -136
  5. package/CHANGELOG.md +0 -501
  6. package/LICENSE +0 -21
  7. package/android/build.gradle +0 -129
  8. package/android/src/androidTest/assets/chorus.wav +0 -0
  9. package/android/src/androidTest/assets/jfk.wav +0 -0
  10. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  11. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  12. package/android/src/androidTest/java/net/siteed/audiostream/AudioProcessorInstrumentedTest.kt +0 -197
  13. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +0 -541
  14. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderPerformanceInstrumentedTest.kt +0 -234
  15. package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  16. package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +0 -324
  17. package/android/src/androidTest/java/net/siteed/audiostream/integration/CompressedOnlyOutputTest.kt +0 -253
  18. package/android/src/androidTest/java/net/siteed/audiostream/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  19. package/android/src/androidTest/java/net/siteed/audiostream/integration/EventEmissionIntervalTest.kt +0 -120
  20. package/android/src/androidTest/java/net/siteed/audiostream/integration/M4aFormatTest.kt +0 -345
  21. package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +0 -340
  22. package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt +0 -252
  23. package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +0 -95
  24. package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +0 -43
  25. package/android/src/main/AndroidManifest.xml +0 -30
  26. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -188
  27. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  28. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +0 -1741
  29. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -136
  30. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -354
  31. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -439
  32. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2237
  33. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -2141
  34. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -167
  35. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  36. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -37
  37. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  38. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -1113
  39. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  40. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  41. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +0 -93
  42. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -72
  43. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -68
  44. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  45. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -257
  46. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  47. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  48. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  49. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  50. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  51. package/android/src/main/res/drawable/ic_play.xml +0 -10
  52. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  53. package/android/src/main/res/layout/notification_recording.xml +0 -37
  54. package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +0 -279
  55. package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +0 -249
  56. package/android/src/test/java/net/siteed/audiostream/AudioFormatTest.kt +0 -151
  57. package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +0 -273
  58. package/android/src/test/java/net/siteed/audiostream/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  59. package/android/src/test/resources/chorus.wav +0 -0
  60. package/android/src/test/resources/generate_test_audio.py +0 -94
  61. package/android/src/test/resources/jfk.wav +0 -0
  62. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  63. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  64. package/app.plugin.js +0 -3
  65. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js +0 -4
  66. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  67. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +0 -210
  68. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  69. package/build/cjs/AudioAnalysis/extractAudioData.js +0 -21
  70. package/build/cjs/AudioAnalysis/extractAudioData.js.map +0 -1
  71. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +0 -92
  72. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  73. package/build/cjs/AudioAnalysis/extractPreview.js +0 -28
  74. package/build/cjs/AudioAnalysis/extractPreview.js.map +0 -1
  75. package/build/cjs/AudioAnalysis/extractWaveform.js +0 -18
  76. package/build/cjs/AudioAnalysis/extractWaveform.js.map +0 -1
  77. package/build/cjs/AudioDeviceManager.js +0 -689
  78. package/build/cjs/AudioDeviceManager.js.map +0 -1
  79. package/build/cjs/AudioRecorder.provider.js +0 -78
  80. package/build/cjs/AudioRecorder.provider.js.map +0 -1
  81. package/build/cjs/ExpoAudioStream.native.js +0 -8
  82. package/build/cjs/ExpoAudioStream.native.js.map +0 -1
  83. package/build/cjs/ExpoAudioStream.types.js +0 -11
  84. package/build/cjs/ExpoAudioStream.types.js.map +0 -1
  85. package/build/cjs/ExpoAudioStream.web.js +0 -708
  86. package/build/cjs/ExpoAudioStream.web.js.map +0 -1
  87. package/build/cjs/ExpoAudioStreamModule.js +0 -718
  88. package/build/cjs/ExpoAudioStreamModule.js.map +0 -1
  89. package/build/cjs/WebRecorder.web.js +0 -777
  90. package/build/cjs/WebRecorder.web.js.map +0 -1
  91. package/build/cjs/constants/platformLimitations.js +0 -99
  92. package/build/cjs/constants/platformLimitations.js.map +0 -1
  93. package/build/cjs/constants.js +0 -17
  94. package/build/cjs/constants.js.map +0 -1
  95. package/build/cjs/events.js +0 -29
  96. package/build/cjs/events.js.map +0 -1
  97. package/build/cjs/hooks/useAudioDevices.js +0 -179
  98. package/build/cjs/hooks/useAudioDevices.js.map +0 -1
  99. package/build/cjs/index.js +0 -58
  100. package/build/cjs/index.js.map +0 -1
  101. package/build/cjs/trimAudio.js +0 -76
  102. package/build/cjs/trimAudio.js.map +0 -1
  103. package/build/cjs/useAudioRecorder.js +0 -518
  104. package/build/cjs/useAudioRecorder.js.map +0 -1
  105. package/build/cjs/utils/BlobFix.js +0 -502
  106. package/build/cjs/utils/BlobFix.js.map +0 -1
  107. package/build/cjs/utils/audioProcessing.js +0 -136
  108. package/build/cjs/utils/audioProcessing.js.map +0 -1
  109. package/build/cjs/utils/cleanNativeOptions.js +0 -22
  110. package/build/cjs/utils/cleanNativeOptions.js.map +0 -1
  111. package/build/cjs/utils/concatenateBuffers.js +0 -25
  112. package/build/cjs/utils/concatenateBuffers.js.map +0 -1
  113. package/build/cjs/utils/convertPCMToFloat32.js +0 -124
  114. package/build/cjs/utils/convertPCMToFloat32.js.map +0 -1
  115. package/build/cjs/utils/crc32.js +0 -52
  116. package/build/cjs/utils/crc32.js.map +0 -1
  117. package/build/cjs/utils/encodingToBitDepth.js +0 -17
  118. package/build/cjs/utils/encodingToBitDepth.js.map +0 -1
  119. package/build/cjs/utils/getWavFileInfo.js +0 -96
  120. package/build/cjs/utils/getWavFileInfo.js.map +0 -1
  121. package/build/cjs/utils/writeWavHeader.js +0 -88
  122. package/build/cjs/utils/writeWavHeader.js.map +0 -1
  123. package/build/cjs/workers/InlineFeaturesExtractor.web.js +0 -859
  124. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +0 -1
  125. package/build/cjs/workers/inlineAudioWebWorker.web.js +0 -184
  126. package/build/cjs/workers/inlineAudioWebWorker.web.js.map +0 -1
  127. package/build/esm/AudioAnalysis/AudioAnalysis.types.js +0 -3
  128. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  129. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +0 -202
  130. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  131. package/build/esm/AudioAnalysis/extractAudioData.js +0 -14
  132. package/build/esm/AudioAnalysis/extractAudioData.js.map +0 -1
  133. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +0 -89
  134. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  135. package/build/esm/AudioAnalysis/extractPreview.js +0 -25
  136. package/build/esm/AudioAnalysis/extractPreview.js.map +0 -1
  137. package/build/esm/AudioAnalysis/extractWaveform.js +0 -11
  138. package/build/esm/AudioAnalysis/extractWaveform.js.map +0 -1
  139. package/build/esm/AudioDeviceManager.js +0 -682
  140. package/build/esm/AudioDeviceManager.js.map +0 -1
  141. package/build/esm/AudioRecorder.provider.js +0 -40
  142. package/build/esm/AudioRecorder.provider.js.map +0 -1
  143. package/build/esm/ExpoAudioStream.native.js +0 -6
  144. package/build/esm/ExpoAudioStream.native.js.map +0 -1
  145. package/build/esm/ExpoAudioStream.types.js +0 -8
  146. package/build/esm/ExpoAudioStream.types.js.map +0 -1
  147. package/build/esm/ExpoAudioStream.web.js +0 -704
  148. package/build/esm/ExpoAudioStream.web.js.map +0 -1
  149. package/build/esm/ExpoAudioStreamModule.js +0 -713
  150. package/build/esm/ExpoAudioStreamModule.js.map +0 -1
  151. package/build/esm/WebRecorder.web.js +0 -773
  152. package/build/esm/WebRecorder.web.js.map +0 -1
  153. package/build/esm/constants/platformLimitations.js +0 -90
  154. package/build/esm/constants/platformLimitations.js.map +0 -1
  155. package/build/esm/constants.js +0 -14
  156. package/build/esm/constants.js.map +0 -1
  157. package/build/esm/events.js +0 -21
  158. package/build/esm/events.js.map +0 -1
  159. package/build/esm/hooks/useAudioDevices.js +0 -176
  160. package/build/esm/hooks/useAudioDevices.js.map +0 -1
  161. package/build/esm/index.js +0 -20
  162. package/build/esm/index.js.map +0 -1
  163. package/build/esm/trimAudio.js +0 -69
  164. package/build/esm/trimAudio.js.map +0 -1
  165. package/build/esm/useAudioRecorder.js +0 -512
  166. package/build/esm/useAudioRecorder.js.map +0 -1
  167. package/build/esm/utils/BlobFix.js +0 -498
  168. package/build/esm/utils/BlobFix.js.map +0 -1
  169. package/build/esm/utils/audioProcessing.js +0 -133
  170. package/build/esm/utils/audioProcessing.js.map +0 -1
  171. package/build/esm/utils/cleanNativeOptions.js +0 -19
  172. package/build/esm/utils/cleanNativeOptions.js.map +0 -1
  173. package/build/esm/utils/concatenateBuffers.js +0 -21
  174. package/build/esm/utils/concatenateBuffers.js.map +0 -1
  175. package/build/esm/utils/convertPCMToFloat32.js +0 -120
  176. package/build/esm/utils/convertPCMToFloat32.js.map +0 -1
  177. package/build/esm/utils/crc32.js +0 -50
  178. package/build/esm/utils/crc32.js.map +0 -1
  179. package/build/esm/utils/encodingToBitDepth.js +0 -13
  180. package/build/esm/utils/encodingToBitDepth.js.map +0 -1
  181. package/build/esm/utils/getWavFileInfo.js +0 -92
  182. package/build/esm/utils/getWavFileInfo.js.map +0 -1
  183. package/build/esm/utils/writeWavHeader.js +0 -84
  184. package/build/esm/utils/writeWavHeader.js.map +0 -1
  185. package/build/esm/workers/InlineFeaturesExtractor.web.js +0 -856
  186. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +0 -1
  187. package/build/esm/workers/inlineAudioWebWorker.web.js +0 -181
  188. package/build/esm/workers/inlineAudioWebWorker.web.js.map +0 -1
  189. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +0 -196
  190. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  191. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts +0 -74
  192. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  193. package/build/types/AudioAnalysis/extractAudioData.d.ts +0 -3
  194. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  195. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  196. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  197. package/build/types/AudioAnalysis/extractPreview.d.ts +0 -11
  198. package/build/types/AudioAnalysis/extractPreview.d.ts.map +0 -1
  199. package/build/types/AudioAnalysis/extractWaveform.d.ts +0 -8
  200. package/build/types/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  201. package/build/types/AudioDeviceManager.d.ts +0 -187
  202. package/build/types/AudioDeviceManager.d.ts.map +0 -1
  203. package/build/types/AudioRecorder.provider.d.ts +0 -11
  204. package/build/types/AudioRecorder.provider.d.ts.map +0 -1
  205. package/build/types/ExpoAudioStream.native.d.ts +0 -3
  206. package/build/types/ExpoAudioStream.native.d.ts.map +0 -1
  207. package/build/types/ExpoAudioStream.types.d.ts +0 -738
  208. package/build/types/ExpoAudioStream.types.d.ts.map +0 -1
  209. package/build/types/ExpoAudioStream.web.d.ts +0 -96
  210. package/build/types/ExpoAudioStream.web.d.ts.map +0 -1
  211. package/build/types/ExpoAudioStreamModule.d.ts +0 -3
  212. package/build/types/ExpoAudioStreamModule.d.ts.map +0 -1
  213. package/build/types/WebRecorder.web.d.ts +0 -198
  214. package/build/types/WebRecorder.web.d.ts.map +0 -1
  215. package/build/types/constants/platformLimitations.d.ts +0 -40
  216. package/build/types/constants/platformLimitations.d.ts.map +0 -1
  217. package/build/types/constants.d.ts +0 -11
  218. package/build/types/constants.d.ts.map +0 -1
  219. package/build/types/events.d.ts +0 -26
  220. package/build/types/events.d.ts.map +0 -1
  221. package/build/types/hooks/useAudioDevices.d.ts +0 -15
  222. package/build/types/hooks/useAudioDevices.d.ts.map +0 -1
  223. package/build/types/index.d.ts +0 -18
  224. package/build/types/index.d.ts.map +0 -1
  225. package/build/types/trimAudio.d.ts +0 -25
  226. package/build/types/trimAudio.d.ts.map +0 -1
  227. package/build/types/useAudioRecorder.d.ts +0 -22
  228. package/build/types/useAudioRecorder.d.ts.map +0 -1
  229. package/build/types/utils/BlobFix.d.ts +0 -9
  230. package/build/types/utils/BlobFix.d.ts.map +0 -1
  231. package/build/types/utils/audioProcessing.d.ts +0 -24
  232. package/build/types/utils/audioProcessing.d.ts.map +0 -1
  233. package/build/types/utils/cleanNativeOptions.d.ts +0 -15
  234. package/build/types/utils/cleanNativeOptions.d.ts.map +0 -1
  235. package/build/types/utils/concatenateBuffers.d.ts +0 -8
  236. package/build/types/utils/concatenateBuffers.d.ts.map +0 -1
  237. package/build/types/utils/convertPCMToFloat32.d.ts +0 -13
  238. package/build/types/utils/convertPCMToFloat32.d.ts.map +0 -1
  239. package/build/types/utils/crc32.d.ts +0 -7
  240. package/build/types/utils/crc32.d.ts.map +0 -1
  241. package/build/types/utils/encodingToBitDepth.d.ts +0 -5
  242. package/build/types/utils/encodingToBitDepth.d.ts.map +0 -1
  243. package/build/types/utils/getWavFileInfo.d.ts +0 -26
  244. package/build/types/utils/getWavFileInfo.d.ts.map +0 -1
  245. package/build/types/utils/writeWavHeader.d.ts +0 -34
  246. package/build/types/utils/writeWavHeader.d.ts.map +0 -1
  247. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  248. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  249. package/build/types/workers/inlineAudioWebWorker.web.d.ts +0 -2
  250. package/build/types/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  251. package/expo-module.config.json +0 -10
  252. package/ios/AudioAnalysisData.swift +0 -74
  253. package/ios/AudioDeviceManager.swift +0 -670
  254. package/ios/AudioNotificationManager.swift +0 -154
  255. package/ios/AudioProcessingHelpers.swift +0 -743
  256. package/ios/AudioProcessor.swift +0 -1151
  257. package/ios/AudioStreamError.swift +0 -7
  258. package/ios/AudioStreamManager.swift +0 -2369
  259. package/ios/AudioStreamManagerDelegate.swift +0 -16
  260. package/ios/DataPoint.swift +0 -54
  261. package/ios/DecodingConfig.swift +0 -59
  262. package/ios/ExpoAudioStream.podspec +0 -33
  263. package/ios/ExpoAudioStreamModule.swift +0 -1019
  264. package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +0 -338
  265. package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  266. package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +0 -130
  267. package/ios/ExpoAudioStudioTests/CompressedOnlyOutputTests.swift +0 -294
  268. package/ios/ExpoAudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  269. package/ios/ExpoAudioStudioTests/Info.plist +0 -22
  270. package/ios/ExpoAudioStudioTests/README.md +0 -39
  271. package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +0 -98
  272. package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +0 -75
  273. package/ios/FFT.swift +0 -62
  274. package/ios/Features.swift +0 -95
  275. package/ios/ISSUE_IOS.md +0 -68
  276. package/ios/Logger.swift +0 -39
  277. package/ios/NotificationExtension.swift +0 -15
  278. package/ios/RecordingResult.swift +0 -22
  279. package/ios/RecordingSettings.swift +0 -308
  280. package/ios/WaveformExtractor.swift +0 -105
  281. package/ios/tests/README.md +0 -41
  282. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  283. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  284. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  285. package/ios/tests/integration/output_control_test.swift +0 -322
  286. package/ios/tests/integration/run_integration_tests.sh +0 -37
  287. package/ios/tests/opus_support_test_macos.swift +0 -154
  288. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  289. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  290. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  291. package/ios/tests/standalone/standalone_test.swift +0 -144
  292. package/plugin/build/index.cjs +0 -194
  293. package/plugin/build/index.d.cts +0 -22
  294. package/plugin/build/index.js +0 -194
  295. package/plugin/src/index.ts +0 -285
  296. package/plugin/tsconfig.json +0 -10
  297. package/plugin/tsconfig.tsbuildinfo +0 -1
  298. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -224
  299. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -344
  300. package/src/AudioAnalysis/extractAudioData.ts +0 -17
  301. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -154
  302. package/src/AudioAnalysis/extractPreview.ts +0 -34
  303. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  304. package/src/AudioDeviceManager.ts +0 -803
  305. package/src/AudioRecorder.provider.tsx +0 -57
  306. package/src/ExpoAudioStream.native.ts +0 -6
  307. package/src/ExpoAudioStream.types.ts +0 -874
  308. package/src/ExpoAudioStream.web.ts +0 -905
  309. package/src/ExpoAudioStreamModule.ts +0 -990
  310. package/src/WebRecorder.web.ts +0 -1005
  311. package/src/constants/platformLimitations.ts +0 -118
  312. package/src/constants.ts +0 -18
  313. package/src/events.ts +0 -60
  314. package/src/hooks/useAudioDevices.ts +0 -213
  315. package/src/index.ts +0 -54
  316. package/src/trimAudio.ts +0 -94
  317. package/src/types/crc-32.d.ts +0 -9
  318. package/src/useAudioRecorder.tsx +0 -766
  319. package/src/utils/BlobFix.ts +0 -561
  320. package/src/utils/audioProcessing.ts +0 -205
  321. package/src/utils/cleanNativeOptions.ts +0 -18
  322. package/src/utils/concatenateBuffers.ts +0 -24
  323. package/src/utils/convertPCMToFloat32.ts +0 -170
  324. package/src/utils/crc32.ts +0 -59
  325. package/src/utils/encodingToBitDepth.ts +0 -18
  326. package/src/utils/getWavFileInfo.ts +0 -132
  327. package/src/utils/writeWavHeader.ts +0 -115
  328. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -855
  329. package/src/workers/inlineAudioWebWorker.web.tsx +0 -180
@@ -1,1019 +0,0 @@
1
- // packages/expo-audio-stream/ios/ExpoAudioStreamModule.swift
2
- import ExpoModulesCore
3
- import AVFoundation
4
-
5
- // Constants
6
- private let audioDataEvent: String = "AudioData"
7
- private let audioAnalysisEvent: String = "AudioAnalysis"
8
- private let recordingInterruptedEvent: String = "onRecordingInterrupted"
9
- private let deviceChangedEvent: String = "deviceChangedEvent"
10
- private let trimProgressEvent: String = "TrimProgress"
11
- private let errorEvent: String = "error"
12
- private let DEFAULT_SEGMENT_DURATION_MS = 100
13
- private let audioDeviceTypeBuiltinMic = "builtin_mic"
14
- private let audioDeviceTypeBluetooth = "bluetooth"
15
- private let audioDeviceTypeUSB = "usb"
16
- private let audioDeviceTypeWiredHeadset = "wired_headset"
17
- private let audioDeviceTypeWiredHeadphones = "wired_headphones"
18
- private let audioDeviceTypeSpeaker = "speaker"
19
- private let audioDeviceTypeUnknown = "unknown"
20
-
21
- public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDeviceManagerDelegate {
22
- private var streamManager = AudioStreamManager()
23
- private let notificationCenter = UNUserNotificationCenter.current()
24
- private let notificationIdentifier = "audio_recording_notification"
25
- private var deviceManager = AudioDeviceManager()
26
- private var deviceChangeObserver: Any?
27
-
28
- public func definition() -> ModuleDefinition {
29
- Name("ExpoAudioStream")
30
-
31
- // Defines event names that the module can send to JavaScript.
32
- Events([
33
- audioDataEvent,
34
- audioAnalysisEvent,
35
- recordingInterruptedEvent,
36
- deviceChangedEvent,
37
- trimProgressEvent,
38
- errorEvent
39
- ])
40
-
41
- OnCreate {
42
- Logger.debug("ExpoAudioStreamModule", "Module created, setting delegate and starting device monitoring.")
43
- streamManager.delegate = self
44
- // Set up device manager delegate to emit device change events
45
- deviceManager.delegate = self
46
-
47
- // Listen for device connection notifications (minimal addition)
48
- NotificationCenter.default.addObserver(
49
- forName: NSNotification.Name("DeviceConnected"),
50
- object: nil,
51
- queue: .main
52
- ) { [weak self] notification in
53
- if let deviceId = notification.userInfo?["deviceId"] as? String {
54
- Logger.debug("ExpoAudioStreamModule", "Device connected: \(deviceId)")
55
- self?.sendEvent(deviceChangedEvent, [
56
- "type": "deviceConnected",
57
- "deviceId": deviceId
58
- ])
59
- }
60
- }
61
- }
62
-
63
- OnDestroy {
64
- Logger.debug("ExpoAudioStreamModule", "Module destroyed, stopping device monitoring.")
65
- _ = streamManager.stopRecording()
66
- // Clear device manager delegate
67
- deviceManager.delegate = nil
68
- }
69
-
70
- /// Extracts audio analysis data from an audio file.
71
- ///
72
- /// - Parameters:
73
- /// - options: A dictionary containing:
74
- /// - `fileUri`: The URI of the audio file.
75
- /// - `pointsPerSecond`: The number of data points to extract per second of audio.
76
- /// - `features`: A dictionary specifying which features to extract (e.g., `energy`, `mfcc`, `rms`, etc.).
77
- /// - promise: A promise to resolve with the extracted audio analysis data or reject with an error.
78
- /// - Returns: Promise to be resolved with audio analysis data.
79
- AsyncFunction("extractAudioAnalysis") { (options: [String: Any], promise: Promise) in
80
- Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis called with options: \(options)")
81
- guard let fileUri = options["fileUri"] as? String,
82
- let url = URL(string: fileUri) else {
83
- Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: Invalid file URI.")
84
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
85
- return
86
- }
87
-
88
- // Get time or byte range options
89
- let startTimeMs = options["startTimeMs"] as? Double
90
- let endTimeMs = options["endTimeMs"] as? Double
91
- let position = options["position"] as? Int
92
- let byteLength = options["length"] as? Int
93
-
94
- // Validate ranges - can have time range OR byte range OR no range
95
- let hasTimeRange = startTimeMs != nil && endTimeMs != nil
96
- let hasByteRange = position != nil && byteLength != nil
97
-
98
- // Only throw if both ranges are provided
99
- guard !(hasTimeRange && hasByteRange) else {
100
- promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
101
- return
102
- }
103
-
104
- let features = options["features"] as? [String: Bool] ?? [:]
105
- let featureOptions = self.extractFeatureOptions(from: features)
106
- let segmentDurationMs = options["segmentDurationMs"] as? Int ?? DEFAULT_SEGMENT_DURATION_MS // Default value of 100ms
107
-
108
- DispatchQueue.global().async(execute: {
109
- do {
110
- let audioFile = try AVAudioFile(forReading: url)
111
- let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
112
- let numberOfChannels = Int(audioFile.fileFormat.channelCount)
113
- let sampleRate = audioFile.fileFormat.sampleRate
114
-
115
- // Convert time range to byte range if needed
116
- let effectivePosition: Int?
117
- let effectiveLength: Int?
118
-
119
- if hasTimeRange {
120
- let bytesPerSecond = Int(sampleRate) * numberOfChannels * (bitDepth / 8)
121
- effectivePosition = Int(startTimeMs! * Double(bytesPerSecond) / 1000.0)
122
- effectiveLength = Int((endTimeMs! - startTimeMs!) * Double(bytesPerSecond) / 1000.0)
123
- } else {
124
- effectivePosition = position
125
- effectiveLength = byteLength
126
- }
127
-
128
- Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis: Processing started for \(fileUri)")
129
- let audioProcessor = try AudioProcessor(url: url, resolve: { result in
130
- Logger.warn("ExpoAudioStreamModule", "extractAudioAnalysis: AudioProcessor resolve called unexpectedly.")
131
- }, reject: { code, message in
132
- Logger.warn("ExpoAudioStreamModule", "extractAudioAnalysis: AudioProcessor reject called unexpectedly: \(code) - \(message)")
133
- })
134
-
135
- if let result = audioProcessor.processAudioData(
136
- numberOfSamples: nil,
137
- offset: 0,
138
- length: nil,
139
- segmentDurationMs: segmentDurationMs,
140
- featureOptions: featureOptions,
141
- bitDepth: bitDepth,
142
- numberOfChannels: numberOfChannels,
143
- position: effectivePosition,
144
- byteLength: effectiveLength
145
- ) {
146
- Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis: Processing successful for \(fileUri)")
147
- promise.resolve(result.toDictionary())
148
- } else {
149
- Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: audioProcessor.processAudioData returned nil for \(fileUri)")
150
- promise.reject("PROCESSING_ERROR", "Failed to process audio data")
151
- }
152
- } catch {
153
- Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: Error initializing AudioProcessor for \(fileUri): \(error.localizedDescription)")
154
- promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
155
- }
156
- })
157
- }
158
-
159
-
160
- /// Asynchronously starts audio recording with the given settings.
161
- ///
162
- /// - Parameters:
163
- /// - options: A dictionary containing:
164
- /// - `sampleRate`: The sample rate for recording (default is 16000.0).
165
- /// - `channelConfig`: The number of channels (default is 1 for mono).
166
- /// - `audioFormat`: The bit depth for recording (default is 16 bits).
167
- /// - `interval`: The interval in milliseconds at which to emit recording data (default is 1000 ms).
168
- /// - `intervalAnalysis`: The interval in milliseconds at which to emit analysis data (default is 500 ms).
169
- /// - `enableProcessing`: Boolean to enable/disable audio processing (default is false).
170
- /// - `pointsPerSecond`: The number of data points to extract per second of audio (default is 20).
171
- /// - `algorithm`: The algorithm to use for extraction (default is "rms").
172
- /// - `featureOptions`: A dictionary of feature options to extract (default is empty).
173
- /// - `maxRecentDataDuration`: The maximum duration of recent data to keep for processing (default is 10.0 seconds).
174
- /// - `compression`: A dictionary containing:
175
- /// - `enabled`: Boolean to enable/disable compression (default is false).
176
- /// - `format`: The compression format (default is "aac").
177
- /// - `bitrate`: The compression bitrate in bps (default is 128000).
178
- /// - promise: A promise to resolve with the recording settings or reject with an error.
179
- AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
180
- Logger.debug("ExpoAudioStreamModule", "startRecording called")
181
- self.checkMicrophonePermission { granted in
182
- guard granted else {
183
- Logger.warn("ExpoAudioStreamModule", "startRecording: Permission denied.")
184
- promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
185
- return
186
- }
187
-
188
- // Check if output.compressed is enabled and format is Opus
189
- var modifiedOptions = options
190
- if let output = options["output"] as? [String: Any],
191
- let compressed = output["compressed"] as? [String: Any],
192
- let enabled = compressed["enabled"] as? Bool, enabled,
193
- let format = compressed["format"] as? String,
194
- format.lowercased() == "opus" {
195
-
196
- // Create mutable copies
197
- var modifiedOutput = output
198
- var modifiedCompressed = compressed
199
-
200
- // Change format to AAC and log warning
201
- modifiedCompressed["format"] = "aac"
202
- modifiedOutput["compressed"] = modifiedCompressed
203
- modifiedOptions["output"] = modifiedOutput
204
-
205
- Logger.warn("ExpoAudioStreamModule", "startRecording: Opus format is not supported on iOS. Falling back to AAC format.")
206
- }
207
-
208
- // Create settings with validation using the potentially modified options
209
- let settingsResult = RecordingSettings.fromDictionary(modifiedOptions)
210
-
211
- switch settingsResult {
212
- case .success(let settings):
213
- // Initialize notification if enabled
214
- if settings.showNotification {
215
- Task {
216
- let notificationGranted = await self.requestNotificationPermissions()
217
- if !notificationGranted {
218
- Logger.debug("ExpoAudioStreamModule", "Notification permissions not granted")
219
- }
220
- }
221
- }
222
-
223
- if let result = self.streamManager.startRecording(settings: settings) {
224
- var resultDict: [String: Any] = [
225
- "fileUri": result.fileUri,
226
- "channels": result.channels,
227
- "bitDepth": result.bitDepth,
228
- "sampleRate": result.sampleRate,
229
- "mimeType": result.mimeType,
230
- ]
231
-
232
- // Add compression info if available
233
- if let compression = result.compression {
234
- resultDict["compression"] = [
235
- "compressedFileUri": compression.compressedFileUri,
236
- "mimeType": compression.mimeType,
237
- "bitrate": compression.bitrate,
238
- "format": compression.format
239
- ]
240
- }
241
-
242
- Logger.info("ExpoAudioStreamModule", "Recording started successfully")
243
- promise.resolve(resultDict)
244
- } else {
245
- Logger.error("ExpoAudioStreamModule", "Failed to start recording")
246
- promise.reject("ERROR", "Failed to start recording.")
247
- }
248
-
249
- case .failure(let error):
250
- Logger.error("ExpoAudioStreamModule", "Invalid settings - \(error.localizedDescription)")
251
- promise.reject("INVALID_SETTINGS", error.localizedDescription)
252
- }
253
- }
254
- }
255
-
256
- /// Retrieves the current status of the audio stream.
257
- ///
258
- /// - Returns: The current status of the audio stream.Ï
259
- Function("status") {
260
- let currentStatus = self.streamManager.getStatus()
261
- Logger.debug("ExpoAudioStreamModule", "status requested: isRecording=\(currentStatus["isRecording"] ?? false), isPaused=\(currentStatus["isPaused"] ?? false)")
262
- return currentStatus
263
- }
264
-
265
- /// Prepares audio recording with the specified settings without starting it.
266
- ///
267
- /// - Parameters:
268
- /// - options: The recording settings to use.
269
- /// - promise: A promise to resolve with true if preparation was successful.
270
- /// - Returns: A promise that resolves with a boolean indicating success.
271
- AsyncFunction("prepareRecording") { (options: [String: Any], promise: Promise) in
272
- Logger.debug("ExpoAudioStreamModule", "prepareRecording called with options: \(options)")
273
- self.checkMicrophonePermission { granted in
274
- guard granted else {
275
- promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
276
- return
277
- }
278
-
279
- // Create settings with validation
280
- let settingsResult = RecordingSettings.fromDictionary(options)
281
-
282
- switch settingsResult {
283
- case .success(let settings):
284
- Logger.debug("ExpoAudioStreamModule", "prepareRecording: Settings parsed successfully. Calling streamManager.prepareRecording")
285
- if self.streamManager.prepareRecording(settings: settings) {
286
- Logger.info("ExpoAudioStreamModule", "prepareRecording: Preparation successful.")
287
- promise.resolve(true)
288
- } else {
289
- Logger.error("ExpoAudioStreamModule", "prepareRecording: streamManager.prepareRecording returned false.")
290
- promise.reject("ERROR", "Failed to prepare recording.")
291
- }
292
-
293
- case .failure(let error):
294
- promise.reject("INVALID_SETTINGS", error.localizedDescription)
295
- }
296
- }
297
- }
298
-
299
- /// Pauses audio recording.
300
- Function("pauseRecording") {
301
- Logger.debug("ExpoAudioStreamModule", "pauseRecording called.")
302
- self.streamManager.pauseRecording()
303
- }
304
-
305
- /// Resumes audio recording.
306
- Function("resumeRecording") {
307
- Logger.debug("ExpoAudioStreamModule", "resumeRecording called.")
308
- self.streamManager.resumeRecording()
309
- }
310
-
311
- /// Asynchronously stops audio recording and retrieves the recording result.
312
- ///
313
- /// - Parameters:
314
- /// - promise: A promise to resolve with the recording result or reject with an error.
315
- AsyncFunction("stopRecording") { (promise: Promise) in
316
- Logger.debug("ExpoAudioStreamModule", "stopRecording called.")
317
-
318
- if let recordingResult = self.streamManager.stopRecording() {
319
- var resultDict: [String: Any] = [
320
- "fileUri": recordingResult.fileUri,
321
- "filename": recordingResult.filename,
322
- "durationMs": recordingResult.duration,
323
- "size": recordingResult.size,
324
- "channels": recordingResult.channels,
325
- "bitDepth": recordingResult.bitDepth,
326
- "sampleRate": recordingResult.sampleRate,
327
- "mimeType": recordingResult.mimeType,
328
- "createdAt": Date().timeIntervalSince1970 * 1000,
329
- ]
330
-
331
- // Add compression info if available
332
- if let compression = recordingResult.compression {
333
- resultDict["compression"] = [
334
- "compressedFileUri": compression.compressedFileUri,
335
- "mimeType": compression.mimeType,
336
- "bitrate": compression.bitrate,
337
- "format": compression.format,
338
- "size": compression.size
339
- ]
340
- }
341
-
342
- Logger.info("ExpoAudioStreamModule", "stopRecording: Recording stopped successfully. fileUri: \(recordingResult.fileUri), size: \(recordingResult.size)")
343
- promise.resolve(resultDict)
344
- } else {
345
- Logger.error("ExpoAudioStreamModule", "stopRecording: streamManager.stopRecording returned nil.")
346
- promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
347
- }
348
- }
349
-
350
- /// Asynchronously lists all audio files stored in the document directory.
351
- ///
352
- /// - Parameters:
353
- /// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
354
- /// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
355
- AsyncFunction("listAudioFiles") { (promise: Promise) in
356
- Logger.debug("ExpoAudioStreamModule", "listAudioFiles called.")
357
- let files = listAudioFiles()
358
- Logger.debug("ExpoAudioStreamModule", "listAudioFiles returning \(files.count) files.")
359
- promise.resolve(files)
360
- }
361
-
362
- /// Clears all audio files stored in the document directory.
363
- Function("clearAudioFiles") {
364
- Logger.debug("ExpoAudioStreamModule", "clearAudioFiles called.")
365
- clearAudioFiles()
366
- }
367
-
368
-
369
- /// Requests audio recording permissions.
370
- ///
371
- /// - Parameters:
372
- /// - promise: A promise to resolve with the permission status or reject with an error.
373
- /// - Returns: Promise to be resolved with the permission status.
374
- AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
375
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
376
- promise.resolve([
377
- "status": granted ? "granted" : "denied",
378
- "granted": granted,
379
- "expires": "never",
380
- "canAskAgain": true
381
- ])
382
- }
383
- }
384
-
385
- AsyncFunction("requestNotificationPermissionsAsync") { (promise: Promise) in
386
- Task {
387
- let granted = await requestNotificationPermissions()
388
- promise.resolve([
389
- "granted": granted,
390
- "status": granted ? "granted" : "denied"
391
- ])
392
- }
393
- }
394
-
395
- /// Gets the current audio recording permissions.
396
- ///
397
- /// - Parameters:
398
- /// - promise: A promise to resolve with the permission status or reject with an error.
399
- /// - Returns: Promise to be resolved with the permission status.
400
- AsyncFunction("getPermissionsAsync") { (promise: Promise) in
401
- let permissionStatus = AVAudioSession.sharedInstance().recordPermission
402
- switch permissionStatus {
403
- case .granted:
404
- promise.resolve([
405
- "status": "granted",
406
- "granted": true,
407
- "expires": "never",
408
- "canAskAgain": true
409
- ])
410
- case .denied:
411
- promise.resolve([
412
- "status": "denied",
413
- "granted": false,
414
- "expires": "never",
415
- "canAskAgain": false
416
- ])
417
- case .undetermined:
418
- promise.resolve([
419
- "status": "undetermined",
420
- "granted": false,
421
- "expires": "never",
422
- "canAskAgain": true
423
- ])
424
- @unknown default:
425
- promise.reject("UNKNOWN_ERROR", "Unknown permission status")
426
- }
427
- }
428
-
429
- /// Trims an audio file to specified start and end times.
430
- /// - Parameters:
431
- /// - options: A dictionary containing:
432
- /// - `fileUri`: The URI of the audio file.
433
- /// - `mode`: Trim mode ('single', 'keep', or 'remove').
434
- /// - `startTimeMs`: Start time in milliseconds (for 'single' mode).
435
- /// - `endTimeMs`: End time in milliseconds (for 'single' mode).
436
- /// - `ranges`: Array of time ranges (for 'keep' and 'remove' modes).
437
- /// - `outputFileName`: Optional name for the output file.
438
- /// - `outputFormat`: Optional output format configuration.
439
- /// - `decodingOptions`: Optional decoding configuration.
440
- AsyncFunction("trimAudio") { (options: [String: Any], promise: Promise) in
441
- guard let fileUri = options["fileUri"] as? String,
442
- let url = URL(string: fileUri) else {
443
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
444
- return
445
- }
446
-
447
- let mode = options["mode"] as? String ?? "single"
448
- let startTimeMs = options["startTimeMs"] as? Double
449
- let endTimeMs = options["endTimeMs"] as? Double
450
- let ranges = options["ranges"] as? [[String: Double]]
451
- let outputFileName = options["outputFileName"] as? String
452
- let outputFormat = options["outputFormat"] as? [String: Any]
453
- let decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
454
-
455
- // Add detailed logging for filename and format options
456
- Logger.debug("ExpoAudioStreamModule", "Trim audio request:")
457
- Logger.debug("ExpoAudioStreamModule", "- Input file: \(fileUri)")
458
- Logger.debug("ExpoAudioStreamModule", "- Mode: \(mode)")
459
- Logger.debug("ExpoAudioStreamModule", "- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
460
- if let format = outputFormat?["format"] as? String {
461
- Logger.debug("ExpoAudioStreamModule", "- Output format: \(format)")
462
- } else {
463
- Logger.debug("ExpoAudioStreamModule", "- Output format: not specified (will use default)")
464
- }
465
-
466
- // Input validation based on mode
467
- switch mode {
468
- case "single":
469
- guard let start = startTimeMs, let end = endTimeMs else {
470
- promise.reject("INVALID_ARGUMENTS", "startTimeMs and endTimeMs required for 'single' mode")
471
- return
472
- }
473
- guard start >= 0, end > start else {
474
- promise.reject("INVALID_ARGUMENTS", "Invalid time range")
475
- return
476
- }
477
- case "keep", "remove":
478
- guard let rangesArray = ranges, !rangesArray.isEmpty else {
479
- promise.reject("INVALID_ARGUMENTS", "'ranges' array required for 'keep' or 'remove' mode")
480
- return
481
- }
482
- default:
483
- promise.reject("INVALID_MODE", "Mode must be 'single', 'keep', or 'remove'")
484
- return
485
- }
486
-
487
- DispatchQueue.global().async {
488
- do {
489
- let audioProcessor = try AudioProcessor(
490
- url: url,
491
- resolve: { result in promise.resolve(result) },
492
- reject: { code, message in promise.reject(code, message) }
493
- )
494
-
495
- let progressCallback: (Float, Int64, Int64) -> Void = { progress, bytesProcessed, totalBytes in
496
- self.sendEvent(trimProgressEvent, [
497
- "progress": progress,
498
- "bytesProcessed": bytesProcessed,
499
- "totalBytes": totalBytes
500
- ])
501
- }
502
-
503
- let startTime = CACurrentMediaTime()
504
- if let result = audioProcessor.trimAudio(
505
- mode: mode,
506
- startTimeMs: startTimeMs,
507
- endTimeMs: endTimeMs,
508
- ranges: ranges,
509
- outputFileName: outputFileName,
510
- outputFormat: outputFormat,
511
- decodingOptions: decodingOptions,
512
- progressCallback: progressCallback
513
- ) {
514
- let processingTimeMs = Int((CACurrentMediaTime() - startTime) * 1000)
515
- var resultDict = result.toDictionary()
516
- resultDict["processingInfo"] = ["durationMs": processingTimeMs]
517
-
518
- let uri = result.uri
519
- Logger.debug("ExpoAudioStreamModule", "Trim completed successfully in \(processingTimeMs)ms")
520
- Logger.debug("ExpoAudioStreamModule", "Output file URI: \(uri)")
521
-
522
- // Verify file exists
523
- let fileManager = FileManager.default
524
- if let url = URL(string: uri) {
525
- let exists = fileManager.fileExists(atPath: url.path)
526
- Logger.debug("ExpoAudioStreamModule", "File exists at path \(url.path): \(exists)")
527
-
528
- // Log filename details
529
- Logger.debug("ExpoAudioStreamModule", "Filename: \(url.lastPathComponent)")
530
- Logger.debug("ExpoAudioStreamModule", "File extension: \(url.pathExtension.lowercased())")
531
-
532
- // If format is AAC, ensure we're using the correct extension and MIME type
533
- if let format = outputFormat?["format"] as? String,
534
- format.lowercased() == "aac" {
535
-
536
- Logger.debug("ExpoAudioStreamModule", "AAC format detected - ensuring correct metadata")
537
-
538
- // For AAC format, ensure we're using the correct extension and MIME type
539
- if url.pathExtension.lowercased() == "m4a" {
540
- Logger.debug("ExpoAudioStreamModule", "File has correct m4a extension for AAC audio")
541
-
542
- // Just update the MIME type in the result to ensure correct playback
543
- if var compression = resultDict["compression"] as? [String: Any] {
544
- compression["mimeType"] = "audio/mp4"
545
- resultDict["compression"] = compression
546
- }
547
-
548
- resultDict["mimeType"] = "audio/mp4"
549
- resultDict["actualFormat"] = "m4a"
550
- } else {
551
- Logger.debug("ExpoAudioStreamModule", "Warning: AAC format should use .m4a extension, but found .\(url.pathExtension.lowercased())")
552
- }
553
- }
554
- }
555
-
556
- promise.resolve(resultDict)
557
- } else {
558
- Logger.debug("ExpoAudioStreamModule", "Failed to trim audio")
559
- promise.reject("TRIM_ERROR", "Failed to trim audio")
560
- }
561
- } catch {
562
- Logger.debug("ExpoAudioStreamModule", "Failed to initialize audio processor: \(error.localizedDescription)")
563
- promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
564
- }
565
- }
566
- }
567
-
568
- /// Extracts raw PCM audio data from a file with time or byte range support
569
- /// - Parameters:
570
- /// - options: A dictionary containing:
571
- /// - `fileUri`: The URI of the audio file
572
- /// - `startTimeMs`: Optional start time in milliseconds
573
- /// - `endTimeMs`: Optional end time in milliseconds
574
- /// - `position`: Optional byte position
575
- /// - `length`: Optional byte length
576
- /// - `includeNormalizedData`: Boolean to include normalized audio data in [-1, 1] range
577
- /// - `includeWavHeader`: Boolean to include WAV header in the PCM data
578
- /// - `decodingOptions`: Decoding configuration
579
- /// - `includeBase64Data`: Boolean to include base64 encoded string representation of the audio data
580
- /// - `computeChecksum`: Boolean to compute and include CRC32 checksum of the PCM data
581
- AsyncFunction("extractAudioData") { (options: [String: Any], promise: Promise) in
582
- guard let fileUri = options["fileUri"] as? String,
583
- let url = URL(string: fileUri) else {
584
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
585
- return
586
- }
587
-
588
- // Get time or byte range options
589
- let startTimeMs = options["startTimeMs"] as? Double
590
- let endTimeMs = options["endTimeMs"] as? Double
591
- let position = options["position"] as? Int
592
- let length = options["length"] as? Int
593
- let includeWavHeader = options["includeWavHeader"] as? Bool ?? false
594
- let includeNormalizedData = options["includeNormalizedData"] as? Bool ?? false
595
- let decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
596
- let includeBase64Data = options["includeBase64Data"] as? Bool ?? false
597
-
598
- // Validate that we have either time range or byte range, but not both and not neither
599
- let hasTimeRange = startTimeMs != nil && endTimeMs != nil
600
- let hasByteRange = position != nil && length != nil
601
-
602
- guard hasTimeRange || hasByteRange else {
603
- promise.reject("INVALID_ARGUMENTS", "Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
604
- return
605
- }
606
-
607
- guard !(hasTimeRange && hasByteRange) else {
608
- promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
609
- return
610
- }
611
-
612
- do {
613
- let audioFile = try AVAudioFile(forReading: url)
614
- let format = audioFile.processingFormat
615
- let sampleRate = format.sampleRate
616
- let channels = Int(format.channelCount)
617
- let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
618
-
619
- // Calculate frame positions
620
- let startFrame: AVAudioFramePosition
621
- let endFrame: AVAudioFramePosition
622
-
623
- if hasTimeRange {
624
- startFrame = AVAudioFramePosition(startTimeMs! * sampleRate / 1000.0)
625
- endFrame = AVAudioFramePosition(endTimeMs! * sampleRate / 1000.0)
626
- } else {
627
- // Convert byte position to frame position
628
- let bytesPerFrame = Int64(channels * (bitDepth / 8))
629
- startFrame = AVAudioFramePosition(position!) / bytesPerFrame
630
- endFrame = startFrame + (AVAudioFramePosition(length!) / bytesPerFrame)
631
- }
632
-
633
- // Validate frame range
634
- guard startFrame >= 0 && endFrame <= audioFile.length && startFrame < endFrame else {
635
- promise.reject("INVALID_RANGE", "Invalid range specified")
636
- return
637
- }
638
-
639
- let frameCount = AVAudioFrameCount(endFrame - startFrame)
640
-
641
-
642
- // Pass both options separately - normalizeAudio from decodingOptions, and includeNormalizedData as is
643
- let decodingConfig = DecodingConfig.fromDictionary(decodingOptions)
644
-
645
- let (pcmData, normalizedData, base64Data) = try extractRawAudioData(
646
- from: url,
647
- startFrame: startFrame,
648
- frameCount: frameCount,
649
- format: format,
650
- decodingConfig: decodingConfig,
651
- includeNormalizedData: includeNormalizedData,
652
- includeBase64Data: includeBase64Data
653
- )
654
-
655
- var resultDict: [String: Any] = [:]
656
-
657
- if includeWavHeader {
658
- // Create WAV header and prepend it to the PCM data
659
- let wavData = createWavHeader(
660
- pcmData: pcmData,
661
- sampleRate: Int(sampleRate),
662
- channels: channels,
663
- bitDepth: bitDepth
664
- )
665
- resultDict["pcmData"] = wavData
666
- resultDict["hasWavHeader"] = true
667
- } else {
668
- resultDict["pcmData"] = pcmData
669
- resultDict["hasWavHeader"] = false
670
- }
671
-
672
- // Add the rest of the data
673
- resultDict["sampleRate"] = Int(sampleRate)
674
- resultDict["channels"] = channels
675
- resultDict["bitDepth"] = bitDepth
676
- resultDict["durationMs"] = Int(Double(frameCount) * 1000.0 / sampleRate)
677
- resultDict["format"] = "pcm_\(bitDepth)bit"
678
- resultDict["samples"] = Int(frameCount) * channels
679
-
680
- // Add normalized data if requested, regardless of normalization setting
681
- if includeNormalizedData {
682
- resultDict["normalizedData"] = normalizedData
683
- }
684
-
685
- // Add checksum if requested
686
- if options["computeChecksum"] as? Bool == true {
687
- let checksum = calculateCRC32(data: pcmData)
688
- resultDict["checksum"] = Int(checksum)
689
-
690
- Logger.debug("ExpoAudioStreamModule", "Computed CRC32 checksum: \(checksum)")
691
- }
692
-
693
- if let includeBase64Data = options["includeBase64Data"] as? Bool, includeBase64Data {
694
- resultDict["base64Data"] = base64Data
695
- }
696
-
697
- promise.resolve(resultDict)
698
-
699
- } catch {
700
- promise.reject("PROCESSING_ERROR", "Failed to process audio file: \(error.localizedDescription)")
701
- }
702
- }
703
-
704
- /// Extracts mel spectrogram data from a file.
705
- ///
706
- /// - Parameters:
707
- /// - options: A dictionary containing:
708
- /// - `fileUri`: The URI of the audio file.
709
- /// - `pointsPerSecond`: The number of data points to extract per second of audio.
710
- /// - promise: A promise to resolve with the extracted mel spectrogram data or reject with an error.
711
- /// - Returns: Promise to be resolved with mel spectrogram data.
712
- AsyncFunction("extractMelSpectrogram") { (options: [String: Any], promise: Promise) in
713
- // This is a placeholder implementation that will be fully implemented later
714
- // Currently, mel spectrogram extraction is only available on Android
715
- promise.reject(
716
- "UNSUPPORTED_PLATFORM",
717
- "Mel spectrogram extraction is currently only available on Android and is experimental"
718
- )
719
- }
720
-
721
- /// Gets available audio input devices with an optional refresh parameter
722
- /// - Parameters:
723
- /// - options: Optional dictionary containing a refresh parameter
724
- /// - promise: A promise to resolve with a list of available audio input devices
725
- AsyncFunction("getAvailableInputDevices") { (options: [String: Any]?, promise: Promise) in
726
- Logger.debug("ExpoAudioStreamModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
727
- if let options = options, let refresh = options["refresh"] as? Bool, refresh {
728
- Logger.debug("ExpoAudioStreamModule", "Forcing refresh of audio devices")
729
- _ = self.deviceManager.forceRefreshAudioSession()
730
- }
731
-
732
- // Call the device manager with the promise
733
- self.deviceManager.getAvailableInputDevices(promise: promise)
734
- }
735
-
736
- /// Refreshes the audio session to detect newly connected devices
737
- /// - Returns: Boolean indicating success
738
- Function("refreshAudioDevices") {
739
- Logger.debug("ExpoAudioStreamModule", "refreshAudioDevices called.")
740
- let success = self.deviceManager.forceRefreshAudioSession()
741
- Logger.debug("ExpoAudioStreamModule", "refreshAudioDevices result: \(success)")
742
- return ["success": success]
743
- }
744
-
745
- /// Gets the currently selected audio input device
746
- ///
747
- /// - Parameters:
748
- /// - promise: A promise to resolve with the currently selected audio input device
749
- AsyncFunction("getCurrentInputDevice") { (promise: Promise) in
750
- Logger.debug("ExpoAudioStreamModule", "getCurrentInputDevice called.")
751
- self.deviceManager.getCurrentInputDevice(promise: promise)
752
- }
753
-
754
- /// Selects a specific audio input device for recording
755
- ///
756
- /// - Parameters:
757
- /// - deviceId: The ID of the device to select
758
- /// - promise: A promise to resolve with boolean indicating success
759
- AsyncFunction("selectInputDevice") { (deviceId: String, promise: Promise) in
760
- Logger.debug("ExpoAudioStreamModule", "selectInputDevice called with ID: \(deviceId)")
761
- self.deviceManager.selectInputDevice(deviceId, promise: promise)
762
- // Sync deviceId into recordingSettings so updateAudioSessionWithCurrentSettings can find the port
763
- self.streamManager.recordingSettings?.deviceId = deviceId
764
- // Update the audio recorder if recording is in progress or prepared
765
- if self.streamManager.isRecording || self.streamManager.isPrepared {
766
- Logger.debug("ExpoAudioStreamModule", "selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
767
- self.streamManager.updateAudioSessionWithCurrentSettings()
768
- } else {
769
- Logger.debug("ExpoAudioStreamModule", "selectInputDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
770
- }
771
- }
772
-
773
- /// Resets to the default audio input device
774
- ///
775
- /// - Parameters:
776
- /// - promise: A promise to resolve with boolean indicating success
777
- AsyncFunction("resetToDefaultDevice") { (promise: Promise) in
778
- Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice called.")
779
- self.deviceManager.resetToDefaultDevice { success, error in
780
- if success {
781
- // Clear stored deviceId so updateAudioSessionWithCurrentSettings won't bail early
782
- self.streamManager.recordingSettings?.deviceId = nil
783
- if self.streamManager.isRecording || self.streamManager.isPrepared {
784
- Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Performing device switch to system default.")
785
- // Bug 1 fix: call performDeviceSwitch(nil) directly — updateAudioSessionWithCurrentSettings
786
- // would bail immediately because deviceId is now nil.
787
- self.streamManager.performDeviceSwitch(port: nil)
788
- } else {
789
- Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Not recording/prepared, no engine action needed.")
790
- }
791
- promise.resolve(true)
792
- } else {
793
- Logger.error("ExpoAudioStreamModule", "resetToDefaultDevice failed: \(error?.localizedDescription ?? "Unknown error")")
794
- promise.reject("DEVICE_ERROR", "Failed to reset to default device: \(error?.localizedDescription ?? "Unknown error")")
795
- }
796
- }
797
- }
798
- }
799
-
800
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
801
- Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveInterruption: \(info)")
802
- // Convert iOS interruption events to match the TypeScript types
803
- var reason: String
804
- var isPaused: Bool = true
805
-
806
- if let type = info["type"] as? String {
807
- switch type {
808
- case "began":
809
- // Phone call or other audio session interruption began
810
- reason = "audioFocusLoss"
811
- case "ended":
812
- reason = "audioFocusGain"
813
- isPaused = false
814
- // Check if this was from a phone call
815
- if let wasSuspended = info["wasSuspended"] as? Bool, wasSuspended {
816
- reason = "phoneCallEnded"
817
- }
818
- default:
819
- return
820
- }
821
- } else if let specificReason = info["reason"] as? String {
822
- // Handle specific reasons that are already properly formatted
823
- reason = specificReason
824
- isPaused = info["isPaused"] as? Bool ?? true
825
- } else {
826
- return
827
- }
828
-
829
- // Send event in the correct format
830
- sendEvent(recordingInterruptedEvent, [
831
- "reason": reason,
832
- "isPaused": isPaused,
833
- "timestamp": Date().timeIntervalSince1970 * 1000
834
- ])
835
- }
836
-
837
- func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
838
- Logger.debug("ExpoAudioStreamModule", "Delegate: didPauseRecording")
839
- sendEvent(recordingInterruptedEvent, [
840
- "reason": "userPaused",
841
- "isPaused": true,
842
- "timestamp": pauseTime.timeIntervalSince1970 * 1000
843
- ])
844
- }
845
-
846
- func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
847
- Logger.debug("ExpoAudioStreamModule", "Delegate: didResumeRecording")
848
- sendEvent(recordingInterruptedEvent, [
849
- "reason": "userResumed",
850
- "isPaused": false,
851
- "timestamp": resumeTime.timeIntervalSince1970 * 1000
852
- ])
853
- }
854
-
855
- func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
856
- Logger.debug("ExpoAudioStreamModule", "Delegate: didUpdateNotificationState: isPaused=\(isPaused)")
857
- sendEvent(recordingInterruptedEvent, [
858
- "reason": "notification",
859
- "isPaused": isPaused,
860
- "timestamp": Date().timeIntervalSince1970 * 1000
861
- ])
862
- }
863
-
864
- /// Handles the reception of audio data from the AudioStreamManager.
865
- ///
866
- /// - Parameters:
867
- /// - manager: The AudioStreamManager instance.
868
- /// - data: The received audio data.
869
- /// - recordingTime: The current recording time.
870
- /// - totalDataSize: The total size of the received audio data.
871
- func audioStreamManager(
872
- _ manager: AudioStreamManager,
873
- didReceiveAudioData data: Data,
874
- recordingTime: TimeInterval,
875
- totalDataSize: Int64,
876
- compressionInfo: [String: Any]?
877
- ) {
878
- // Reduce log frequency or detail for this potentially high-frequency event
879
- // Logger.debug("[ExpoAudioStreamModule] Delegate: didReceiveAudioData: size=\(data.count), totalSize=\(totalDataSize)")
880
- var resultDict: [String: Any] = [
881
- "fileUri": manager.recordingFileURL?.absoluteString ?? "",
882
- "lastEmittedSize": totalDataSize,
883
- "encoded": data.base64EncodedString(),
884
- "deltaSize": data.count,
885
- "position": Int64(recordingTime * 1000),
886
- "mimeType": manager.mimeType,
887
- "totalSize": totalDataSize,
888
- "streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
889
- ]
890
-
891
- if let compressionInfo = compressionInfo {
892
- resultDict["compression"] = compressionInfo
893
- }
894
-
895
- sendEvent(audioDataEvent, resultDict)
896
- }
897
-
898
- private func requestNotificationPermissions() async -> Bool {
899
- do {
900
- let options: UNAuthorizationOptions = [.alert, .sound]
901
- return try await notificationCenter.requestAuthorization(options: options)
902
- } catch {
903
- Logger.debug("ExpoAudioStreamModule", "Failed to request notification permissions: \(error)")
904
- return false
905
- }
906
- }
907
-
908
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
909
- if let data = result {
910
- Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveProcessingResult: Received \(data.dataPoints.count) data points.")
911
- } else {
912
- Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveProcessingResult: Received nil result.")
913
- }
914
- let resultDict = result?.toDictionary() ?? [:]
915
- sendEvent(audioAnalysisEvent, resultDict)
916
- }
917
-
918
- /// Checks microphone permission and calls the completion handler with the result.
919
- ///
920
- /// - Parameters:
921
- /// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
922
- private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
923
- switch AVAudioSession.sharedInstance().recordPermission {
924
- case .granted:
925
- DispatchQueue.main.async { completion(true) }
926
- case .denied:
927
- DispatchQueue.main.async { completion(false) }
928
- case .undetermined:
929
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
930
- DispatchQueue.main.async {
931
- completion(granted)
932
- }
933
- }
934
- @unknown default:
935
- DispatchQueue.main.async { completion(false) }
936
- }
937
- }
938
-
939
- /// Clears all audio files stored in the document directory.
940
- private func clearAudioFiles() {
941
- let fileURLs = listAudioFiles() // This now returns full URLs as strings
942
- fileURLs.forEach { fileURLString in
943
- if let fileURL = URL(string: fileURLString) {
944
- do {
945
- try FileManager.default.removeItem(at: fileURL)
946
- print("ExpoAudioStreamModule", "Removed file at:", fileURL.path)
947
- } catch {
948
- print("ExpoAudioStreamModule", "Error removing file at \(fileURL.path):", error.localizedDescription)
949
- }
950
- } else {
951
- print("ExpoAudioStreamModule", "Invalid URL string: \(fileURLString)")
952
- }
953
- }
954
- }
955
-
956
- /// Extracts feature options from the provided options dictionary.
957
- ///
958
- /// - Parameters:
959
- /// - options: The options dictionary containing feature flags.
960
- /// - Returns: A dictionary with feature flags and their boolean values.
961
- private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
962
- return [
963
- "energy": options["energy"] as? Bool ?? false,
964
- "mfcc": options["mfcc"] as? Bool ?? false,
965
- "rms": options["rms"] as? Bool ?? false,
966
- "zcr": options["zcr"] as? Bool ?? false,
967
- "dB": options["dB"] as? Bool ?? false,
968
- "spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
969
- "spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
970
- "spectralRollOff": options["spectralRollOff"] as? Bool ?? false,
971
- "spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
972
- "chromagram": options["chromagram"] as? Bool ?? false,
973
- "tempo": options["tempo"] as? Bool ?? false,
974
- "hnr": options["hnr"] as? Bool ?? false,
975
- "melSpectrogram": options["melSpectrogram"] as? Bool ?? false,
976
- "spectralContrast": options["spectralContrast"] as? Bool ?? false,
977
- "tonnetz": options["tonnetz"] as? Bool ?? false,
978
- "pitch": options["pitch"] as? Bool ?? false,
979
- "crc32": options["crc32"] as? Bool ?? false
980
- ]
981
- }
982
-
983
- /// Lists all audio files stored in the document directory.
984
- ///
985
- /// - Returns: An array of file URIs as strings.
986
- func listAudioFiles() -> [String] {
987
- guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
988
- print("ExpoAudioStreamModule", "Failed to access document directory.")
989
- return []
990
- }
991
-
992
- do {
993
- let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
994
- let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
995
- return audioFiles
996
- } catch {
997
- print("ExpoAudioStreamModule", "Error listing audio files:", error.localizedDescription)
998
- return []
999
- }
1000
- }
1001
-
1002
- func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
1003
- Logger.error("ExpoAudioStreamModule", "Delegate: didFailWithError: \(error)")
1004
- sendEvent(errorEvent, [ "message": error ])
1005
- }
1006
-
1007
- // MARK: - AudioDeviceManagerDelegate
1008
-
1009
- /// Handles device disconnection events from the AudioDeviceManager
1010
- func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String) {
1011
- Logger.debug("ExpoAudioStreamModule", "Device disconnected: \(deviceId)")
1012
-
1013
- // Emit device change event to match Android implementation
1014
- sendEvent(deviceChangedEvent, [
1015
- "type": "deviceDisconnected",
1016
- "deviceId": deviceId
1017
- ])
1018
- }
1019
- }