@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,1111 @@
1
+ // packages/audio-studio/ios/AudioStudioModule.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 AudioStudioModule: 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("AudioStudio")
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("AudioStudioModule", "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("AudioStudioModule", "Device connected: \(deviceId)")
55
+ self?.sendEvent(deviceChangedEvent, [
56
+ "type": "deviceConnected",
57
+ "deviceId": deviceId
58
+ ])
59
+ }
60
+ }
61
+ }
62
+
63
+ OnDestroy {
64
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "extractAudioAnalysis called with options: \(options)")
81
+ guard let fileUri = options["fileUri"] as? String,
82
+ let url = URL(string: fileUri) else {
83
+ Logger.error("AudioStudioModule", "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("AudioStudioModule", "extractAudioAnalysis: Processing started for \(fileUri)")
129
+ let audioProcessor = try AudioProcessor(url: url, resolve: { result in
130
+ Logger.warn("AudioStudioModule", "extractAudioAnalysis: AudioProcessor resolve called unexpectedly.")
131
+ }, reject: { code, message in
132
+ Logger.warn("AudioStudioModule", "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("AudioStudioModule", "extractAudioAnalysis: Processing successful for \(fileUri)")
147
+ promise.resolve(result.toDictionary())
148
+ } else {
149
+ Logger.error("AudioStudioModule", "extractAudioAnalysis: audioProcessor.processAudioData returned nil for \(fileUri)")
150
+ promise.reject("PROCESSING_ERROR", "Failed to process audio data")
151
+ }
152
+ } catch {
153
+ Logger.error("AudioStudioModule", "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("AudioStudioModule", "startRecording called")
181
+ self.checkMicrophonePermission { granted in
182
+ guard granted else {
183
+ Logger.warn("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "Recording started successfully")
243
+ promise.resolve(resultDict)
244
+ } else {
245
+ Logger.error("AudioStudioModule", "Failed to start recording")
246
+ promise.reject("ERROR", "Failed to start recording.")
247
+ }
248
+
249
+ case .failure(let error):
250
+ Logger.error("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "prepareRecording: Settings parsed successfully. Calling streamManager.prepareRecording")
285
+ if self.streamManager.prepareRecording(settings: settings) {
286
+ Logger.info("AudioStudioModule", "prepareRecording: Preparation successful.")
287
+ promise.resolve(true)
288
+ } else {
289
+ Logger.error("AudioStudioModule", "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("AudioStudioModule", "pauseRecording called.")
302
+ self.streamManager.pauseRecording()
303
+ }
304
+
305
+ /// Resumes audio recording.
306
+ Function("resumeRecording") {
307
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "stopRecording: Recording stopped successfully. fileUri: \(recordingResult.fileUri), size: \(recordingResult.size)")
343
+ promise.resolve(resultDict)
344
+ } else {
345
+ Logger.error("AudioStudioModule", "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("AudioStudioModule", "listAudioFiles called.")
357
+ let files = listAudioFiles()
358
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "Trim audio request:")
457
+ Logger.debug("AudioStudioModule", "- Input file: \(fileUri)")
458
+ Logger.debug("AudioStudioModule", "- Mode: \(mode)")
459
+ Logger.debug("AudioStudioModule", "- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
460
+ if let format = outputFormat?["format"] as? String {
461
+ Logger.debug("AudioStudioModule", "- Output format: \(format)")
462
+ } else {
463
+ Logger.debug("AudioStudioModule", "- 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("AudioStudioModule", "Trim completed successfully in \(processingTimeMs)ms")
520
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "File exists at path \(url.path): \(exists)")
527
+
528
+ // Log filename details
529
+ Logger.debug("AudioStudioModule", "Filename: \(url.lastPathComponent)")
530
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "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("AudioStudioModule", "Failed to trim audio")
559
+ promise.reject("TRIM_ERROR", "Failed to trim audio")
560
+ }
561
+ } catch {
562
+ Logger.debug("AudioStudioModule", "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("AudioStudioModule", "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
+ do {
714
+ guard let fileUri = options["fileUri"] as? String else {
715
+ throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "fileUri is required"])
716
+ }
717
+ guard let windowSizeMs = options["windowSizeMs"] as? Double else {
718
+ throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "windowSizeMs is required"])
719
+ }
720
+ guard let hopLengthMs = options["hopLengthMs"] as? Double else {
721
+ throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "hopLengthMs is required"])
722
+ }
723
+ guard let nMels = options["nMels"] as? Int ?? (options["nMels"] as? Double).map({ Int($0) }) else {
724
+ throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "nMels is required"])
725
+ }
726
+
727
+ let fMin = Float(options["fMin"] as? Double ?? 0.0)
728
+ let fMaxParam = options["fMax"] as? Double
729
+ let windowType = options["windowType"] as? String ?? "hann"
730
+ let logScale = options["logScale"] as? Bool ?? true
731
+ let normalize = options["normalize"] as? Bool ?? false
732
+ let startTimeMs = options["startTimeMs"] as? Double
733
+ let endTimeMs = options["endTimeMs"] as? Double
734
+
735
+ // Load audio file to PCM float samples
736
+ let audioData = try loadAudioFile(fileUri)
737
+ let sampleRate = audioData.sampleRate
738
+ var samples = audioData.samples
739
+
740
+ // Apply time range trimming if specified
741
+ if let startMs = startTimeMs {
742
+ let startSample = Int(startMs * Double(sampleRate) / 1000.0)
743
+ let endSample: Int
744
+ if let endMs = endTimeMs {
745
+ endSample = min(Int(endMs * Double(sampleRate) / 1000.0), samples.count)
746
+ } else {
747
+ endSample = samples.count
748
+ }
749
+ if startSample < endSample && startSample < samples.count {
750
+ samples = Array(samples[startSample..<endSample])
751
+ }
752
+ }
753
+
754
+ let fMax = fMaxParam.map { Float($0) } ?? Float(sampleRate) / 2.0
755
+
756
+ // Convert ms to samples
757
+ let windowSizeSamples = Int(windowSizeMs * Double(sampleRate) / 1000.0)
758
+ let hopLengthSamples = Int(hopLengthMs * Double(sampleRate) / 1000.0)
759
+
760
+ let windowTypeInt: Int32 = windowType.lowercased() == "hamming" ? 1 : 0
761
+
762
+ // Call shared C++ implementation via ObjC++ wrapper
763
+ guard let result = samples.withUnsafeBufferPointer({ bufferPtr -> [AnyHashable: Any]? in
764
+ guard let baseAddress = bufferPtr.baseAddress else { return nil }
765
+ return MelSpectrogramWrapper.compute(
766
+ withSamples: baseAddress,
767
+ numSamples: Int32(samples.count),
768
+ sampleRate: Int32(sampleRate),
769
+ fftLength: 2048,
770
+ windowSizeSamples: Int32(windowSizeSamples),
771
+ hopLengthSamples: Int32(hopLengthSamples),
772
+ nMels: Int32(nMels),
773
+ fMin: fMin,
774
+ fMax: fMax,
775
+ windowType: windowTypeInt,
776
+ logScale: logScale,
777
+ normalize: normalize
778
+ )
779
+ }) else {
780
+ throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio data is too short for spectrogram analysis"])
781
+ }
782
+
783
+ let timeSteps = result["timeSteps"] as! Int
784
+ let durationMs = Double(samples.count) / Double(sampleRate) * 1000.0
785
+
786
+ let output: [String: Any] = [
787
+ "spectrogram": result["spectrogram"]!,
788
+ "sampleRate": sampleRate,
789
+ "nMels": nMels,
790
+ "timeSteps": timeSteps,
791
+ "durationMs": durationMs
792
+ ]
793
+
794
+ promise.resolve(output)
795
+ } catch {
796
+ promise.reject("SPECTROGRAM_ERROR", "Failed to extract mel spectrogram: \(error.localizedDescription)")
797
+ }
798
+ }
799
+
800
+ /// Gets available audio input devices with an optional refresh parameter
801
+ /// - Parameters:
802
+ /// - options: Optional dictionary containing a refresh parameter
803
+ /// - promise: A promise to resolve with a list of available audio input devices
804
+ AsyncFunction("getAvailableInputDevices") { (options: [String: Any]?, promise: Promise) in
805
+ Logger.debug("AudioStudioModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
806
+ if let options = options, let refresh = options["refresh"] as? Bool, refresh {
807
+ Logger.debug("AudioStudioModule", "Forcing refresh of audio devices")
808
+ _ = self.deviceManager.forceRefreshAudioSession()
809
+ }
810
+
811
+ // Call the device manager with the promise
812
+ self.deviceManager.getAvailableInputDevices(promise: promise)
813
+ }
814
+
815
+ /// Refreshes the audio session to detect newly connected devices
816
+ /// - Returns: Boolean indicating success
817
+ Function("refreshAudioDevices") {
818
+ Logger.debug("AudioStudioModule", "refreshAudioDevices called.")
819
+ let success = self.deviceManager.forceRefreshAudioSession()
820
+ Logger.debug("AudioStudioModule", "refreshAudioDevices result: \(success)")
821
+ return ["success": success]
822
+ }
823
+
824
+ /// Gets the currently selected audio input device
825
+ ///
826
+ /// - Parameters:
827
+ /// - promise: A promise to resolve with the currently selected audio input device
828
+ AsyncFunction("getCurrentInputDevice") { (promise: Promise) in
829
+ Logger.debug("AudioStudioModule", "getCurrentInputDevice called.")
830
+ self.deviceManager.getCurrentInputDevice(promise: promise)
831
+ }
832
+
833
+ /// Selects a specific audio input device for recording
834
+ ///
835
+ /// - Parameters:
836
+ /// - deviceId: The ID of the device to select
837
+ /// - promise: A promise to resolve with boolean indicating success
838
+ AsyncFunction("selectInputDevice") { (deviceId: String, promise: Promise) in
839
+ Logger.debug("AudioStudioModule", "selectInputDevice called with ID: \(deviceId)")
840
+ self.deviceManager.selectInputDevice(deviceId, promise: promise)
841
+ // Sync deviceId into recordingSettings so updateAudioSessionWithCurrentSettings can find the port
842
+ self.streamManager.recordingSettings?.deviceId = deviceId
843
+ // Update the audio recorder if recording is in progress or prepared
844
+ if self.streamManager.isRecording || self.streamManager.isPrepared {
845
+ Logger.debug("AudioStudioModule", "selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
846
+ self.streamManager.updateAudioSessionWithCurrentSettings()
847
+ } else {
848
+ Logger.debug("AudioStudioModule", "selectInputDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
849
+ }
850
+ }
851
+
852
+ /// Resets to the default audio input device
853
+ ///
854
+ /// - Parameters:
855
+ /// - promise: A promise to resolve with boolean indicating success
856
+ AsyncFunction("resetToDefaultDevice") { (promise: Promise) in
857
+ Logger.debug("AudioStudioModule", "resetToDefaultDevice called.")
858
+ self.deviceManager.resetToDefaultDevice { success, error in
859
+ if success {
860
+ // Clear stored deviceId so updateAudioSessionWithCurrentSettings won't bail early
861
+ self.streamManager.recordingSettings?.deviceId = nil
862
+ if self.streamManager.isRecording || self.streamManager.isPrepared {
863
+ Logger.debug("AudioStudioModule", "resetToDefaultDevice: Performing device switch to system default.")
864
+ // Bug 1 fix: call performDeviceSwitch(nil) directly — updateAudioSessionWithCurrentSettings
865
+ // would bail immediately because deviceId is now nil.
866
+ self.streamManager.performDeviceSwitch(port: nil)
867
+ } else {
868
+ Logger.debug("AudioStudioModule", "resetToDefaultDevice: Not recording/prepared, no engine action needed.")
869
+ }
870
+ promise.resolve(true)
871
+ } else {
872
+ Logger.error("AudioStudioModule", "resetToDefaultDevice failed: \(error?.localizedDescription ?? "Unknown error")")
873
+ promise.reject("DEVICE_ERROR", "Failed to reset to default device: \(error?.localizedDescription ?? "Unknown error")")
874
+ }
875
+ }
876
+ }
877
+ }
878
+
879
+ func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
880
+ Logger.debug("AudioStudioModule", "Delegate: didReceiveInterruption: \(info)")
881
+ // Convert iOS interruption events to match the TypeScript types
882
+ var reason: String
883
+ var isPaused: Bool = true
884
+
885
+ if let type = info["type"] as? String {
886
+ switch type {
887
+ case "began":
888
+ // Phone call or other audio session interruption began
889
+ reason = "audioFocusLoss"
890
+ case "ended":
891
+ reason = "audioFocusGain"
892
+ isPaused = false
893
+ // Check if this was from a phone call
894
+ if let wasSuspended = info["wasSuspended"] as? Bool, wasSuspended {
895
+ reason = "phoneCallEnded"
896
+ }
897
+ default:
898
+ return
899
+ }
900
+ } else if let specificReason = info["reason"] as? String {
901
+ // Handle specific reasons that are already properly formatted
902
+ reason = specificReason
903
+ isPaused = info["isPaused"] as? Bool ?? true
904
+ } else {
905
+ return
906
+ }
907
+
908
+ // Send event in the correct format
909
+ sendEvent(recordingInterruptedEvent, [
910
+ "reason": reason,
911
+ "isPaused": isPaused,
912
+ "timestamp": Date().timeIntervalSince1970 * 1000
913
+ ])
914
+ }
915
+
916
+ func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
917
+ Logger.debug("AudioStudioModule", "Delegate: didPauseRecording")
918
+ sendEvent(recordingInterruptedEvent, [
919
+ "reason": "userPaused",
920
+ "isPaused": true,
921
+ "timestamp": pauseTime.timeIntervalSince1970 * 1000
922
+ ])
923
+ }
924
+
925
+ func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
926
+ Logger.debug("AudioStudioModule", "Delegate: didResumeRecording")
927
+ sendEvent(recordingInterruptedEvent, [
928
+ "reason": "userResumed",
929
+ "isPaused": false,
930
+ "timestamp": resumeTime.timeIntervalSince1970 * 1000
931
+ ])
932
+ }
933
+
934
+ func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
935
+ Logger.debug("AudioStudioModule", "Delegate: didUpdateNotificationState: isPaused=\(isPaused)")
936
+ sendEvent(recordingInterruptedEvent, [
937
+ "reason": "notification",
938
+ "isPaused": isPaused,
939
+ "timestamp": Date().timeIntervalSince1970 * 1000
940
+ ])
941
+ }
942
+
943
+ /// Handles the reception of audio data from the AudioStreamManager.
944
+ ///
945
+ /// - Parameters:
946
+ /// - manager: The AudioStreamManager instance.
947
+ /// - data: The received audio data.
948
+ /// - recordingTime: The current recording time.
949
+ /// - totalDataSize: The total size of the received audio data.
950
+ func audioStreamManager(
951
+ _ manager: AudioStreamManager,
952
+ didReceiveAudioData data: Data,
953
+ recordingTime: TimeInterval,
954
+ totalDataSize: Int64,
955
+ compressionInfo: [String: Any]?
956
+ ) {
957
+ // Reduce log frequency or detail for this potentially high-frequency event
958
+ // Logger.debug("[AudioStudioModule] Delegate: didReceiveAudioData: size=\(data.count), totalSize=\(totalDataSize)")
959
+ var resultDict: [String: Any] = [
960
+ "fileUri": manager.recordingFileURL?.absoluteString ?? "",
961
+ "lastEmittedSize": totalDataSize,
962
+ "deltaSize": data.count,
963
+ "position": Int64(recordingTime * 1000),
964
+ "mimeType": manager.mimeType,
965
+ "totalSize": totalDataSize,
966
+ "streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
967
+ ]
968
+
969
+ if manager.recordingSettings?.streamFormat == "float32" {
970
+ let sampleCount = data.count / 2
971
+ var floatArray = [Float](repeating: 0, count: sampleCount)
972
+ data.withUnsafeBytes { ptr in
973
+ let int16Ptr = ptr.bindMemory(to: Int16.self)
974
+ for i in 0..<sampleCount {
975
+ floatArray[i] = Float(int16Ptr[i]) / 32768.0
976
+ }
977
+ }
978
+ resultDict["pcmFloat32"] = floatArray
979
+ } else {
980
+ resultDict["encoded"] = data.base64EncodedString()
981
+ }
982
+
983
+ if let compressionInfo = compressionInfo {
984
+ resultDict["compression"] = compressionInfo
985
+ }
986
+
987
+ sendEvent(audioDataEvent, resultDict)
988
+ }
989
+
990
+ private func requestNotificationPermissions() async -> Bool {
991
+ do {
992
+ let options: UNAuthorizationOptions = [.alert, .sound]
993
+ return try await notificationCenter.requestAuthorization(options: options)
994
+ } catch {
995
+ Logger.debug("AudioStudioModule", "Failed to request notification permissions: \(error)")
996
+ return false
997
+ }
998
+ }
999
+
1000
+ func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
1001
+ if let data = result {
1002
+ Logger.debug("AudioStudioModule", "Delegate: didReceiveProcessingResult: Received \(data.dataPoints.count) data points.")
1003
+ } else {
1004
+ Logger.debug("AudioStudioModule", "Delegate: didReceiveProcessingResult: Received nil result.")
1005
+ }
1006
+ let resultDict = result?.toDictionary() ?? [:]
1007
+ sendEvent(audioAnalysisEvent, resultDict)
1008
+ }
1009
+
1010
+ /// Checks microphone permission and calls the completion handler with the result.
1011
+ ///
1012
+ /// - Parameters:
1013
+ /// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
1014
+ private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
1015
+ switch AVAudioSession.sharedInstance().recordPermission {
1016
+ case .granted:
1017
+ DispatchQueue.main.async { completion(true) }
1018
+ case .denied:
1019
+ DispatchQueue.main.async { completion(false) }
1020
+ case .undetermined:
1021
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
1022
+ DispatchQueue.main.async {
1023
+ completion(granted)
1024
+ }
1025
+ }
1026
+ @unknown default:
1027
+ DispatchQueue.main.async { completion(false) }
1028
+ }
1029
+ }
1030
+
1031
+ /// Clears all audio files stored in the document directory.
1032
+ private func clearAudioFiles() {
1033
+ let fileURLs = listAudioFiles() // This now returns full URLs as strings
1034
+ fileURLs.forEach { fileURLString in
1035
+ if let fileURL = URL(string: fileURLString) {
1036
+ do {
1037
+ try FileManager.default.removeItem(at: fileURL)
1038
+ print("AudioStudioModule", "Removed file at:", fileURL.path)
1039
+ } catch {
1040
+ print("AudioStudioModule", "Error removing file at \(fileURL.path):", error.localizedDescription)
1041
+ }
1042
+ } else {
1043
+ print("AudioStudioModule", "Invalid URL string: \(fileURLString)")
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ /// Extracts feature options from the provided options dictionary.
1049
+ ///
1050
+ /// - Parameters:
1051
+ /// - options: The options dictionary containing feature flags.
1052
+ /// - Returns: A dictionary with feature flags and their boolean values.
1053
+ private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
1054
+ return [
1055
+ "energy": options["energy"] as? Bool ?? false,
1056
+ "mfcc": options["mfcc"] as? Bool ?? false,
1057
+ "rms": options["rms"] as? Bool ?? false,
1058
+ "zcr": options["zcr"] as? Bool ?? false,
1059
+ "dB": options["dB"] as? Bool ?? false,
1060
+ "spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
1061
+ "spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
1062
+ "spectralRolloff": options["spectralRolloff"] as? Bool ?? false,
1063
+ "spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
1064
+ "chromagram": options["chromagram"] as? Bool ?? false,
1065
+ "tempo": options["tempo"] as? Bool ?? false,
1066
+ "hnr": options["hnr"] as? Bool ?? false,
1067
+ "melSpectrogram": options["melSpectrogram"] as? Bool ?? false,
1068
+ "spectralContrast": options["spectralContrast"] as? Bool ?? false,
1069
+ "tonnetz": options["tonnetz"] as? Bool ?? false,
1070
+ "pitch": options["pitch"] as? Bool ?? false,
1071
+ "crc32": options["crc32"] as? Bool ?? false
1072
+ ]
1073
+ }
1074
+
1075
+ /// Lists all audio files stored in the document directory.
1076
+ ///
1077
+ /// - Returns: An array of file URIs as strings.
1078
+ func listAudioFiles() -> [String] {
1079
+ guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
1080
+ print("AudioStudioModule", "Failed to access document directory.")
1081
+ return []
1082
+ }
1083
+
1084
+ do {
1085
+ let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
1086
+ let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
1087
+ return audioFiles
1088
+ } catch {
1089
+ print("AudioStudioModule", "Error listing audio files:", error.localizedDescription)
1090
+ return []
1091
+ }
1092
+ }
1093
+
1094
+ func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
1095
+ Logger.error("AudioStudioModule", "Delegate: didFailWithError: \(error)")
1096
+ sendEvent(errorEvent, [ "message": error ])
1097
+ }
1098
+
1099
+ // MARK: - AudioDeviceManagerDelegate
1100
+
1101
+ /// Handles device disconnection events from the AudioDeviceManager
1102
+ func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String) {
1103
+ Logger.debug("AudioStudioModule", "Device disconnected: \(deviceId)")
1104
+
1105
+ // Emit device change event to match Android implementation
1106
+ sendEvent(deviceChangedEvent, [
1107
+ "type": "deviceDisconnected",
1108
+ "deviceId": deviceId
1109
+ ])
1110
+ }
1111
+ }