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