@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,861 @@
1
+ // packages/audio-studio/src/WebRecorder.web.ts
2
+ import { initMelStreamingWasm, computeMelFrameWasm, } from './AudioAnalysis/melSpectrogramWasm';
3
+ import { encodingToBitDepth } from './utils/encodingToBitDepth';
4
+ import { writeWavHeader } from './utils/writeWavHeader';
5
+ import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web';
6
+ import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web';
7
+ import { wasmGlueJs } from './workers/wasmGlueString.web';
8
+ const DEFAULT_WEB_BITDEPTH = 32;
9
+ const DEFAULT_SEGMENT_DURATION_MS = 100;
10
+ const DEFAULT_WEB_INTERVAL = 500;
11
+ const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
12
+ const TAG = 'WebRecorder';
13
+ export class WebRecorder {
14
+ audioContext;
15
+ audioWorkletNode;
16
+ featureExtractorWorker;
17
+ featureExtractorWorkerUrl;
18
+ source;
19
+ emitAudioEventCallback;
20
+ emitAudioAnalysisCallback;
21
+ config;
22
+ position = 0;
23
+ numberOfChannels; // Number of audio channels
24
+ bitDepth; // Bit depth of the audio
25
+ exportBitDepth; // Bit depth of the audio
26
+ audioAnalysisData; // Keep updating the full audio analysis data with latest events
27
+ logger;
28
+ compressedMediaRecorder = null;
29
+ compressedChunks = [];
30
+ compressedSize = 0;
31
+ pendingCompressedChunk = null;
32
+ dataPointIdCounter = 0; // Add this property to track the counter
33
+ deviceDisconnectionHandler = null;
34
+ mediaStream = null;
35
+ onInterruptionCallback;
36
+ _isDeviceDisconnected = false;
37
+ pcmData = null; // Store original PCM data
38
+ totalSampleCount = 0;
39
+ melWasmReady = false;
40
+ melWasmInitPromise = null;
41
+ melPendingChunks = [];
42
+ pendingMelFrames = []; // Queued mel frames to attach to datapoints
43
+ /**
44
+ * Flag to indicate whether this is the first audio chunk after a device switch
45
+ * Used to maintain proper duration counting
46
+ */
47
+ isFirstChunkAfterSwitch = false;
48
+ /**
49
+ * Gets whether the recording device has been disconnected
50
+ */
51
+ get isDeviceDisconnected() {
52
+ return this._isDeviceDisconnected;
53
+ }
54
+ /**
55
+ * Initializes a new WebRecorder instance for audio recording and processing
56
+ * @param audioContext - The AudioContext to use for recording
57
+ * @param source - The MediaStreamAudioSourceNode providing the audio input
58
+ * @param recordingConfig - Configuration options for the recording
59
+ * @param emitAudioEventCallback - Callback function for audio data events
60
+ * @param emitAudioAnalysisCallback - Callback function for audio analysis events
61
+ * @param onInterruption - Callback for recording interruptions
62
+ * @param logger - Optional logger for debugging information
63
+ */
64
+ constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, onInterruption, logger, }) {
65
+ this.audioContext = audioContext;
66
+ this.source = source;
67
+ this.emitAudioEventCallback = emitAudioEventCallback;
68
+ this.emitAudioAnalysisCallback = emitAudioAnalysisCallback;
69
+ this.config = recordingConfig;
70
+ this.logger = logger;
71
+ const audioContextFormat = this.checkAudioContextFormat({
72
+ sampleRate: this.audioContext.sampleRate,
73
+ });
74
+ this.logger?.debug('Initialized WebRecorder with config:', {
75
+ sampleRate: audioContextFormat.sampleRate,
76
+ bitDepth: audioContextFormat.bitDepth,
77
+ numberOfChannels: audioContextFormat.numberOfChannels,
78
+ });
79
+ this.bitDepth = audioContextFormat.bitDepth;
80
+ this.numberOfChannels =
81
+ audioContextFormat.numberOfChannels ||
82
+ DEFAULT_WEB_NUMBER_OF_CHANNELS; // Default to 1 if not available
83
+ this.exportBitDepth =
84
+ encodingToBitDepth({
85
+ encoding: recordingConfig.encoding ?? 'pcm_32bit',
86
+ }) ||
87
+ audioContextFormat.bitDepth ||
88
+ DEFAULT_WEB_BITDEPTH;
89
+ this.audioAnalysisData = {
90
+ amplitudeRange: { min: 0, max: 0 },
91
+ rmsRange: { min: 0, max: 0 },
92
+ dataPoints: [],
93
+ durationMs: 0,
94
+ samples: 0,
95
+ bitDepth: this.bitDepth,
96
+ numberOfChannels: this.numberOfChannels,
97
+ sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
98
+ segmentDurationMs: this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms segments
99
+ extractionTimeMs: 0,
100
+ };
101
+ if (recordingConfig.enableProcessing) {
102
+ this.initFeatureExtractorWorker();
103
+ if (recordingConfig.features?.melSpectrogram) {
104
+ const sr = this.config.sampleRate || this.audioContext.sampleRate;
105
+ const segMs = this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS;
106
+ const windowSamples = Math.floor((sr * segMs) / 1000);
107
+ this.melWasmInitPromise = initMelStreamingWasm(sr, 128, 2048, windowSamples, windowSamples)
108
+ .then(() => {
109
+ this.melWasmReady = true;
110
+ this.logger?.log('Mel WASM streaming processor ready');
111
+ // Process any chunks that arrived during init
112
+ for (const pending of this.melPendingChunks) {
113
+ this.computeMelFrames(pending.chunk, pending.sampleRate);
114
+ }
115
+ this.melPendingChunks = [];
116
+ })
117
+ .catch((err) => {
118
+ console.error(`[${TAG}] Failed to init mel WASM:`, err);
119
+ this.melWasmInitPromise = null;
120
+ this.melPendingChunks = [];
121
+ });
122
+ }
123
+ }
124
+ // Initialize compressed recording if enabled
125
+ if (recordingConfig.output?.compressed?.enabled) {
126
+ this.initializeCompressedRecorder();
127
+ }
128
+ this.mediaStream = source.mediaStream;
129
+ this.onInterruptionCallback = onInterruption;
130
+ // Setup device disconnection detection
131
+ this.setupDeviceDisconnectionDetection();
132
+ }
133
+ /**
134
+ * Initializes the audio worklet using an inline script
135
+ * Creates and connects the audio processing pipeline
136
+ */
137
+ async init() {
138
+ try {
139
+ // Create and use inline audio worklet
140
+ const blob = new Blob([InlineAudioWebWorker], {
141
+ type: 'application/javascript',
142
+ });
143
+ const url = URL.createObjectURL(blob);
144
+ await this.audioContext.audioWorklet.addModule(url);
145
+ this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'recorder-processor');
146
+ this.audioWorkletNode.port.onmessage = async (event) => {
147
+ const command = event.data.command;
148
+ if (command === 'debug') {
149
+ this.logger?.debug(`[AudioWorklet] ${event.data.message}`);
150
+ return;
151
+ }
152
+ if (command !== 'newData')
153
+ return;
154
+ const pcmBufferFloat = event.data.recordedData;
155
+ if (!pcmBufferFloat) {
156
+ this.logger?.warn('Received empty audio buffer', event);
157
+ return;
158
+ }
159
+ // Process data in smaller chunks and emit immediately
160
+ const sampleRate = event.data.sampleRate ?? this.audioContext.sampleRate;
161
+ // Use chunk size from config interval or default to 2 seconds
162
+ const intervalMs = this.config.interval ?? DEFAULT_WEB_INTERVAL;
163
+ const chunkSize = Math.floor(sampleRate * (intervalMs / 1000));
164
+ const duration = pcmBufferFloat.length / sampleRate;
165
+ // Use incoming position if provided by worklet, otherwise use our tracked position
166
+ const incomingPosition = typeof event.data.position === 'number'
167
+ ? event.data.position
168
+ : this.position;
169
+ // Simple position tracking for logging (no duplicate filtering)
170
+ this.logger?.debug(`Audio chunk: position=${incomingPosition.toFixed(3)}s, size=${pcmBufferFloat.length}`);
171
+ // Calculate bytes per sample based on bit depth
172
+ const bytesPerSample = this.bitDepth / 8;
173
+ // Emit chunks without storing them
174
+ for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
175
+ const chunk = pcmBufferFloat.slice(i, i + chunkSize);
176
+ const chunkPosition = incomingPosition + i / sampleRate;
177
+ // Calculate byte positions and samples
178
+ const startPosition = Math.floor(i * bytesPerSample);
179
+ const endPosition = Math.floor((i + chunk.length) * bytesPerSample);
180
+ const samples = chunk.length; // Number of samples in this chunk
181
+ // Only store PCM data if primary output is enabled
182
+ const shouldStoreUncompressed = this.config.output?.primary?.enabled ?? true;
183
+ // Store PCM chunks when needed - this is for the final WAV file
184
+ if (shouldStoreUncompressed) {
185
+ // Store the original Float32Array data for later WAV creation
186
+ this.appendPcmData(chunk);
187
+ this.totalSampleCount += chunk.length;
188
+ }
189
+ this.computeMelFrames(chunk, sampleRate);
190
+ // Process features if enabled
191
+ if (this.config.enableProcessing &&
192
+ this.featureExtractorWorker) {
193
+ this.featureExtractorWorker.postMessage({
194
+ command: 'process',
195
+ channelData: chunk,
196
+ sampleRate,
197
+ segmentDurationMs: this.config.segmentDurationMs ??
198
+ DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
199
+ bitDepth: this.bitDepth,
200
+ fullAudioDurationMs: chunkPosition * 1000,
201
+ numberOfChannels: this.numberOfChannels,
202
+ features: this.config.features,
203
+ intervalAnalysis: this.config.intervalAnalysis,
204
+ startPosition,
205
+ endPosition,
206
+ samples,
207
+ });
208
+ }
209
+ // Prepare compression data if available
210
+ const compression = this.pendingCompressedChunk
211
+ ? {
212
+ data: this.pendingCompressedChunk,
213
+ size: this.pendingCompressedChunk.size,
214
+ totalSize: this.compressedSize,
215
+ mimeType: 'audio/webm',
216
+ format: this.config.output?.compressed?.format ??
217
+ 'opus',
218
+ bitrate: this.config.output?.compressed?.bitrate ??
219
+ 128000,
220
+ }
221
+ : undefined;
222
+ // Emit chunk immediately - whether compressed or not
223
+ this.emitAudioEventCallback({
224
+ data: chunk,
225
+ position: chunkPosition,
226
+ compression,
227
+ });
228
+ // Reset pending compressed chunk after we've used it
229
+ this.pendingCompressedChunk = null;
230
+ }
231
+ // Update our position based on the worklet's position if provided
232
+ this.position = incomingPosition + duration;
233
+ };
234
+ // Ensure we use all relevant settings from config
235
+ const recordSampleRate = this.audioContext.sampleRate;
236
+ const exportSampleRate = this.config.sampleRate ?? this.audioContext.sampleRate;
237
+ const channels = this.config.channels ?? this.numberOfChannels;
238
+ const interval = this.config.interval ?? DEFAULT_WEB_INTERVAL;
239
+ this.logger?.debug(`WebRecorder initialized with config:`, {
240
+ recordSampleRate,
241
+ exportSampleRate,
242
+ bitDepth: this.bitDepth,
243
+ exportBitDepth: this.exportBitDepth,
244
+ channels,
245
+ interval,
246
+ position: this.position,
247
+ deviceId: this.config.deviceId ?? 'default',
248
+ compression: this.config.output?.compressed
249
+ ? {
250
+ enabled: this.config.output.compressed.enabled,
251
+ format: this.config.output.compressed.format,
252
+ bitrate: this.config.output.compressed.bitrate,
253
+ }
254
+ : 'disabled',
255
+ });
256
+ // Initialize the worklet with all settings from config
257
+ this.audioWorkletNode.port.postMessage({
258
+ command: 'init',
259
+ recordSampleRate,
260
+ exportSampleRate,
261
+ bitDepth: this.bitDepth,
262
+ exportBitDepth: this.exportBitDepth,
263
+ channels,
264
+ interval,
265
+ position: this.position, // Pass the current position to the processor
266
+ enableLogging: true,
267
+ streamFormat: this.config.streamFormat,
268
+ });
269
+ // Connect the source to the AudioWorkletNode and start recording
270
+ this.source.connect(this.audioWorkletNode);
271
+ this.audioWorkletNode.connect(this.audioContext.destination);
272
+ }
273
+ catch (error) {
274
+ console.error(`[${TAG}] Failed to initialize WebRecorder`, error);
275
+ }
276
+ }
277
+ /**
278
+ * Append new PCM data to the existing buffer
279
+ * @param newData New Float32Array data to append
280
+ */
281
+ appendPcmData(newData) {
282
+ // Clone the incoming data to ensure it's not modified
283
+ const dataToAdd = new Float32Array(newData);
284
+ if (!this.pcmData) {
285
+ // First chunk - create a copy to avoid references to original data
286
+ this.pcmData = new Float32Array(dataToAdd);
287
+ return;
288
+ }
289
+ // Create a new buffer with increased size
290
+ const newBuffer = new Float32Array(this.pcmData.length + dataToAdd.length);
291
+ // Copy existing data
292
+ newBuffer.set(this.pcmData);
293
+ // Append new data
294
+ newBuffer.set(dataToAdd, this.pcmData.length);
295
+ // Replace existing buffer
296
+ this.pcmData = newBuffer;
297
+ }
298
+ /**
299
+ * Initializes the feature extractor worker for audio analysis
300
+ * Creates an inline worker from a blob for audio feature extraction
301
+ */
302
+ initFeatureExtractorWorker() {
303
+ try {
304
+ const blob = new Blob([wasmGlueJs, '\n', InlineFeaturesExtractor], {
305
+ type: 'application/javascript',
306
+ });
307
+ const url = URL.createObjectURL(blob);
308
+ this.featureExtractorWorkerUrl = url;
309
+ this.featureExtractorWorker = new Worker(url);
310
+ this.featureExtractorWorker.onmessage =
311
+ this.handleFeatureExtractorMessage.bind(this);
312
+ this.featureExtractorWorker.onerror = (error) => {
313
+ console.error(`[${TAG}] Feature extractor worker error:`, error);
314
+ };
315
+ // Initialize worker with counter if needed
316
+ if (this.dataPointIdCounter > 0) {
317
+ this.featureExtractorWorker.postMessage({
318
+ command: 'resetCounter',
319
+ value: this.dataPointIdCounter,
320
+ });
321
+ this.logger?.debug(`Initialized worker with counter value ${this.dataPointIdCounter}`);
322
+ }
323
+ this.logger?.log('Feature extractor worker initialized successfully');
324
+ }
325
+ catch (error) {
326
+ console.error(`[${TAG}] Failed to initialize feature extractor worker`, error);
327
+ }
328
+ }
329
+ /**
330
+ * Processes audio analysis results from the feature extractor worker
331
+ * Updates the audio analysis data and emits events
332
+ * @param event - The event containing audio analysis results
333
+ */
334
+ handleFeatureExtractorMessage(event) {
335
+ if (event.data.command !== 'features')
336
+ return;
337
+ const segmentResult = event.data.result;
338
+ // Attach WASM-computed mel frames to datapoints
339
+ if (this.pendingMelFrames.length > 0) {
340
+ for (const dp of segmentResult.dataPoints) {
341
+ if (this.pendingMelFrames.length === 0)
342
+ break;
343
+ const melFrame = this.pendingMelFrames.shift();
344
+ if (melFrame) {
345
+ if (!dp.features)
346
+ dp.features = {};
347
+ dp.features.melSpectrogram = melFrame;
348
+ }
349
+ }
350
+ }
351
+ const uniqueNewDataPoints = this.filterUniqueDataPoints(segmentResult.dataPoints);
352
+ // Update counter based on the highest ID seen
353
+ this.updateDataPointCounter(uniqueNewDataPoints);
354
+ // Update analysis data with the new results
355
+ this.updateAudioAnalysisData(segmentResult, uniqueNewDataPoints);
356
+ // Send filtered result to avoid duplicate IDs
357
+ const filteredSegmentResult = {
358
+ ...segmentResult,
359
+ dataPoints: uniqueNewDataPoints,
360
+ };
361
+ this.emitAudioAnalysisCallback(filteredSegmentResult);
362
+ }
363
+ /**
364
+ * Compute mel spectrogram frames via WASM C++ for each segment in the chunk.
365
+ * Frames are queued in pendingMelFrames and attached to datapoints in handleFeatureExtractorMessage.
366
+ */
367
+ computeMelFrames(chunk, sampleRate) {
368
+ if (!this.config.features?.melSpectrogram)
369
+ return;
370
+ if (!this.melWasmReady) {
371
+ // Buffer chunks while WASM is still initializing
372
+ if (this.melWasmInitPromise) {
373
+ this.melPendingChunks.push({
374
+ chunk: new Float32Array(chunk),
375
+ sampleRate,
376
+ });
377
+ }
378
+ return;
379
+ }
380
+ const segMs = this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS;
381
+ const samplesPerSeg = Math.floor((sampleRate * segMs) / 1000);
382
+ const numSegs = Math.floor(chunk.length / samplesPerSeg);
383
+ for (let s = 0; s < numSegs; s++) {
384
+ const seg = chunk.slice(s * samplesPerSeg, (s + 1) * samplesPerSeg);
385
+ this.pendingMelFrames.push(computeMelFrameWasm(seg));
386
+ }
387
+ const rem = chunk.length % samplesPerSeg;
388
+ if (rem > samplesPerSeg / 4) {
389
+ const seg = chunk.slice(numSegs * samplesPerSeg);
390
+ this.pendingMelFrames.push(computeMelFrameWasm(seg));
391
+ }
392
+ // Cap queue to prevent unbounded memory growth if consumer falls behind
393
+ const MAX_PENDING_MEL_FRAMES = 200;
394
+ if (this.pendingMelFrames.length > MAX_PENDING_MEL_FRAMES) {
395
+ this.pendingMelFrames = this.pendingMelFrames.slice(-MAX_PENDING_MEL_FRAMES);
396
+ }
397
+ }
398
+ /**
399
+ * Filters out data points with duplicate IDs
400
+ */
401
+ filterUniqueDataPoints(dataPoints) {
402
+ // Track existing IDs to prevent duplicates
403
+ const existingIds = new Set(this.audioAnalysisData.dataPoints.map((dp) => dp.id));
404
+ // Filter out datapoints with duplicate IDs
405
+ const uniquePoints = dataPoints.filter((dp) => !existingIds.has(dp.id));
406
+ // Log filtered duplicates if any
407
+ if (uniquePoints.length < dataPoints.length && this.logger?.warn) {
408
+ this.logger.warn(`Filtered ${dataPoints.length - uniquePoints.length} duplicate datapoints`);
409
+ }
410
+ return uniquePoints;
411
+ }
412
+ /**
413
+ * Updates the counter based on the highest ID in datapoints
414
+ */
415
+ updateDataPointCounter(dataPoints) {
416
+ if (dataPoints.length === 0)
417
+ return;
418
+ const lastDataPoint = dataPoints[dataPoints.length - 1];
419
+ if (lastDataPoint && typeof lastDataPoint.id === 'number') {
420
+ const nextIdValue = lastDataPoint.id + 1;
421
+ if (nextIdValue > this.dataPointIdCounter) {
422
+ this.dataPointIdCounter = nextIdValue;
423
+ this.logger?.debug(`Counter updated to ${this.dataPointIdCounter}`);
424
+ }
425
+ }
426
+ }
427
+ /**
428
+ * Updates audio analysis data with segment results
429
+ */
430
+ updateAudioAnalysisData(segmentResult, uniqueDataPoints) {
431
+ // Add unique data points to our analysis data
432
+ this.audioAnalysisData.dataPoints.push(...uniqueDataPoints);
433
+ this.audioAnalysisData.durationMs += segmentResult.durationMs;
434
+ this.audioAnalysisData.sampleRate = segmentResult.sampleRate;
435
+ // Update amplitude range if present
436
+ if (segmentResult.amplitudeRange) {
437
+ this.audioAnalysisData.amplitudeRange = this.mergeRange(this.audioAnalysisData.amplitudeRange, segmentResult.amplitudeRange);
438
+ }
439
+ // Update RMS range if present
440
+ if (segmentResult.rmsRange) {
441
+ this.audioAnalysisData.rmsRange = this.mergeRange(this.audioAnalysisData.rmsRange, segmentResult.rmsRange);
442
+ }
443
+ }
444
+ /**
445
+ * Merges value ranges
446
+ */
447
+ mergeRange(existing, newRange) {
448
+ if (!existing)
449
+ return { ...newRange };
450
+ return {
451
+ min: Math.min(existing.min, newRange.min),
452
+ max: Math.max(existing.max, newRange.max),
453
+ };
454
+ }
455
+ /**
456
+ * Reset the data point counter to a specific value or zero
457
+ * @param startCounterFrom Optional value to start the counter from (for continuing from previous recordings)
458
+ */
459
+ resetDataPointCounter(startCounterFrom) {
460
+ // Set the counter with the passed value or 0
461
+ this.dataPointIdCounter = startCounterFrom ?? 0;
462
+ this.logger?.debug(`Reset data point counter to ${this.dataPointIdCounter}`);
463
+ // Update worker counter if available
464
+ if (this.featureExtractorWorker) {
465
+ this.featureExtractorWorker.postMessage({
466
+ command: 'resetCounter',
467
+ value: this.dataPointIdCounter,
468
+ });
469
+ }
470
+ else {
471
+ this.logger?.warn('No feature extractor worker available to update counter');
472
+ }
473
+ }
474
+ /**
475
+ * Get the current data point counter value
476
+ * @returns The current value of the data point counter
477
+ */
478
+ getDataPointCounter() {
479
+ return this.dataPointIdCounter;
480
+ }
481
+ /**
482
+ * Prepares the recorder for continuity after device switch
483
+ * Sets up all necessary state to maintain proper recording continuity
484
+ */
485
+ prepareForDeviceSwitch() {
486
+ this.isFirstChunkAfterSwitch = true;
487
+ this.logger?.debug(`Prepared for device switch at position ${this.position}s`);
488
+ }
489
+ /**
490
+ * Starts the audio recording process
491
+ * Connects the audio nodes and begins capturing audio data
492
+ * @param preserveCounters If true, do not reset the counter (used for device switching)
493
+ */
494
+ start(preserveCounters = false) {
495
+ this.source.connect(this.audioWorkletNode);
496
+ this.audioWorkletNode.connect(this.audioContext.destination);
497
+ // Only reset the counter when not preserving state (e.g., for a fresh recording)
498
+ if (!preserveCounters) {
499
+ this.logger?.debug('Starting fresh recording, resetting counter to 0');
500
+ this.resetDataPointCounter(0); // Explicitly reset to 0 for new recordings
501
+ this.isFirstChunkAfterSwitch = false;
502
+ // Clear PCM data for new recording
503
+ this.pcmData = null;
504
+ this.totalSampleCount = 0;
505
+ }
506
+ else {
507
+ this.logger?.debug(`Preserving counter at ${this.dataPointIdCounter} during device switch`);
508
+ }
509
+ if (this.compressedMediaRecorder) {
510
+ this.compressedMediaRecorder.start(this.config.interval ?? 1000);
511
+ }
512
+ }
513
+ /**
514
+ * Creates a WAV file from the stored PCM data
515
+ */
516
+ createWavFromPcmData() {
517
+ try {
518
+ // Check if we have PCM data
519
+ if (!this.pcmData || this.pcmData.length === 0) {
520
+ this.logger?.warn('No PCM data available to create WAV file');
521
+ return null;
522
+ }
523
+ const sampleRate = this.config.sampleRate ?? this.audioContext.sampleRate;
524
+ const channels = this.numberOfChannels || 1;
525
+ // Convert float32 PCM data to 16-bit PCM for WAV
526
+ const bytesPerSample = 2; // 16-bit = 2 bytes
527
+ const dataLength = this.pcmData.length * bytesPerSample;
528
+ const buffer = new ArrayBuffer(dataLength);
529
+ const view = new DataView(buffer);
530
+ // Convert Float32Array (-1 to 1) to Int16Array (-32768 to 32767)
531
+ for (let i = 0; i < this.pcmData.length; i++) {
532
+ const sample = Math.max(-1, Math.min(1, this.pcmData[i]));
533
+ const int16Value = Math.round(sample * 32767);
534
+ view.setInt16(i * 2, int16Value, true);
535
+ }
536
+ // Use the existing writeWavHeader utility to add a WAV header
537
+ const wavBuffer = writeWavHeader({
538
+ buffer,
539
+ sampleRate,
540
+ numChannels: channels,
541
+ bitDepth: 16,
542
+ isFloat: false,
543
+ });
544
+ return new Blob([wavBuffer], { type: 'audio/wav' });
545
+ }
546
+ catch (error) {
547
+ this.logger?.error('Error creating WAV file from PCM data:', error);
548
+ return null;
549
+ }
550
+ }
551
+ /**
552
+ * Stops the audio recording process and returns the recorded data
553
+ * @returns Promise resolving to an object containing compressed and/or uncompressed blobs
554
+ */
555
+ async stop() {
556
+ try {
557
+ // Stop any compressed recording first
558
+ if (this.compressedMediaRecorder &&
559
+ this.compressedMediaRecorder.state !== 'inactive') {
560
+ this.compressedMediaRecorder.stop();
561
+ }
562
+ // Wait for any pending compressed chunks to be processed
563
+ if (this.compressedMediaRecorder) {
564
+ // Small delay to ensure all data is processed
565
+ await new Promise((resolve) => setTimeout(resolve, 100));
566
+ }
567
+ // Create uncompressed WAV file from the PCM data
568
+ let uncompressedBlob;
569
+ // Only create WAV if we have PCM data
570
+ if (this.pcmData && this.pcmData.length > 0) {
571
+ uncompressedBlob = this.createWavFromPcmData() || undefined;
572
+ }
573
+ // Return the compressed and/or uncompressed blobs if available
574
+ return {
575
+ compressedBlob: this.compressedChunks.length > 0
576
+ ? new Blob(this.compressedChunks, {
577
+ type: 'audio/webm;codecs=opus',
578
+ })
579
+ : undefined,
580
+ uncompressedBlob,
581
+ };
582
+ }
583
+ finally {
584
+ this.cleanup();
585
+ // Reset the chunks array
586
+ this.compressedChunks = [];
587
+ this.compressedSize = 0;
588
+ this.pendingCompressedChunk = null;
589
+ this.pcmData = null;
590
+ this.totalSampleCount = 0;
591
+ this.dataPointIdCounter = 0; // Reset counter
592
+ }
593
+ }
594
+ /**
595
+ * Cleans up resources when recording is stopped
596
+ * Closes audio context and disconnects nodes
597
+ */
598
+ cleanup() {
599
+ // Remove device disconnection handler
600
+ if (this.deviceDisconnectionHandler) {
601
+ this.deviceDisconnectionHandler();
602
+ this.deviceDisconnectionHandler = null;
603
+ }
604
+ // Check if AudioContext is already closed before attempting to close it
605
+ if (this.audioContext && this.audioContext.state !== 'closed') {
606
+ this.audioContext.close().catch((e) => {
607
+ // Log closure errors but continue cleanup
608
+ this.logger?.warn('Error closing AudioContext:', e);
609
+ });
610
+ }
611
+ // Safely disconnect audioWorkletNode if it exists
612
+ if (this.audioWorkletNode) {
613
+ try {
614
+ this.audioWorkletNode.disconnect();
615
+ }
616
+ catch (e) {
617
+ // Log disconnection errors but continue cleanup
618
+ this.logger?.warn('Error disconnecting audioWorkletNode:', e);
619
+ }
620
+ }
621
+ // Safely disconnect source if it exists
622
+ if (this.source) {
623
+ try {
624
+ this.source.disconnect();
625
+ }
626
+ catch (e) {
627
+ // Log disconnection errors but continue cleanup
628
+ this.logger?.warn('Error disconnecting source:', e);
629
+ }
630
+ }
631
+ // Terminate feature extractor worker and revoke blob URL
632
+ if (this.featureExtractorWorker) {
633
+ this.featureExtractorWorker.terminate();
634
+ this.featureExtractorWorker = undefined;
635
+ }
636
+ if (this.featureExtractorWorkerUrl) {
637
+ URL.revokeObjectURL(this.featureExtractorWorkerUrl);
638
+ this.featureExtractorWorkerUrl = undefined;
639
+ }
640
+ // Always stop media stream tracks to release hardware resources
641
+ this.stopMediaStreamTracks();
642
+ // Mark as disconnected to prevent future errors
643
+ this._isDeviceDisconnected = true;
644
+ }
645
+ /**
646
+ * Pauses the audio recording process
647
+ * Disconnects audio nodes and pauses the media recorder
648
+ */
649
+ pause() {
650
+ try {
651
+ // Note: We're just pausing, not disconnecting the device
652
+ // Simply disconnect nodes temporarily without marking device as disconnected
653
+ this.source.disconnect(this.audioWorkletNode);
654
+ this.audioWorkletNode.disconnect(this.audioContext.destination);
655
+ this.audioWorkletNode.port.postMessage({ command: 'pause' });
656
+ if (this.compressedMediaRecorder?.state === 'recording') {
657
+ this.compressedMediaRecorder.pause();
658
+ }
659
+ this.logger?.debug('Recording paused successfully');
660
+ }
661
+ catch (error) {
662
+ this.logger?.error('Error in pause(): ', error);
663
+ // Already disconnected, just ignore and continue
664
+ }
665
+ }
666
+ /**
667
+ * Stops all media stream tracks to release hardware resources
668
+ * Ensures recording indicators (like microphone icon) are turned off
669
+ */
670
+ stopMediaStreamTracks() {
671
+ // Stop all audio tracks to stop the recording icon
672
+ if (this.mediaStream) {
673
+ const tracks = this.mediaStream.getTracks();
674
+ tracks.forEach((track) => track.stop());
675
+ }
676
+ else if (this.source?.mediaStream) {
677
+ const tracks = this.source.mediaStream.getTracks();
678
+ tracks.forEach((track) => track.stop());
679
+ }
680
+ }
681
+ /**
682
+ * Determines the audio format capabilities of the current audio context
683
+ * @param sampleRate - The sample rate to check
684
+ * @returns Object containing format information (sample rate, bit depth, channels)
685
+ */
686
+ checkAudioContextFormat({ sampleRate }) {
687
+ // Create a silent AudioBuffer
688
+ const frameCount = sampleRate * 1.0; // 1 second buffer
689
+ const audioBuffer = this.audioContext.createBuffer(1, frameCount, sampleRate);
690
+ // Check the format
691
+ const channelData = audioBuffer.getChannelData(0);
692
+ const bitDepth = channelData.BYTES_PER_ELEMENT * 8; // 4 bytes per element means 32-bit
693
+ return {
694
+ sampleRate: audioBuffer.sampleRate,
695
+ bitDepth,
696
+ numberOfChannels: audioBuffer.numberOfChannels,
697
+ };
698
+ }
699
+ /**
700
+ * Resumes a paused recording
701
+ * Reconnects audio nodes and resumes the media recorder
702
+ */
703
+ resume() {
704
+ // If device was disconnected, we can't resume
705
+ if (this._isDeviceDisconnected) {
706
+ this.logger?.warn('Cannot resume recording: device disconnected');
707
+ return;
708
+ }
709
+ try {
710
+ this.source.connect(this.audioWorkletNode);
711
+ this.audioWorkletNode.connect(this.audioContext.destination);
712
+ this.audioWorkletNode.port.postMessage({ command: 'resume' });
713
+ this.compressedMediaRecorder?.resume();
714
+ }
715
+ catch (error) {
716
+ this.logger?.error('Error in resume(): ', error);
717
+ // Rethrow the error to inform callers
718
+ throw new Error(`Failed to resume recording: ${error instanceof Error ? error.message : 'unknown error'}`);
719
+ }
720
+ }
721
+ /**
722
+ * Initializes the compressed media recorder if compression is enabled
723
+ * Sets up event handlers for compressed audio data
724
+ */
725
+ initializeCompressedRecorder() {
726
+ try {
727
+ const mimeType = 'audio/webm;codecs=opus';
728
+ if (!MediaRecorder.isTypeSupported(mimeType)) {
729
+ this.logger?.warn('Opus compression not supported in this browser');
730
+ return;
731
+ }
732
+ this.compressedMediaRecorder = new MediaRecorder(this.source.mediaStream, {
733
+ mimeType,
734
+ audioBitsPerSecond: this.config.output?.compressed?.bitrate ?? 128000,
735
+ });
736
+ this.compressedMediaRecorder.ondataavailable = (event) => {
737
+ if (event.data.size > 0) {
738
+ // Store the compressed chunk for final blob creation
739
+ this.compressedChunks.push(event.data);
740
+ this.compressedSize += event.data.size;
741
+ // Store the pending compressed chunk for the next PCM chunk to use
742
+ this.pendingCompressedChunk = event.data;
743
+ }
744
+ };
745
+ }
746
+ catch (error) {
747
+ this.logger?.error('Failed to initialize compressed recorder:', error);
748
+ // Setting to null to indicate initialization failed
749
+ this.compressedMediaRecorder = null;
750
+ }
751
+ }
752
+ /**
753
+ * Processes features if enabled
754
+ */
755
+ processFeatures(chunk, sampleRate, chunkPosition, startPosition, endPosition, samples) {
756
+ this.computeMelFrames(chunk, sampleRate);
757
+ if (this.config.enableProcessing && this.featureExtractorWorker) {
758
+ this.featureExtractorWorker.postMessage({
759
+ command: 'process',
760
+ channelData: chunk,
761
+ sampleRate,
762
+ segmentDurationMs: this.config.segmentDurationMs ??
763
+ DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
764
+ bitDepth: this.bitDepth,
765
+ fullAudioDurationMs: chunkPosition * 1000,
766
+ numberOfChannels: this.numberOfChannels,
767
+ features: this.config.features,
768
+ intervalAnalysis: this.config.intervalAnalysis,
769
+ startPosition,
770
+ endPosition,
771
+ samples,
772
+ });
773
+ }
774
+ }
775
+ /**
776
+ * Sets up detection for device disconnection events
777
+ */
778
+ setupDeviceDisconnectionDetection() {
779
+ if (!this.mediaStream)
780
+ return;
781
+ // Function to handle track ending (which happens on device disconnection)
782
+ const handleTrackEnded = () => {
783
+ this.logger?.warn('Audio track ended - device disconnected');
784
+ this._isDeviceDisconnected = true;
785
+ // Use the callback to notify parent component about device disconnection
786
+ if (this.onInterruptionCallback) {
787
+ this.onInterruptionCallback({
788
+ reason: 'deviceDisconnected',
789
+ isPaused: true,
790
+ timestamp: Date.now(),
791
+ });
792
+ this.logger?.debug('Notified about device disconnection');
793
+ }
794
+ // Ensure we disconnect nodes to prevent zombie recordings
795
+ if (this.audioWorkletNode) {
796
+ this.audioWorkletNode.port.postMessage({
797
+ command: 'deviceDisconnected',
798
+ });
799
+ try {
800
+ this.source.disconnect(this.audioWorkletNode);
801
+ this.audioWorkletNode.disconnect();
802
+ }
803
+ catch (e) {
804
+ // Ignore disconnection errors as the track might already be gone
805
+ this.logger?.warn('Error disconnecting audioWorkletNode:', e);
806
+ }
807
+ }
808
+ };
809
+ // Add listeners to all audio tracks
810
+ const tracks = this.mediaStream.getAudioTracks();
811
+ tracks.forEach((track) => {
812
+ track.addEventListener('ended', handleTrackEnded);
813
+ });
814
+ // Store the handler for cleanup
815
+ this.deviceDisconnectionHandler = () => {
816
+ tracks.forEach((track) => {
817
+ track.removeEventListener('ended', handleTrackEnded);
818
+ });
819
+ };
820
+ }
821
+ /**
822
+ * Explicitly set the position for continuous recording across device switches
823
+ * @param position The position in seconds to continue from
824
+ */
825
+ setPosition(position) {
826
+ if (position >= 0) {
827
+ this.position = position;
828
+ this.logger?.debug(`Position explicitly set to ${position} seconds`);
829
+ }
830
+ else {
831
+ this.logger?.warn(`Invalid position value: ${position}, ignoring`);
832
+ }
833
+ }
834
+ /**
835
+ * Get the current position in seconds
836
+ * @returns The current position
837
+ */
838
+ getPosition() {
839
+ return this.position;
840
+ }
841
+ /**
842
+ * Gets the current compressed chunks
843
+ * @returns Array of current compressed audio chunks
844
+ */
845
+ getCompressedChunks() {
846
+ return [...this.compressedChunks];
847
+ }
848
+ /**
849
+ * Sets the compressed chunks from a previous recorder
850
+ * @param chunks Array of compressed chunks from a previous recorder
851
+ */
852
+ setCompressedChunks(chunks) {
853
+ if (chunks && chunks.length > 0) {
854
+ this.logger?.debug(`Adding ${chunks.length} compressed chunks from previous device`);
855
+ this.compressedChunks = [...chunks, ...this.compressedChunks];
856
+ // Update size
857
+ this.compressedSize = this.compressedChunks.reduce((size, chunk) => size + chunk.size, 0);
858
+ }
859
+ }
860
+ }
861
+ //# sourceMappingURL=WebRecorder.web.js.map