@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,911 @@
1
+ // src/AudioStudio.web.ts
2
+ import { LegacyEventEmitter } from 'expo-modules-core'
3
+
4
+ import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
5
+ import {
6
+ AudioRecording,
7
+ AudioStreamStatus,
8
+ BitDepth,
9
+ ConsoleLike,
10
+ RecordingConfig,
11
+ RecordingInterruptionReason,
12
+ StartRecordingResult,
13
+ } from './AudioStudio.types'
14
+ import { WebRecorder } from './WebRecorder.web'
15
+ import {
16
+ DEFAULT_BIT_DEPTH,
17
+ DEFAULT_INTERVAL_MS,
18
+ DEFAULT_ANALYSIS_INTERVAL_MS,
19
+ DEFAULT_MAX_BUFFER_SIZE,
20
+ } from './constants'
21
+ import { AudioEventPayload } from './events'
22
+ import { encodingToBitDepth } from './utils/encodingToBitDepth'
23
+
24
+ export interface AudioStreamEvent {
25
+ type: string
26
+ device?: string
27
+ timestamp: Date
28
+ }
29
+
30
+ export interface AudioStudioOptions {
31
+ logger?: ConsoleLike
32
+ eventCallback?: (event: AudioStreamEvent) => void
33
+ }
34
+
35
+ export interface EmitAudioEventProps {
36
+ data: Float32Array
37
+ position: number
38
+ compression?: {
39
+ data: Blob
40
+ size: number
41
+ totalSize: number
42
+ mimeType: string
43
+ format: string
44
+ bitrate: number
45
+ }
46
+ }
47
+ export type EmitAudioEventFunction = (_: EmitAudioEventProps) => void
48
+ export type EmitAudioAnalysisFunction = (_: AudioAnalysis) => void
49
+
50
+ export interface AudioStudioWebProps {
51
+ logger?: ConsoleLike
52
+ audioWorkletUrl: string
53
+ featuresExtratorUrl: string
54
+ maxBufferSize?: number // Maximum number of chunks to keep in memory
55
+ }
56
+
57
+ export class AudioStudioWeb extends LegacyEventEmitter {
58
+ customRecorder: WebRecorder | null
59
+ audioChunks: Float32Array[]
60
+ isRecording: boolean
61
+ isPaused: boolean
62
+ recordingStartTime: number
63
+ pausedTime: number
64
+ currentDurationMs: number
65
+ currentSize: number
66
+ currentInterval: number
67
+ currentIntervalAnalysis: number
68
+ lastEmittedSize: number
69
+ lastEmittedTime: number
70
+ lastEmittedCompressionSize: number
71
+ lastEmittedAnalysisTime: number
72
+ streamUuid: string | null
73
+ extension: 'webm' | 'wav' = 'wav' // Default extension is 'wav'
74
+ recordingConfig?: RecordingConfig
75
+ bitDepth: BitDepth // Bit depth of the audio
76
+ audioWorkletUrl: string
77
+ featuresExtratorUrl: string
78
+ logger?: ConsoleLike
79
+ latestPosition: number = 0
80
+ totalCompressedSize: number = 0
81
+ private readonly maxBufferSize: number
82
+ private eventCallback?: (event: AudioStreamEvent) => void
83
+
84
+ constructor({
85
+ audioWorkletUrl,
86
+ featuresExtratorUrl,
87
+ logger,
88
+ maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
89
+ }: AudioStudioWebProps) {
90
+ const mockNativeModule = {
91
+ addListener: () => {},
92
+ removeListeners: () => {},
93
+ }
94
+ super(mockNativeModule) // Pass the mock native module to the parent class
95
+
96
+ this.logger = logger
97
+ this.customRecorder = null
98
+ this.audioChunks = []
99
+ this.isRecording = false
100
+ this.isPaused = false
101
+ this.recordingStartTime = 0
102
+ this.pausedTime = 0
103
+ this.currentDurationMs = 0
104
+ this.currentSize = 0
105
+ this.bitDepth = DEFAULT_BIT_DEPTH
106
+ this.currentInterval = DEFAULT_INTERVAL_MS
107
+ this.currentIntervalAnalysis = DEFAULT_ANALYSIS_INTERVAL_MS
108
+ this.lastEmittedSize = 0
109
+ this.lastEmittedTime = 0
110
+ this.latestPosition = 0
111
+ this.lastEmittedCompressionSize = 0
112
+ this.lastEmittedAnalysisTime = 0
113
+ this.streamUuid = null // Initialize UUID on first recording start
114
+ this.audioWorkletUrl = audioWorkletUrl
115
+ this.featuresExtratorUrl = featuresExtratorUrl
116
+ this.maxBufferSize = maxBufferSize
117
+ }
118
+
119
+ // Utility to handle user media stream
120
+ async getMediaStream() {
121
+ try {
122
+ this.logger?.debug('Requesting user media (microphone)...')
123
+
124
+ // First check if the browser supports the necessary audio APIs
125
+ if (!navigator?.mediaDevices?.getUserMedia) {
126
+ this.logger?.error(
127
+ 'Browser does not support mediaDevices.getUserMedia'
128
+ )
129
+ throw new Error('Browser does not support audio recording')
130
+ }
131
+
132
+ // Get media with detailed audio constraints for better diagnostics
133
+ const constraints = {
134
+ audio: {
135
+ echoCancellation: true,
136
+ noiseSuppression: true,
137
+ autoGainControl: true,
138
+ // Add deviceId constraint if specified
139
+ ...(this.recordingConfig?.deviceId
140
+ ? {
141
+ deviceId: {
142
+ exact: this.recordingConfig.deviceId,
143
+ },
144
+ }
145
+ : {}),
146
+ },
147
+ }
148
+
149
+ this.logger?.debug('Media constraints:', constraints)
150
+
151
+ const stream =
152
+ await navigator.mediaDevices.getUserMedia(constraints)
153
+
154
+ // Get detailed info about the audio track for debugging
155
+ const audioTracks = stream.getAudioTracks()
156
+ if (audioTracks.length > 0) {
157
+ const track = audioTracks[0]
158
+ const settings = track.getSettings()
159
+ this.logger?.debug('Audio track obtained:', {
160
+ label: track.label,
161
+ id: track.id,
162
+ enabled: track.enabled,
163
+ muted: track.muted,
164
+ readyState: track.readyState,
165
+ settings,
166
+ })
167
+ } else {
168
+ this.logger?.warn('Stream has no audio tracks!')
169
+ }
170
+
171
+ return stream
172
+ } catch (error) {
173
+ this.logger?.error('Failed to get media stream:', error)
174
+ throw error
175
+ }
176
+ }
177
+
178
+ // Prepare recording with options
179
+ async prepareRecording(
180
+ recordingConfig: RecordingConfig = {}
181
+ ): Promise<boolean> {
182
+ if (this.isRecording) {
183
+ this.logger?.warn(
184
+ 'Cannot prepare: Recording is already in progress'
185
+ )
186
+ return false
187
+ }
188
+
189
+ try {
190
+ // Check permissions and initialize basic settings
191
+ await this.getMediaStream().then((stream) => {
192
+ // Just verify we can access the microphone by getting a stream, then release it
193
+ stream.getTracks().forEach((track) => track.stop())
194
+ })
195
+
196
+ this.bitDepth = encodingToBitDepth({
197
+ encoding: recordingConfig.encoding ?? 'pcm_32bit',
198
+ })
199
+
200
+ // Store recording configuration for later use
201
+ this.recordingConfig = recordingConfig
202
+
203
+ // Use custom filename if provided, otherwise fallback to timestamp
204
+ if (recordingConfig.filename) {
205
+ // Remove any existing extension from the filename
206
+ this.streamUuid = recordingConfig.filename.replace(
207
+ /\.[^/.]+$/,
208
+ ''
209
+ )
210
+ } else {
211
+ this.streamUuid = Date.now().toString()
212
+ }
213
+
214
+ this.logger?.debug('Recording preparation completed successfully')
215
+ return true
216
+ } catch (error) {
217
+ this.logger?.error('Error preparing recording:', error)
218
+ return false
219
+ }
220
+ }
221
+
222
+ // Start recording with options
223
+ async startRecording(
224
+ recordingConfig: RecordingConfig = {}
225
+ ): Promise<StartRecordingResult> {
226
+ if (this.isRecording) {
227
+ throw new Error('Recording is already in progress')
228
+ }
229
+
230
+ // If we haven't prepared or have different settings, prepare now
231
+ if (
232
+ !this.recordingConfig ||
233
+ this.recordingConfig.sampleRate !== recordingConfig.sampleRate ||
234
+ this.recordingConfig.channels !== recordingConfig.channels ||
235
+ this.recordingConfig.encoding !== recordingConfig.encoding
236
+ ) {
237
+ await this.prepareRecording(recordingConfig)
238
+ } else {
239
+ this.logger?.debug(
240
+ 'Using previously prepared recording configuration'
241
+ )
242
+ }
243
+
244
+ // Save recording config for reference
245
+ this.recordingConfig = recordingConfig
246
+
247
+ const audioContext = new (window.AudioContext ||
248
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
249
+ // @ts-ignore - Allow webkitAudioContext for Safari
250
+ window.webkitAudioContext)()
251
+ const stream = await this.getMediaStream()
252
+
253
+ const source = audioContext.createMediaStreamSource(stream)
254
+
255
+ this.customRecorder = new WebRecorder({
256
+ logger: this.logger,
257
+ audioContext,
258
+ source,
259
+ recordingConfig,
260
+ emitAudioEventCallback: this.customRecorderEventCallback.bind(this),
261
+ emitAudioAnalysisCallback:
262
+ this.customRecorderAnalysisCallback.bind(this),
263
+ onInterruption: this.handleRecordingInterruption.bind(this),
264
+ })
265
+ await this.customRecorder.init()
266
+ this.customRecorder.start()
267
+
268
+ this.isRecording = true
269
+ this.recordingStartTime = Date.now()
270
+ this.pausedTime = 0
271
+ this.isPaused = false
272
+ this.lastEmittedSize = 0
273
+ this.lastEmittedTime = 0
274
+ this.lastEmittedCompressionSize = 0
275
+ this.currentInterval = recordingConfig.interval ?? 1000
276
+ this.currentIntervalAnalysis = recordingConfig.intervalAnalysis ?? 500
277
+ this.lastEmittedAnalysisTime = Date.now()
278
+
279
+ // Use custom filename if provided, otherwise fallback to timestamp
280
+ if (recordingConfig.filename) {
281
+ // Remove any existing extension from the filename
282
+ this.streamUuid = recordingConfig.filename.replace(/\.[^/.]+$/, '')
283
+ } else {
284
+ this.streamUuid = Date.now().toString()
285
+ }
286
+
287
+ const fileUri = `${this.streamUuid}.${this.extension}`
288
+ const streamConfig: StartRecordingResult = {
289
+ fileUri,
290
+ mimeType: `audio/${this.extension}`,
291
+ bitDepth: this.bitDepth,
292
+ channels: recordingConfig.channels ?? 1,
293
+ sampleRate: recordingConfig.sampleRate ?? 44100,
294
+ compression: recordingConfig.output?.compressed?.enabled
295
+ ? {
296
+ ...recordingConfig.output.compressed,
297
+ bitrate:
298
+ recordingConfig.output.compressed.bitrate ?? 128000,
299
+ size: 0,
300
+ mimeType: 'audio/webm',
301
+ format:
302
+ recordingConfig.output.compressed.format ?? 'opus',
303
+ compressedFileUri: '',
304
+ }
305
+ : undefined,
306
+ }
307
+ return streamConfig
308
+ }
309
+
310
+ /**
311
+ * Centralized handler for recording interruptions
312
+ */
313
+ private handleRecordingInterruption(event: {
314
+ reason: RecordingInterruptionReason | string
315
+ isPaused: boolean
316
+ timestamp: number
317
+ message?: string
318
+ }): void {
319
+ this.logger?.debug(`Received recording interruption: ${event.reason}`)
320
+
321
+ // Update local state if the interruption should pause recording
322
+ if (event.isPaused) {
323
+ this.isPaused = true
324
+
325
+ // If this is a device disconnection, handle according to behavior setting
326
+ if (event.reason === 'deviceDisconnected') {
327
+ this.pausedTime = Date.now()
328
+
329
+ // Check if we should try fallback to another device
330
+ if (
331
+ this.recordingConfig?.deviceDisconnectionBehavior ===
332
+ 'fallback'
333
+ ) {
334
+ this.logger?.debug(
335
+ 'Device disconnected with fallback behavior - attempting to switch to default device'
336
+ )
337
+
338
+ // Try to restart with default device
339
+ this.handleDeviceFallback().catch((error) => {
340
+ // If fallback fails, emit warning
341
+ this.logger?.error('Device fallback failed:', error)
342
+ this.emit('onRecordingInterrupted', {
343
+ reason: 'deviceSwitchFailed',
344
+ isPaused: true,
345
+ timestamp: Date.now(),
346
+ message:
347
+ 'Failed to switch to fallback device. Recording paused.',
348
+ })
349
+ })
350
+ } else {
351
+ // Just warn about disconnection if fallback not enabled
352
+ this.logger?.warn(
353
+ 'Device disconnected - recording paused automatically'
354
+ )
355
+ this.emit('onRecordingInterrupted', event)
356
+ }
357
+ } else {
358
+ // For other interruption types, just emit the event
359
+ this.emit('onRecordingInterrupted', event)
360
+ }
361
+ } else {
362
+ // If not causing a pause, just forward the event
363
+ this.emit('onRecordingInterrupted', event)
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Handler for audio events from the WebRecorder
369
+ */
370
+ private customRecorderEventCallback({
371
+ data,
372
+ position,
373
+ compression,
374
+ }: EmitAudioEventProps): void {
375
+ // Keep only the latest chunks based on maxBufferSize
376
+ this.audioChunks.push(new Float32Array(data))
377
+ if (this.audioChunks.length > this.maxBufferSize) {
378
+ this.audioChunks.shift() // Remove oldest chunk
379
+ }
380
+ this.currentSize += data.byteLength
381
+ this.emitAudioEvent({ data, position, compression })
382
+ this.lastEmittedTime = Date.now()
383
+ this.lastEmittedSize = this.currentSize
384
+ this.lastEmittedCompressionSize = compression?.size ?? 0
385
+ }
386
+
387
+ /**
388
+ * Handler for audio analysis events from the WebRecorder
389
+ */
390
+ private customRecorderAnalysisCallback(
391
+ audioAnalysisData: AudioAnalysis
392
+ ): void {
393
+ this.emit('AudioAnalysis', audioAnalysisData)
394
+ }
395
+
396
+ // Get recording duration
397
+ private getRecordingDuration(): number {
398
+ if (!this.isRecording) {
399
+ return 0
400
+ }
401
+
402
+ return this.currentDurationMs
403
+ }
404
+
405
+ emitAudioEvent({ data, position, compression }: EmitAudioEventProps) {
406
+ const fileUri = `${this.streamUuid}.${this.extension}`
407
+ if (compression?.size) {
408
+ this.lastEmittedCompressionSize = compression.size
409
+ this.totalCompressedSize = compression.totalSize
410
+ }
411
+
412
+ // Update latest position for tracking
413
+ this.latestPosition = position
414
+
415
+ // Calculate duration of this chunk in ms
416
+ const sampleRate = this.recordingConfig?.sampleRate || 44100
417
+ const chunkDurationMs = (data.length / sampleRate) * 1000
418
+
419
+ // Handle duration calculation
420
+ if (this.customRecorder?.isFirstChunkAfterSwitch) {
421
+ this.logger?.debug(
422
+ `Processing first chunk after device switch, duration preserved at ${this.currentDurationMs}ms`
423
+ )
424
+ this.customRecorder.isFirstChunkAfterSwitch = false
425
+ } else {
426
+ this.currentDurationMs += chunkDurationMs
427
+ }
428
+
429
+ const audioEventPayload: AudioEventPayload = {
430
+ fileUri,
431
+ mimeType: `audio/${this.extension}`,
432
+ lastEmittedSize: this.lastEmittedSize,
433
+ deltaSize: data.byteLength,
434
+ position,
435
+ totalSize: this.currentSize,
436
+ buffer: data,
437
+ streamUuid: this.streamUuid ?? '',
438
+ compression: compression
439
+ ? {
440
+ data: compression?.data,
441
+ totalSize: this.totalCompressedSize,
442
+ eventDataSize: compression?.size ?? 0,
443
+ position,
444
+ }
445
+ : undefined,
446
+ }
447
+
448
+ this.emit('AudioData', audioEventPayload)
449
+ }
450
+
451
+ // Stop recording
452
+ async stopRecording(): Promise<AudioRecording> {
453
+ if (!this.customRecorder) {
454
+ throw new Error('Recorder is not initialized')
455
+ }
456
+
457
+ this.logger?.debug('Starting stop process')
458
+
459
+ try {
460
+ const { compressedBlob, uncompressedBlob } =
461
+ await this.customRecorder.stop()
462
+
463
+ this.isRecording = false
464
+ this.isPaused = false
465
+
466
+ let compression: AudioRecording['compression']
467
+ let fileUri = `${this.streamUuid}.${this.extension}`
468
+ let mimeType = `audio/${this.extension}`
469
+
470
+ // Handle both compressed and uncompressed blobs according to new output configuration
471
+ const primaryEnabled =
472
+ this.recordingConfig?.output?.primary?.enabled ?? true
473
+ const compressedEnabled =
474
+ this.recordingConfig?.output?.compressed?.enabled ?? false
475
+
476
+ // Process compressed blob if available and enabled
477
+ if (compressedBlob && compressedEnabled) {
478
+ const compressedUri = URL.createObjectURL(compressedBlob)
479
+ const compressedInfo = {
480
+ compressedFileUri: compressedUri,
481
+ size: compressedBlob.size,
482
+ mimeType: 'audio/webm',
483
+ format:
484
+ this.recordingConfig?.output?.compressed?.format ??
485
+ 'opus',
486
+ bitrate:
487
+ this.recordingConfig?.output?.compressed?.bitrate ??
488
+ 128000,
489
+ }
490
+
491
+ // Store compression info
492
+ compression = compressedInfo
493
+
494
+ // If primary is disabled, use compressed as main file
495
+ if (!primaryEnabled) {
496
+ this.logger?.debug(
497
+ 'Using compressed audio as primary output (primary disabled)'
498
+ )
499
+ fileUri = compressedUri
500
+ mimeType = 'audio/webm'
501
+ }
502
+ }
503
+
504
+ // Process uncompressed WAV if available and primary is enabled
505
+ if (uncompressedBlob && primaryEnabled) {
506
+ const wavUri = URL.createObjectURL(uncompressedBlob)
507
+ fileUri = wavUri
508
+ mimeType = 'audio/wav'
509
+ } else if (!primaryEnabled && !compressedEnabled) {
510
+ // No outputs enabled - streaming only mode
511
+ this.logger?.debug('No outputs enabled - streaming only mode')
512
+ fileUri = ''
513
+ mimeType = 'audio/wav'
514
+ }
515
+
516
+ // Use the stored streamUuid for the final filename
517
+ const filename = fileUri
518
+ ? `${this.streamUuid}.${this.extension}`
519
+ : 'stream-only'
520
+ const result: AudioRecording = {
521
+ fileUri,
522
+ filename,
523
+ bitDepth: this.bitDepth,
524
+ createdAt: this.recordingStartTime,
525
+ channels: this.recordingConfig?.channels ?? 1,
526
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
527
+ durationMs: this.currentDurationMs,
528
+ size: primaryEnabled ? this.currentSize : 0,
529
+ mimeType,
530
+ compression,
531
+ }
532
+
533
+ // Reset after creating the result
534
+ this.streamUuid = null
535
+
536
+ // Reset recording state variables to prepare for next recording
537
+ this.currentDurationMs = 0
538
+ this.currentSize = 0
539
+ this.lastEmittedSize = 0
540
+ this.totalCompressedSize = 0
541
+ this.lastEmittedCompressionSize = 0
542
+ this.audioChunks = []
543
+
544
+ return result
545
+ } catch (error) {
546
+ this.logger?.error('Error stopping recording:', error)
547
+ throw error
548
+ }
549
+ }
550
+
551
+ // Pause recording
552
+ async pauseRecording() {
553
+ if (!this.isRecording) {
554
+ throw new Error('Recording is not active')
555
+ }
556
+
557
+ if (this.isPaused) {
558
+ this.logger?.debug('Recording already paused, skipping')
559
+ return
560
+ }
561
+
562
+ try {
563
+ if (this.customRecorder) {
564
+ this.customRecorder.pause()
565
+ }
566
+ this.isPaused = true
567
+ this.pausedTime = Date.now()
568
+ } catch (error) {
569
+ this.logger?.error('Error in pauseRecording', error)
570
+ // Even if the pause operation failed, make sure our state is consistent
571
+ this.isPaused = true
572
+ this.pausedTime = Date.now()
573
+ }
574
+ }
575
+
576
+ // Resume recording
577
+ async resumeRecording() {
578
+ if (!this.isPaused) {
579
+ throw new Error('Recording is not paused')
580
+ }
581
+
582
+ this.logger?.debug('Resuming recording', {
583
+ deviceDisconnectionBehavior:
584
+ this.recordingConfig?.deviceDisconnectionBehavior,
585
+ isDeviceDisconnected: this.customRecorder?.isDeviceDisconnected,
586
+ })
587
+
588
+ try {
589
+ // If we have no recorder, or if the device is disconnected, always attempt fallback
590
+ if (
591
+ !this.customRecorder ||
592
+ this.customRecorder.isDeviceDisconnected
593
+ ) {
594
+ this.logger?.debug(
595
+ 'No recorder exists or device disconnected - attempting fallback on resume'
596
+ )
597
+ await this.handleDeviceFallback()
598
+ // handleDeviceFallback will manage resuming if successful, or emit error if failed.
599
+ return
600
+ }
601
+
602
+ // Normal resume path - device is still connected
603
+ this.customRecorder.resume()
604
+ this.isPaused = false
605
+
606
+ // Adjust the recording start time to account for the pause duration
607
+ const pauseDuration = Date.now() - this.pausedTime
608
+ this.recordingStartTime += pauseDuration
609
+ this.pausedTime = 0
610
+
611
+ this.emit('onRecordingInterrupted', {
612
+ reason: 'userResumed',
613
+ isPaused: false,
614
+ timestamp: Date.now(),
615
+ })
616
+ } catch (error) {
617
+ this.logger?.error('Resume failed:', error)
618
+ // Fallback to emitting a general failure if resume fails unexpectedly
619
+ this.emit('onRecordingInterrupted', {
620
+ reason: 'resumeFailed', // Use a more specific reason
621
+ isPaused: true, // Remain paused if resume fails
622
+ timestamp: Date.now(),
623
+ message:
624
+ 'Failed to resume recording. Please stop and start again.',
625
+ })
626
+ }
627
+ }
628
+
629
+ // Get current status
630
+ status() {
631
+ const durationMs = this.getRecordingDuration()
632
+
633
+ const status: AudioStreamStatus = {
634
+ isRecording: this.isRecording,
635
+ isPaused: this.isPaused,
636
+ durationMs,
637
+ size: this.currentSize,
638
+ interval: this.currentInterval,
639
+ intervalAnalysis: this.currentIntervalAnalysis,
640
+ mimeType: `audio/${this.extension}`,
641
+ compression: this.recordingConfig?.output?.compressed?.enabled
642
+ ? {
643
+ size: this.totalCompressedSize,
644
+ mimeType: 'audio/webm',
645
+ format:
646
+ this.recordingConfig.output.compressed.format ??
647
+ 'opus',
648
+ bitrate:
649
+ this.recordingConfig.output.compressed.bitrate ??
650
+ 128000,
651
+ compressedFileUri: `${this.streamUuid}.webm`,
652
+ }
653
+ : undefined,
654
+ }
655
+ return status
656
+ }
657
+
658
+ /**
659
+ * Handles device fallback when the current device is disconnected
660
+ */
661
+ private async handleDeviceFallback(): Promise<boolean> {
662
+ this.logger?.debug('Starting device fallback procedure')
663
+
664
+ if (!this.isRecording) {
665
+ return false
666
+ }
667
+
668
+ try {
669
+ // Save important state before switching
670
+ const currentPosition = this.latestPosition
671
+ const existingAudioChunks = [...this.audioChunks]
672
+
673
+ // Save compressed chunks if available
674
+ let compressedChunks: Blob[] = []
675
+ if (this.customRecorder) {
676
+ try {
677
+ compressedChunks = this.customRecorder.getCompressedChunks()
678
+ } catch (err) {
679
+ this.logger?.warn('Failed to get compressed chunks:', err)
680
+ }
681
+ }
682
+
683
+ // Save the current counter value for continuity
684
+ let currentDataPointCounter = 0
685
+ if (this.customRecorder) {
686
+ currentDataPointCounter =
687
+ this.customRecorder.getDataPointCounter()
688
+ }
689
+
690
+ // Clean up existing recorder
691
+ if (this.customRecorder) {
692
+ try {
693
+ this.customRecorder.cleanup()
694
+ } catch (cleanupError) {
695
+ this.logger?.warn('Error during cleanup:', cleanupError)
696
+ }
697
+ }
698
+
699
+ // Keep recording state true but mark as paused
700
+ this.isPaused = true
701
+ this.pausedTime = Date.now()
702
+
703
+ // Store current size and other stats
704
+ const previousTotalSize = this.currentSize
705
+ const previousLastEmittedSize = this.lastEmittedSize
706
+ const previousCompressedSize = this.totalCompressedSize
707
+
708
+ // Try to get a fallback device
709
+ const fallbackDeviceInfo = await this.getFallbackDevice()
710
+ if (!fallbackDeviceInfo) {
711
+ this.emit('onRecordingInterrupted', {
712
+ reason: 'deviceSwitchFailed',
713
+ isPaused: true,
714
+ timestamp: Date.now(),
715
+ message:
716
+ 'Failed to switch to fallback device. Recording paused.',
717
+ })
718
+ return false
719
+ }
720
+
721
+ // Start recording with the new device
722
+ try {
723
+ const stream = await this.requestPermissionsAndGetUserMedia(
724
+ fallbackDeviceInfo.deviceId
725
+ )
726
+ const audioContext = new (window.AudioContext ||
727
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
728
+ // @ts-ignore - Allow webkitAudioContext for Safari
729
+ window.webkitAudioContext)()
730
+
731
+ const source = audioContext.createMediaStreamSource(stream)
732
+
733
+ // Create a new recorder with the fallback device
734
+ this.customRecorder = new WebRecorder({
735
+ logger: this.logger,
736
+ audioContext,
737
+ source,
738
+ recordingConfig: this.recordingConfig || {},
739
+ emitAudioEventCallback:
740
+ this.customRecorderEventCallback.bind(this),
741
+ emitAudioAnalysisCallback:
742
+ this.customRecorderAnalysisCallback.bind(this),
743
+ onInterruption: this.handleRecordingInterruption.bind(this),
744
+ })
745
+
746
+ await this.customRecorder.init()
747
+
748
+ // Set the initial position to continue from the previous device
749
+ this.customRecorder.setPosition(currentPosition)
750
+
751
+ // Reset the data point counter to continue from where the previous device left off
752
+ if (currentDataPointCounter > 0) {
753
+ this.customRecorder.resetDataPointCounter(
754
+ currentDataPointCounter
755
+ )
756
+ }
757
+
758
+ // Prepare the recorder to handle the device switch properly
759
+ this.customRecorder.prepareForDeviceSwitch()
760
+
761
+ // Restore the existing audio chunks
762
+ if (existingAudioChunks.length > 0) {
763
+ this.audioChunks = existingAudioChunks
764
+ }
765
+
766
+ // Restore compressed chunks if available
767
+ if (compressedChunks.length > 0) {
768
+ this.customRecorder.setCompressedChunks(compressedChunks)
769
+ }
770
+
771
+ // Start the new recorder while preserving counters
772
+ this.customRecorder.start(true)
773
+
774
+ // Update recording state
775
+ this.isPaused = false
776
+ this.recordingStartTime = Date.now()
777
+
778
+ // Restore size counters to maintain continuity
779
+ this.currentSize = previousTotalSize
780
+ this.lastEmittedSize = previousLastEmittedSize
781
+ this.totalCompressedSize = previousCompressedSize
782
+
783
+ // Notify that we switched to a fallback device
784
+ if (this.eventCallback) {
785
+ this.eventCallback({
786
+ type: 'deviceFallback',
787
+ device: fallbackDeviceInfo.deviceId,
788
+ timestamp: new Date(),
789
+ })
790
+ }
791
+ return true
792
+ } catch (error) {
793
+ this.logger?.error(
794
+ 'Failed to start recording with fallback device',
795
+ error
796
+ )
797
+ this.isPaused = true
798
+ this.emit('onRecordingInterrupted', {
799
+ reason: 'deviceSwitchFailed',
800
+ isPaused: true,
801
+ timestamp: Date.now(),
802
+ message:
803
+ 'Failed to switch to fallback device. Recording paused.',
804
+ })
805
+ return false
806
+ }
807
+ } catch (error) {
808
+ this.logger?.error('Failed to use fallback device', error)
809
+ this.isPaused = true
810
+ this.emit('onRecordingInterrupted', {
811
+ reason: 'deviceSwitchFailed',
812
+ isPaused: true,
813
+ timestamp: Date.now(),
814
+ message:
815
+ 'Failed to switch to fallback device. Recording paused.',
816
+ })
817
+ return false
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Attempts to get a fallback audio device
823
+ */
824
+ private async getFallbackDevice(): Promise<MediaDeviceInfo | null> {
825
+ try {
826
+ // Get list of available audio input devices
827
+ const devices = await navigator.mediaDevices.enumerateDevices()
828
+ const audioInputDevices = devices.filter(
829
+ (device) => device.kind === 'audioinput'
830
+ )
831
+
832
+ if (audioInputDevices.length === 0) {
833
+ return null
834
+ }
835
+
836
+ // Try to find a device that's not the current one
837
+ if (this.customRecorder) {
838
+ try {
839
+ // Use mediaDevices.enumerateDevices to find the current active device
840
+ const tracks = navigator.mediaDevices
841
+ .getUserMedia({ audio: true })
842
+ .then((stream) => {
843
+ const track = stream.getAudioTracks()[0]
844
+ return track ? track.label : ''
845
+ })
846
+ .catch(() => '')
847
+
848
+ const currentTrackLabel = await tracks
849
+
850
+ if (currentTrackLabel) {
851
+ // Find a device with a different label
852
+ const differentDevice = audioInputDevices.find(
853
+ (device) =>
854
+ device.label &&
855
+ device.label !== currentTrackLabel
856
+ )
857
+
858
+ if (differentDevice) {
859
+ return differentDevice
860
+ }
861
+ }
862
+ } catch (err) {
863
+ this.logger?.warn(
864
+ 'Error determining current device, using default',
865
+ err
866
+ )
867
+ }
868
+ }
869
+
870
+ // Return the first available device (default device)
871
+ return audioInputDevices[0]
872
+ } catch (error) {
873
+ this.logger?.error('Error finding fallback device:', error)
874
+ return null
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Gets user media with specific device ID
880
+ */
881
+ private async requestPermissionsAndGetUserMedia(
882
+ deviceId: string
883
+ ): Promise<MediaStream> {
884
+ try {
885
+ // Request the specific device
886
+ return await navigator.mediaDevices.getUserMedia({
887
+ audio: {
888
+ deviceId: { exact: deviceId },
889
+ },
890
+ })
891
+ } catch (error) {
892
+ this.logger?.error(
893
+ `Failed to get media for device ${deviceId}`,
894
+ error
895
+ )
896
+ // Try with default constraints as fallback
897
+ return await navigator.mediaDevices.getUserMedia({ audio: true })
898
+ }
899
+ }
900
+
901
+ init(options?: AudioStudioOptions): Promise<void> {
902
+ try {
903
+ this.logger = options?.logger
904
+ this.eventCallback = options?.eventCallback
905
+ return Promise.resolve()
906
+ } catch (error) {
907
+ this.logger?.error('Error initializing AudioStudio', error)
908
+ return Promise.reject(error)
909
+ }
910
+ }
911
+ }