@siteed/expo-audio-studio 2.18.5 → 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 (329) hide show
  1. package/README.md +13 -297
  2. package/index.d.ts +1 -0
  3. package/index.js +1 -0
  4. package/package.json +6 -135
  5. package/CHANGELOG.md +0 -493
  6. package/LICENSE +0 -21
  7. package/android/build.gradle +0 -129
  8. package/android/src/androidTest/assets/chorus.wav +0 -0
  9. package/android/src/androidTest/assets/jfk.wav +0 -0
  10. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  11. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  12. package/android/src/androidTest/java/net/siteed/audiostream/AudioProcessorInstrumentedTest.kt +0 -197
  13. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +0 -541
  14. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderPerformanceInstrumentedTest.kt +0 -234
  15. package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  16. package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +0 -324
  17. package/android/src/androidTest/java/net/siteed/audiostream/integration/CompressedOnlyOutputTest.kt +0 -253
  18. package/android/src/androidTest/java/net/siteed/audiostream/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  19. package/android/src/androidTest/java/net/siteed/audiostream/integration/EventEmissionIntervalTest.kt +0 -120
  20. package/android/src/androidTest/java/net/siteed/audiostream/integration/M4aFormatTest.kt +0 -345
  21. package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +0 -340
  22. package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt +0 -252
  23. package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +0 -95
  24. package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +0 -43
  25. package/android/src/main/AndroidManifest.xml +0 -30
  26. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -188
  27. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  28. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +0 -1741
  29. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -136
  30. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -354
  31. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -439
  32. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2237
  33. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -2141
  34. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -167
  35. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  36. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -37
  37. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  38. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -1113
  39. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  40. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  41. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +0 -93
  42. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -72
  43. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -68
  44. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  45. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -257
  46. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  47. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  48. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  49. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  50. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  51. package/android/src/main/res/drawable/ic_play.xml +0 -10
  52. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  53. package/android/src/main/res/layout/notification_recording.xml +0 -37
  54. package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +0 -279
  55. package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +0 -249
  56. package/android/src/test/java/net/siteed/audiostream/AudioFormatTest.kt +0 -151
  57. package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +0 -273
  58. package/android/src/test/java/net/siteed/audiostream/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  59. package/android/src/test/resources/chorus.wav +0 -0
  60. package/android/src/test/resources/generate_test_audio.py +0 -94
  61. package/android/src/test/resources/jfk.wav +0 -0
  62. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  63. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  64. package/app.plugin.js +0 -3
  65. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js +0 -4
  66. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  67. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +0 -210
  68. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  69. package/build/cjs/AudioAnalysis/extractAudioData.js +0 -21
  70. package/build/cjs/AudioAnalysis/extractAudioData.js.map +0 -1
  71. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +0 -92
  72. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  73. package/build/cjs/AudioAnalysis/extractPreview.js +0 -28
  74. package/build/cjs/AudioAnalysis/extractPreview.js.map +0 -1
  75. package/build/cjs/AudioAnalysis/extractWaveform.js +0 -18
  76. package/build/cjs/AudioAnalysis/extractWaveform.js.map +0 -1
  77. package/build/cjs/AudioDeviceManager.js +0 -689
  78. package/build/cjs/AudioDeviceManager.js.map +0 -1
  79. package/build/cjs/AudioRecorder.provider.js +0 -78
  80. package/build/cjs/AudioRecorder.provider.js.map +0 -1
  81. package/build/cjs/ExpoAudioStream.native.js +0 -8
  82. package/build/cjs/ExpoAudioStream.native.js.map +0 -1
  83. package/build/cjs/ExpoAudioStream.types.js +0 -11
  84. package/build/cjs/ExpoAudioStream.types.js.map +0 -1
  85. package/build/cjs/ExpoAudioStream.web.js +0 -708
  86. package/build/cjs/ExpoAudioStream.web.js.map +0 -1
  87. package/build/cjs/ExpoAudioStreamModule.js +0 -718
  88. package/build/cjs/ExpoAudioStreamModule.js.map +0 -1
  89. package/build/cjs/WebRecorder.web.js +0 -777
  90. package/build/cjs/WebRecorder.web.js.map +0 -1
  91. package/build/cjs/constants/platformLimitations.js +0 -99
  92. package/build/cjs/constants/platformLimitations.js.map +0 -1
  93. package/build/cjs/constants.js +0 -17
  94. package/build/cjs/constants.js.map +0 -1
  95. package/build/cjs/events.js +0 -29
  96. package/build/cjs/events.js.map +0 -1
  97. package/build/cjs/hooks/useAudioDevices.js +0 -179
  98. package/build/cjs/hooks/useAudioDevices.js.map +0 -1
  99. package/build/cjs/index.js +0 -58
  100. package/build/cjs/index.js.map +0 -1
  101. package/build/cjs/trimAudio.js +0 -76
  102. package/build/cjs/trimAudio.js.map +0 -1
  103. package/build/cjs/useAudioRecorder.js +0 -518
  104. package/build/cjs/useAudioRecorder.js.map +0 -1
  105. package/build/cjs/utils/BlobFix.js +0 -502
  106. package/build/cjs/utils/BlobFix.js.map +0 -1
  107. package/build/cjs/utils/audioProcessing.js +0 -136
  108. package/build/cjs/utils/audioProcessing.js.map +0 -1
  109. package/build/cjs/utils/cleanNativeOptions.js +0 -22
  110. package/build/cjs/utils/cleanNativeOptions.js.map +0 -1
  111. package/build/cjs/utils/concatenateBuffers.js +0 -25
  112. package/build/cjs/utils/concatenateBuffers.js.map +0 -1
  113. package/build/cjs/utils/convertPCMToFloat32.js +0 -124
  114. package/build/cjs/utils/convertPCMToFloat32.js.map +0 -1
  115. package/build/cjs/utils/crc32.js +0 -52
  116. package/build/cjs/utils/crc32.js.map +0 -1
  117. package/build/cjs/utils/encodingToBitDepth.js +0 -17
  118. package/build/cjs/utils/encodingToBitDepth.js.map +0 -1
  119. package/build/cjs/utils/getWavFileInfo.js +0 -96
  120. package/build/cjs/utils/getWavFileInfo.js.map +0 -1
  121. package/build/cjs/utils/writeWavHeader.js +0 -88
  122. package/build/cjs/utils/writeWavHeader.js.map +0 -1
  123. package/build/cjs/workers/InlineFeaturesExtractor.web.js +0 -859
  124. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +0 -1
  125. package/build/cjs/workers/inlineAudioWebWorker.web.js +0 -184
  126. package/build/cjs/workers/inlineAudioWebWorker.web.js.map +0 -1
  127. package/build/esm/AudioAnalysis/AudioAnalysis.types.js +0 -3
  128. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  129. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +0 -202
  130. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  131. package/build/esm/AudioAnalysis/extractAudioData.js +0 -14
  132. package/build/esm/AudioAnalysis/extractAudioData.js.map +0 -1
  133. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +0 -89
  134. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  135. package/build/esm/AudioAnalysis/extractPreview.js +0 -25
  136. package/build/esm/AudioAnalysis/extractPreview.js.map +0 -1
  137. package/build/esm/AudioAnalysis/extractWaveform.js +0 -11
  138. package/build/esm/AudioAnalysis/extractWaveform.js.map +0 -1
  139. package/build/esm/AudioDeviceManager.js +0 -682
  140. package/build/esm/AudioDeviceManager.js.map +0 -1
  141. package/build/esm/AudioRecorder.provider.js +0 -40
  142. package/build/esm/AudioRecorder.provider.js.map +0 -1
  143. package/build/esm/ExpoAudioStream.native.js +0 -6
  144. package/build/esm/ExpoAudioStream.native.js.map +0 -1
  145. package/build/esm/ExpoAudioStream.types.js +0 -8
  146. package/build/esm/ExpoAudioStream.types.js.map +0 -1
  147. package/build/esm/ExpoAudioStream.web.js +0 -704
  148. package/build/esm/ExpoAudioStream.web.js.map +0 -1
  149. package/build/esm/ExpoAudioStreamModule.js +0 -713
  150. package/build/esm/ExpoAudioStreamModule.js.map +0 -1
  151. package/build/esm/WebRecorder.web.js +0 -773
  152. package/build/esm/WebRecorder.web.js.map +0 -1
  153. package/build/esm/constants/platformLimitations.js +0 -90
  154. package/build/esm/constants/platformLimitations.js.map +0 -1
  155. package/build/esm/constants.js +0 -14
  156. package/build/esm/constants.js.map +0 -1
  157. package/build/esm/events.js +0 -21
  158. package/build/esm/events.js.map +0 -1
  159. package/build/esm/hooks/useAudioDevices.js +0 -176
  160. package/build/esm/hooks/useAudioDevices.js.map +0 -1
  161. package/build/esm/index.js +0 -20
  162. package/build/esm/index.js.map +0 -1
  163. package/build/esm/trimAudio.js +0 -69
  164. package/build/esm/trimAudio.js.map +0 -1
  165. package/build/esm/useAudioRecorder.js +0 -512
  166. package/build/esm/useAudioRecorder.js.map +0 -1
  167. package/build/esm/utils/BlobFix.js +0 -498
  168. package/build/esm/utils/BlobFix.js.map +0 -1
  169. package/build/esm/utils/audioProcessing.js +0 -133
  170. package/build/esm/utils/audioProcessing.js.map +0 -1
  171. package/build/esm/utils/cleanNativeOptions.js +0 -19
  172. package/build/esm/utils/cleanNativeOptions.js.map +0 -1
  173. package/build/esm/utils/concatenateBuffers.js +0 -21
  174. package/build/esm/utils/concatenateBuffers.js.map +0 -1
  175. package/build/esm/utils/convertPCMToFloat32.js +0 -120
  176. package/build/esm/utils/convertPCMToFloat32.js.map +0 -1
  177. package/build/esm/utils/crc32.js +0 -50
  178. package/build/esm/utils/crc32.js.map +0 -1
  179. package/build/esm/utils/encodingToBitDepth.js +0 -13
  180. package/build/esm/utils/encodingToBitDepth.js.map +0 -1
  181. package/build/esm/utils/getWavFileInfo.js +0 -92
  182. package/build/esm/utils/getWavFileInfo.js.map +0 -1
  183. package/build/esm/utils/writeWavHeader.js +0 -84
  184. package/build/esm/utils/writeWavHeader.js.map +0 -1
  185. package/build/esm/workers/InlineFeaturesExtractor.web.js +0 -856
  186. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +0 -1
  187. package/build/esm/workers/inlineAudioWebWorker.web.js +0 -181
  188. package/build/esm/workers/inlineAudioWebWorker.web.js.map +0 -1
  189. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +0 -196
  190. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  191. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts +0 -74
  192. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  193. package/build/types/AudioAnalysis/extractAudioData.d.ts +0 -3
  194. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  195. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  196. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  197. package/build/types/AudioAnalysis/extractPreview.d.ts +0 -11
  198. package/build/types/AudioAnalysis/extractPreview.d.ts.map +0 -1
  199. package/build/types/AudioAnalysis/extractWaveform.d.ts +0 -8
  200. package/build/types/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  201. package/build/types/AudioDeviceManager.d.ts +0 -187
  202. package/build/types/AudioDeviceManager.d.ts.map +0 -1
  203. package/build/types/AudioRecorder.provider.d.ts +0 -11
  204. package/build/types/AudioRecorder.provider.d.ts.map +0 -1
  205. package/build/types/ExpoAudioStream.native.d.ts +0 -3
  206. package/build/types/ExpoAudioStream.native.d.ts.map +0 -1
  207. package/build/types/ExpoAudioStream.types.d.ts +0 -738
  208. package/build/types/ExpoAudioStream.types.d.ts.map +0 -1
  209. package/build/types/ExpoAudioStream.web.d.ts +0 -96
  210. package/build/types/ExpoAudioStream.web.d.ts.map +0 -1
  211. package/build/types/ExpoAudioStreamModule.d.ts +0 -3
  212. package/build/types/ExpoAudioStreamModule.d.ts.map +0 -1
  213. package/build/types/WebRecorder.web.d.ts +0 -198
  214. package/build/types/WebRecorder.web.d.ts.map +0 -1
  215. package/build/types/constants/platformLimitations.d.ts +0 -40
  216. package/build/types/constants/platformLimitations.d.ts.map +0 -1
  217. package/build/types/constants.d.ts +0 -11
  218. package/build/types/constants.d.ts.map +0 -1
  219. package/build/types/events.d.ts +0 -26
  220. package/build/types/events.d.ts.map +0 -1
  221. package/build/types/hooks/useAudioDevices.d.ts +0 -15
  222. package/build/types/hooks/useAudioDevices.d.ts.map +0 -1
  223. package/build/types/index.d.ts +0 -18
  224. package/build/types/index.d.ts.map +0 -1
  225. package/build/types/trimAudio.d.ts +0 -25
  226. package/build/types/trimAudio.d.ts.map +0 -1
  227. package/build/types/useAudioRecorder.d.ts +0 -22
  228. package/build/types/useAudioRecorder.d.ts.map +0 -1
  229. package/build/types/utils/BlobFix.d.ts +0 -9
  230. package/build/types/utils/BlobFix.d.ts.map +0 -1
  231. package/build/types/utils/audioProcessing.d.ts +0 -24
  232. package/build/types/utils/audioProcessing.d.ts.map +0 -1
  233. package/build/types/utils/cleanNativeOptions.d.ts +0 -15
  234. package/build/types/utils/cleanNativeOptions.d.ts.map +0 -1
  235. package/build/types/utils/concatenateBuffers.d.ts +0 -8
  236. package/build/types/utils/concatenateBuffers.d.ts.map +0 -1
  237. package/build/types/utils/convertPCMToFloat32.d.ts +0 -13
  238. package/build/types/utils/convertPCMToFloat32.d.ts.map +0 -1
  239. package/build/types/utils/crc32.d.ts +0 -7
  240. package/build/types/utils/crc32.d.ts.map +0 -1
  241. package/build/types/utils/encodingToBitDepth.d.ts +0 -5
  242. package/build/types/utils/encodingToBitDepth.d.ts.map +0 -1
  243. package/build/types/utils/getWavFileInfo.d.ts +0 -26
  244. package/build/types/utils/getWavFileInfo.d.ts.map +0 -1
  245. package/build/types/utils/writeWavHeader.d.ts +0 -34
  246. package/build/types/utils/writeWavHeader.d.ts.map +0 -1
  247. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  248. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  249. package/build/types/workers/inlineAudioWebWorker.web.d.ts +0 -2
  250. package/build/types/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  251. package/expo-module.config.json +0 -10
  252. package/ios/AudioAnalysisData.swift +0 -74
  253. package/ios/AudioDeviceManager.swift +0 -666
  254. package/ios/AudioNotificationManager.swift +0 -154
  255. package/ios/AudioProcessingHelpers.swift +0 -743
  256. package/ios/AudioProcessor.swift +0 -1151
  257. package/ios/AudioStreamError.swift +0 -7
  258. package/ios/AudioStreamManager.swift +0 -2352
  259. package/ios/AudioStreamManagerDelegate.swift +0 -16
  260. package/ios/DataPoint.swift +0 -54
  261. package/ios/DecodingConfig.swift +0 -59
  262. package/ios/ExpoAudioStream.podspec +0 -33
  263. package/ios/ExpoAudioStreamModule.swift +0 -1013
  264. package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +0 -338
  265. package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  266. package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +0 -130
  267. package/ios/ExpoAudioStudioTests/CompressedOnlyOutputTests.swift +0 -294
  268. package/ios/ExpoAudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  269. package/ios/ExpoAudioStudioTests/Info.plist +0 -22
  270. package/ios/ExpoAudioStudioTests/README.md +0 -39
  271. package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +0 -98
  272. package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +0 -75
  273. package/ios/FFT.swift +0 -62
  274. package/ios/Features.swift +0 -95
  275. package/ios/ISSUE_IOS.md +0 -68
  276. package/ios/Logger.swift +0 -39
  277. package/ios/NotificationExtension.swift +0 -15
  278. package/ios/RecordingResult.swift +0 -22
  279. package/ios/RecordingSettings.swift +0 -308
  280. package/ios/WaveformExtractor.swift +0 -105
  281. package/ios/tests/README.md +0 -41
  282. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  283. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  284. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  285. package/ios/tests/integration/output_control_test.swift +0 -322
  286. package/ios/tests/integration/run_integration_tests.sh +0 -37
  287. package/ios/tests/opus_support_test_macos.swift +0 -154
  288. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  289. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  290. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  291. package/ios/tests/standalone/standalone_test.swift +0 -144
  292. package/plugin/build/index.cjs +0 -194
  293. package/plugin/build/index.d.cts +0 -22
  294. package/plugin/build/index.js +0 -194
  295. package/plugin/src/index.ts +0 -285
  296. package/plugin/tsconfig.json +0 -10
  297. package/plugin/tsconfig.tsbuildinfo +0 -1
  298. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -224
  299. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -344
  300. package/src/AudioAnalysis/extractAudioData.ts +0 -17
  301. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -154
  302. package/src/AudioAnalysis/extractPreview.ts +0 -34
  303. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  304. package/src/AudioDeviceManager.ts +0 -803
  305. package/src/AudioRecorder.provider.tsx +0 -57
  306. package/src/ExpoAudioStream.native.ts +0 -6
  307. package/src/ExpoAudioStream.types.ts +0 -874
  308. package/src/ExpoAudioStream.web.ts +0 -905
  309. package/src/ExpoAudioStreamModule.ts +0 -990
  310. package/src/WebRecorder.web.ts +0 -1005
  311. package/src/constants/platformLimitations.ts +0 -118
  312. package/src/constants.ts +0 -18
  313. package/src/events.ts +0 -60
  314. package/src/hooks/useAudioDevices.ts +0 -213
  315. package/src/index.ts +0 -54
  316. package/src/trimAudio.ts +0 -94
  317. package/src/types/crc-32.d.ts +0 -9
  318. package/src/useAudioRecorder.tsx +0 -766
  319. package/src/utils/BlobFix.ts +0 -561
  320. package/src/utils/audioProcessing.ts +0 -205
  321. package/src/utils/cleanNativeOptions.ts +0 -18
  322. package/src/utils/concatenateBuffers.ts +0 -24
  323. package/src/utils/convertPCMToFloat32.ts +0 -170
  324. package/src/utils/crc32.ts +0 -59
  325. package/src/utils/encodingToBitDepth.ts +0 -18
  326. package/src/utils/getWavFileInfo.ts +0 -132
  327. package/src/utils/writeWavHeader.ts +0 -115
  328. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -855
  329. package/src/workers/inlineAudioWebWorker.web.tsx +0 -180
@@ -1,1005 +0,0 @@
1
- // packages/expo-audio-stream/src/WebRecorder.web.ts
2
-
3
- import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
4
- import { ConsoleLike, RecordingConfig } from './ExpoAudioStream.types'
5
- import {
6
- EmitAudioAnalysisFunction,
7
- EmitAudioEventFunction,
8
- } from './ExpoAudioStream.web'
9
- import { encodingToBitDepth } from './utils/encodingToBitDepth'
10
- import { writeWavHeader } from './utils/writeWavHeader'
11
- import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web'
12
- import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web'
13
-
14
- interface AudioWorkletEvent {
15
- data: {
16
- command: string
17
- recordedData?: Float32Array
18
- sampleRate?: number
19
- position?: number
20
- message?: string // For debug messages
21
- }
22
- }
23
-
24
- interface AudioFeaturesEvent {
25
- data: {
26
- command: string
27
- result: AudioAnalysis
28
- }
29
- }
30
-
31
- const DEFAULT_WEB_BITDEPTH = 32
32
- const DEFAULT_SEGMENT_DURATION_MS = 100
33
- const DEFAULT_WEB_INTERVAL = 500
34
- const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1
35
-
36
- const TAG = 'WebRecorder'
37
-
38
- export class WebRecorder {
39
- public audioContext: AudioContext
40
- private audioWorkletNode!: AudioWorkletNode
41
- private featureExtractorWorker?: Worker
42
- private source: MediaStreamAudioSourceNode
43
- private emitAudioEventCallback: EmitAudioEventFunction
44
- private emitAudioAnalysisCallback: EmitAudioAnalysisFunction
45
- private config: RecordingConfig
46
- private position: number = 0
47
- private numberOfChannels: number // Number of audio channels
48
- private bitDepth: number // Bit depth of the audio
49
- private exportBitDepth: number // Bit depth of the audio
50
- private audioAnalysisData: AudioAnalysis // Keep updating the full audio analysis data with latest events
51
- private readonly logger?: ConsoleLike
52
- private compressedMediaRecorder: MediaRecorder | null = null
53
- private compressedChunks: Blob[] = []
54
- private compressedSize: number = 0
55
- private pendingCompressedChunk: Blob | null = null
56
- private dataPointIdCounter: number = 0 // Add this property to track the counter
57
- private deviceDisconnectionHandler: (() => void) | null = null
58
- private readonly mediaStream: MediaStream | null = null
59
- private readonly onInterruptionCallback?: (event: {
60
- reason: string
61
- isPaused: boolean
62
- timestamp: number
63
- }) => void
64
- private _isDeviceDisconnected: boolean = false
65
- private pcmData: Float32Array | null = null // Store original PCM data
66
- private totalSampleCount: number = 0
67
-
68
- /**
69
- * Flag to indicate whether this is the first audio chunk after a device switch
70
- * Used to maintain proper duration counting
71
- */
72
- public isFirstChunkAfterSwitch: boolean = false
73
-
74
- /**
75
- * Gets whether the recording device has been disconnected
76
- */
77
- get isDeviceDisconnected(): boolean {
78
- return this._isDeviceDisconnected
79
- }
80
-
81
- /**
82
- * Initializes a new WebRecorder instance for audio recording and processing
83
- * @param audioContext - The AudioContext to use for recording
84
- * @param source - The MediaStreamAudioSourceNode providing the audio input
85
- * @param recordingConfig - Configuration options for the recording
86
- * @param emitAudioEventCallback - Callback function for audio data events
87
- * @param emitAudioAnalysisCallback - Callback function for audio analysis events
88
- * @param onInterruption - Callback for recording interruptions
89
- * @param logger - Optional logger for debugging information
90
- */
91
- constructor({
92
- audioContext,
93
- source,
94
- recordingConfig,
95
- emitAudioEventCallback,
96
- emitAudioAnalysisCallback,
97
- onInterruption,
98
- logger,
99
- }: {
100
- audioContext: AudioContext
101
- source: MediaStreamAudioSourceNode
102
- recordingConfig: RecordingConfig
103
- emitAudioEventCallback: EmitAudioEventFunction
104
- emitAudioAnalysisCallback: EmitAudioAnalysisFunction
105
- onInterruption?: (event: {
106
- reason: string
107
- isPaused: boolean
108
- timestamp: number
109
- }) => void
110
- logger?: ConsoleLike
111
- }) {
112
- this.audioContext = audioContext
113
- this.source = source
114
- this.emitAudioEventCallback = emitAudioEventCallback
115
- this.emitAudioAnalysisCallback = emitAudioAnalysisCallback
116
- this.config = recordingConfig
117
- this.logger = logger
118
-
119
- const audioContextFormat = this.checkAudioContextFormat({
120
- sampleRate: this.audioContext.sampleRate,
121
- })
122
- this.logger?.debug('Initialized WebRecorder with config:', {
123
- sampleRate: audioContextFormat.sampleRate,
124
- bitDepth: audioContextFormat.bitDepth,
125
- numberOfChannels: audioContextFormat.numberOfChannels,
126
- })
127
-
128
- this.bitDepth = audioContextFormat.bitDepth
129
- this.numberOfChannels =
130
- audioContextFormat.numberOfChannels ||
131
- DEFAULT_WEB_NUMBER_OF_CHANNELS // Default to 1 if not available
132
- this.exportBitDepth =
133
- encodingToBitDepth({
134
- encoding: recordingConfig.encoding ?? 'pcm_32bit',
135
- }) ||
136
- audioContextFormat.bitDepth ||
137
- DEFAULT_WEB_BITDEPTH
138
-
139
- this.audioAnalysisData = {
140
- amplitudeRange: { min: 0, max: 0 },
141
- rmsRange: { min: 0, max: 0 },
142
- dataPoints: [],
143
- durationMs: 0,
144
- samples: 0,
145
- bitDepth: this.bitDepth,
146
- numberOfChannels: this.numberOfChannels,
147
- sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
148
- segmentDurationMs:
149
- this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms segments
150
- extractionTimeMs: 0,
151
- }
152
-
153
- if (recordingConfig.enableProcessing) {
154
- this.initFeatureExtractorWorker()
155
- }
156
-
157
- // Initialize compressed recording if enabled
158
- if (recordingConfig.output?.compressed?.enabled) {
159
- this.initializeCompressedRecorder()
160
- }
161
-
162
- this.mediaStream = source.mediaStream
163
- this.onInterruptionCallback = onInterruption
164
-
165
- // Setup device disconnection detection
166
- this.setupDeviceDisconnectionDetection()
167
- }
168
-
169
- /**
170
- * Initializes the audio worklet using an inline script
171
- * Creates and connects the audio processing pipeline
172
- */
173
- async init() {
174
- try {
175
- // Create and use inline audio worklet
176
- const blob = new Blob([InlineAudioWebWorker], {
177
- type: 'application/javascript',
178
- })
179
- const url = URL.createObjectURL(blob)
180
- await this.audioContext.audioWorklet.addModule(url)
181
-
182
- this.audioWorkletNode = new AudioWorkletNode(
183
- this.audioContext,
184
- 'recorder-processor'
185
- )
186
-
187
- this.audioWorkletNode.port.onmessage = async (
188
- event: AudioWorkletEvent
189
- ) => {
190
- const command = event.data.command
191
- if (command === 'debug') {
192
- this.logger?.debug(`[AudioWorklet] ${event.data.message}`)
193
- return
194
- }
195
-
196
- if (command !== 'newData') return
197
-
198
- const pcmBufferFloat = event.data.recordedData
199
- if (!pcmBufferFloat) {
200
- this.logger?.warn('Received empty audio buffer', event)
201
- return
202
- }
203
-
204
- // Process data in smaller chunks and emit immediately
205
- const sampleRate =
206
- event.data.sampleRate ?? this.audioContext.sampleRate
207
- // Use chunk size from config interval or default to 2 seconds
208
- const intervalMs = this.config.interval ?? DEFAULT_WEB_INTERVAL
209
- const chunkSize = Math.floor(sampleRate * (intervalMs / 1000))
210
- const duration = pcmBufferFloat.length / sampleRate
211
-
212
- // Use incoming position if provided by worklet, otherwise use our tracked position
213
- const incomingPosition =
214
- typeof event.data.position === 'number'
215
- ? event.data.position
216
- : this.position
217
-
218
- // Simple position tracking for logging (no duplicate filtering)
219
- this.logger?.debug(
220
- `Audio chunk: position=${incomingPosition.toFixed(3)}s, size=${pcmBufferFloat.length}`
221
- )
222
-
223
- // Calculate bytes per sample based on bit depth
224
- const bytesPerSample = this.bitDepth / 8
225
-
226
- // Emit chunks without storing them
227
- for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
228
- const chunk = pcmBufferFloat.slice(i, i + chunkSize)
229
- const chunkPosition = incomingPosition + i / sampleRate
230
-
231
- // Calculate byte positions and samples
232
- const startPosition = Math.floor(i * bytesPerSample)
233
- const endPosition = Math.floor(
234
- (i + chunk.length) * bytesPerSample
235
- )
236
- const samples = chunk.length // Number of samples in this chunk
237
-
238
- // Only store PCM data if primary output is enabled
239
- const shouldStoreUncompressed =
240
- this.config.output?.primary?.enabled ?? true
241
-
242
- // Store PCM chunks when needed - this is for the final WAV file
243
- if (shouldStoreUncompressed) {
244
- // Store the original Float32Array data for later WAV creation
245
- this.appendPcmData(chunk)
246
- this.totalSampleCount += chunk.length
247
- }
248
-
249
- // Process features if enabled
250
- if (
251
- this.config.enableProcessing &&
252
- this.featureExtractorWorker
253
- ) {
254
- this.featureExtractorWorker.postMessage({
255
- command: 'process',
256
- channelData: chunk,
257
- sampleRate,
258
- segmentDurationMs:
259
- this.config.segmentDurationMs ??
260
- DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
261
- bitDepth: this.bitDepth,
262
- fullAudioDurationMs: chunkPosition * 1000,
263
- numberOfChannels: this.numberOfChannels,
264
- features: this.config.features,
265
- intervalAnalysis: this.config.intervalAnalysis,
266
- startPosition,
267
- endPosition,
268
- samples,
269
- })
270
- }
271
-
272
- // Prepare compression data if available
273
- const compression = this.pendingCompressedChunk
274
- ? {
275
- data: this.pendingCompressedChunk,
276
- size: this.pendingCompressedChunk.size,
277
- totalSize: this.compressedSize,
278
- mimeType: 'audio/webm',
279
- format:
280
- this.config.output?.compressed?.format ??
281
- 'opus',
282
- bitrate:
283
- this.config.output?.compressed?.bitrate ??
284
- 128000,
285
- }
286
- : undefined
287
-
288
- // Emit chunk immediately - whether compressed or not
289
- this.emitAudioEventCallback({
290
- data: chunk,
291
- position: chunkPosition,
292
- compression,
293
- })
294
-
295
- // Reset pending compressed chunk after we've used it
296
- this.pendingCompressedChunk = null
297
- }
298
-
299
- // Update our position based on the worklet's position if provided
300
- this.position = incomingPosition + duration
301
- }
302
-
303
- // Ensure we use all relevant settings from config
304
- const recordSampleRate = this.audioContext.sampleRate
305
- const exportSampleRate =
306
- this.config.sampleRate ?? this.audioContext.sampleRate
307
- const channels = this.config.channels ?? this.numberOfChannels
308
- const interval = this.config.interval ?? DEFAULT_WEB_INTERVAL
309
-
310
- this.logger?.debug(`WebRecorder initialized with config:`, {
311
- recordSampleRate,
312
- exportSampleRate,
313
- bitDepth: this.bitDepth,
314
- exportBitDepth: this.exportBitDepth,
315
- channels,
316
- interval,
317
- position: this.position,
318
- deviceId: this.config.deviceId ?? 'default',
319
- compression: this.config.output?.compressed
320
- ? {
321
- enabled: this.config.output.compressed.enabled,
322
- format: this.config.output.compressed.format,
323
- bitrate: this.config.output.compressed.bitrate,
324
- }
325
- : 'disabled',
326
- })
327
-
328
- // Initialize the worklet with all settings from config
329
- this.audioWorkletNode.port.postMessage({
330
- command: 'init',
331
- recordSampleRate,
332
- exportSampleRate,
333
- bitDepth: this.bitDepth,
334
- exportBitDepth: this.exportBitDepth,
335
- channels,
336
- interval,
337
- position: this.position, // Pass the current position to the processor
338
- enableLogging: true,
339
- })
340
-
341
- // Connect the source to the AudioWorkletNode and start recording
342
- this.source.connect(this.audioWorkletNode)
343
- this.audioWorkletNode.connect(this.audioContext.destination)
344
- } catch (error) {
345
- console.error(`[${TAG}] Failed to initialize WebRecorder`, error)
346
- }
347
- }
348
-
349
- /**
350
- * Append new PCM data to the existing buffer
351
- * @param newData New Float32Array data to append
352
- */
353
- private appendPcmData(newData: Float32Array): void {
354
- // Clone the incoming data to ensure it's not modified
355
- const dataToAdd = new Float32Array(newData)
356
-
357
- if (!this.pcmData) {
358
- // First chunk - create a copy to avoid references to original data
359
- this.pcmData = new Float32Array(dataToAdd)
360
- return
361
- }
362
-
363
- // Create a new buffer with increased size
364
- const newBuffer = new Float32Array(
365
- this.pcmData.length + dataToAdd.length
366
- )
367
-
368
- // Copy existing data
369
- newBuffer.set(this.pcmData)
370
-
371
- // Append new data
372
- newBuffer.set(dataToAdd, this.pcmData.length)
373
-
374
- // Replace existing buffer
375
- this.pcmData = newBuffer
376
- }
377
-
378
- /**
379
- * Initializes the feature extractor worker for audio analysis
380
- * Creates an inline worker from a blob for audio feature extraction
381
- */
382
- initFeatureExtractorWorker() {
383
- try {
384
- const blob = new Blob([InlineFeaturesExtractor], {
385
- type: 'application/javascript',
386
- })
387
- const url = URL.createObjectURL(blob)
388
- this.featureExtractorWorker = new Worker(url)
389
- this.featureExtractorWorker.onmessage =
390
- this.handleFeatureExtractorMessage.bind(this)
391
- this.featureExtractorWorker.onerror = (error) => {
392
- console.error(`[${TAG}] Feature extractor worker error:`, error)
393
- }
394
-
395
- // Initialize worker with counter if needed
396
- if (this.dataPointIdCounter > 0) {
397
- this.featureExtractorWorker.postMessage({
398
- command: 'resetCounter',
399
- value: this.dataPointIdCounter,
400
- })
401
- this.logger?.debug(
402
- `Initialized worker with counter value ${this.dataPointIdCounter}`
403
- )
404
- }
405
-
406
- this.logger?.log(
407
- 'Feature extractor worker initialized successfully'
408
- )
409
- } catch (error) {
410
- console.error(
411
- `[${TAG}] Failed to initialize feature extractor worker`,
412
- error
413
- )
414
- }
415
- }
416
-
417
- /**
418
- * Processes audio analysis results from the feature extractor worker
419
- * Updates the audio analysis data and emits events
420
- * @param event - The event containing audio analysis results
421
- */
422
- handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
423
- if (event.data.command !== 'features') return
424
-
425
- const segmentResult = event.data.result
426
- const uniqueNewDataPoints = this.filterUniqueDataPoints(
427
- segmentResult.dataPoints
428
- )
429
-
430
- // Update counter based on the highest ID seen
431
- this.updateDataPointCounter(uniqueNewDataPoints)
432
-
433
- // Update analysis data with the new results
434
- this.updateAudioAnalysisData(segmentResult, uniqueNewDataPoints)
435
-
436
- // Send filtered result to avoid duplicate IDs
437
- const filteredSegmentResult = {
438
- ...segmentResult,
439
- dataPoints: uniqueNewDataPoints,
440
- }
441
-
442
- this.emitAudioAnalysisCallback(filteredSegmentResult)
443
- }
444
-
445
- /**
446
- * Filters out data points with duplicate IDs
447
- */
448
- private filterUniqueDataPoints(dataPoints: any[]): any[] {
449
- // Track existing IDs to prevent duplicates
450
- const existingIds = new Set(
451
- this.audioAnalysisData.dataPoints.map((dp) => dp.id)
452
- )
453
-
454
- // Filter out datapoints with duplicate IDs
455
- const uniquePoints = dataPoints.filter((dp) => !existingIds.has(dp.id))
456
-
457
- // Log filtered duplicates if any
458
- if (uniquePoints.length < dataPoints.length && this.logger?.warn) {
459
- this.logger.warn(
460
- `Filtered ${dataPoints.length - uniquePoints.length} duplicate datapoints`
461
- )
462
- }
463
-
464
- return uniquePoints
465
- }
466
-
467
- /**
468
- * Updates the counter based on the highest ID in datapoints
469
- */
470
- private updateDataPointCounter(dataPoints: any[]): void {
471
- if (dataPoints.length === 0) return
472
-
473
- const lastDataPoint = dataPoints[dataPoints.length - 1]
474
- if (lastDataPoint && typeof lastDataPoint.id === 'number') {
475
- const nextIdValue = lastDataPoint.id + 1
476
- if (nextIdValue > this.dataPointIdCounter) {
477
- this.dataPointIdCounter = nextIdValue
478
- this.logger?.debug(
479
- `Counter updated to ${this.dataPointIdCounter}`
480
- )
481
- }
482
- }
483
- }
484
-
485
- /**
486
- * Updates audio analysis data with segment results
487
- */
488
- private updateAudioAnalysisData(
489
- segmentResult: AudioAnalysis,
490
- uniqueDataPoints: any[]
491
- ): void {
492
- // Add unique data points to our analysis data
493
- this.audioAnalysisData.dataPoints.push(...uniqueDataPoints)
494
- this.audioAnalysisData.durationMs += segmentResult.durationMs
495
- this.audioAnalysisData.sampleRate = segmentResult.sampleRate
496
-
497
- // Update amplitude range if present
498
- if (segmentResult.amplitudeRange) {
499
- this.audioAnalysisData.amplitudeRange = this.mergeRange(
500
- this.audioAnalysisData.amplitudeRange,
501
- segmentResult.amplitudeRange
502
- )
503
- }
504
-
505
- // Update RMS range if present
506
- if (segmentResult.rmsRange) {
507
- this.audioAnalysisData.rmsRange = this.mergeRange(
508
- this.audioAnalysisData.rmsRange,
509
- segmentResult.rmsRange
510
- )
511
- }
512
- }
513
-
514
- /**
515
- * Merges value ranges
516
- */
517
- private mergeRange(
518
- existing: { min: number; max: number } | undefined,
519
- newRange: { min: number; max: number }
520
- ): { min: number; max: number } {
521
- if (!existing) return { ...newRange }
522
-
523
- return {
524
- min: Math.min(existing.min, newRange.min),
525
- max: Math.max(existing.max, newRange.max),
526
- }
527
- }
528
-
529
- /**
530
- * Reset the data point counter to a specific value or zero
531
- * @param startCounterFrom Optional value to start the counter from (for continuing from previous recordings)
532
- */
533
- resetDataPointCounter(startCounterFrom?: number): void {
534
- // Set the counter with the passed value or 0
535
- this.dataPointIdCounter = startCounterFrom ?? 0
536
- this.logger?.debug(
537
- `Reset data point counter to ${this.dataPointIdCounter}`
538
- )
539
-
540
- // Update worker counter if available
541
- if (this.featureExtractorWorker) {
542
- this.featureExtractorWorker.postMessage({
543
- command: 'resetCounter',
544
- value: this.dataPointIdCounter,
545
- })
546
- } else {
547
- this.logger?.warn(
548
- 'No feature extractor worker available to update counter'
549
- )
550
- }
551
- }
552
-
553
- /**
554
- * Get the current data point counter value
555
- * @returns The current value of the data point counter
556
- */
557
- getDataPointCounter(): number {
558
- return this.dataPointIdCounter
559
- }
560
-
561
- /**
562
- * Prepares the recorder for continuity after device switch
563
- * Sets up all necessary state to maintain proper recording continuity
564
- */
565
- prepareForDeviceSwitch(): void {
566
- this.isFirstChunkAfterSwitch = true
567
- this.logger?.debug(
568
- `Prepared for device switch at position ${this.position}s`
569
- )
570
- }
571
-
572
- /**
573
- * Starts the audio recording process
574
- * Connects the audio nodes and begins capturing audio data
575
- * @param preserveCounters If true, do not reset the counter (used for device switching)
576
- */
577
- start(preserveCounters = false) {
578
- this.source.connect(this.audioWorkletNode)
579
- this.audioWorkletNode.connect(this.audioContext.destination)
580
-
581
- // Only reset the counter when not preserving state (e.g., for a fresh recording)
582
- if (!preserveCounters) {
583
- this.logger?.debug(
584
- 'Starting fresh recording, resetting counter to 0'
585
- )
586
- this.resetDataPointCounter(0) // Explicitly reset to 0 for new recordings
587
- this.isFirstChunkAfterSwitch = false
588
-
589
- // Clear PCM data for new recording
590
- this.pcmData = null
591
- this.totalSampleCount = 0
592
- } else {
593
- this.logger?.debug(
594
- `Preserving counter at ${this.dataPointIdCounter} during device switch`
595
- )
596
- }
597
-
598
- if (this.compressedMediaRecorder) {
599
- this.compressedMediaRecorder.start(this.config.interval ?? 1000)
600
- }
601
- }
602
-
603
- /**
604
- * Creates a WAV file from the stored PCM data
605
- */
606
- private createWavFromPcmData(): Blob | null {
607
- try {
608
- // Check if we have PCM data
609
- if (!this.pcmData || this.pcmData.length === 0) {
610
- this.logger?.warn('No PCM data available to create WAV file')
611
- return null
612
- }
613
-
614
- const sampleRate =
615
- this.config.sampleRate ?? this.audioContext.sampleRate
616
- const channels = this.numberOfChannels || 1
617
-
618
- // Convert float32 PCM data to 16-bit PCM for WAV
619
- const bytesPerSample = 2 // 16-bit = 2 bytes
620
- const dataLength = this.pcmData.length * bytesPerSample
621
- const buffer = new ArrayBuffer(dataLength)
622
- const view = new DataView(buffer)
623
-
624
- // Convert Float32Array (-1 to 1) to Int16Array (-32768 to 32767)
625
- for (let i = 0; i < this.pcmData.length; i++) {
626
- const sample = Math.max(-1, Math.min(1, this.pcmData[i]))
627
- const int16Value = Math.round(sample * 32767)
628
- view.setInt16(i * 2, int16Value, true)
629
- }
630
-
631
- // Use the existing writeWavHeader utility to add a WAV header
632
- const wavBuffer = writeWavHeader({
633
- buffer,
634
- sampleRate,
635
- numChannels: channels,
636
- bitDepth: 16,
637
- isFloat: false,
638
- })
639
-
640
- return new Blob([wavBuffer], { type: 'audio/wav' })
641
- } catch (error) {
642
- this.logger?.error('Error creating WAV file from PCM data:', error)
643
- return null
644
- }
645
- }
646
-
647
- /**
648
- * Stops the audio recording process and returns the recorded data
649
- * @returns Promise resolving to an object containing compressed and/or uncompressed blobs
650
- */
651
- async stop(): Promise<{ compressedBlob?: Blob; uncompressedBlob?: Blob }> {
652
- try {
653
- // Stop any compressed recording first
654
- if (
655
- this.compressedMediaRecorder &&
656
- this.compressedMediaRecorder.state !== 'inactive'
657
- ) {
658
- this.compressedMediaRecorder.stop()
659
- }
660
-
661
- // Wait for any pending compressed chunks to be processed
662
- if (this.compressedMediaRecorder) {
663
- // Small delay to ensure all data is processed
664
- await new Promise((resolve) => setTimeout(resolve, 100))
665
- }
666
-
667
- // Create uncompressed WAV file from the PCM data
668
- let uncompressedBlob: Blob | undefined
669
-
670
- // Only create WAV if we have PCM data
671
- if (this.pcmData && this.pcmData.length > 0) {
672
- uncompressedBlob = this.createWavFromPcmData() || undefined
673
- }
674
-
675
- // Return the compressed and/or uncompressed blobs if available
676
- return {
677
- compressedBlob:
678
- this.compressedChunks.length > 0
679
- ? new Blob(this.compressedChunks, {
680
- type: 'audio/webm;codecs=opus',
681
- })
682
- : undefined,
683
- uncompressedBlob,
684
- }
685
- } finally {
686
- this.cleanup()
687
- // Reset the chunks array
688
- this.compressedChunks = []
689
- this.compressedSize = 0
690
- this.pendingCompressedChunk = null
691
- this.pcmData = null
692
- this.totalSampleCount = 0
693
- this.dataPointIdCounter = 0 // Reset counter
694
- }
695
- }
696
-
697
- /**
698
- * Cleans up resources when recording is stopped
699
- * Closes audio context and disconnects nodes
700
- */
701
- public cleanup() {
702
- // Remove device disconnection handler
703
- if (this.deviceDisconnectionHandler) {
704
- this.deviceDisconnectionHandler()
705
- this.deviceDisconnectionHandler = null
706
- }
707
-
708
- // Check if AudioContext is already closed before attempting to close it
709
- if (this.audioContext && this.audioContext.state !== 'closed') {
710
- this.audioContext.close().catch((e) => {
711
- // Log closure errors but continue cleanup
712
- this.logger?.warn('Error closing AudioContext:', e)
713
- })
714
- }
715
-
716
- // Safely disconnect audioWorkletNode if it exists
717
- if (this.audioWorkletNode) {
718
- try {
719
- this.audioWorkletNode.disconnect()
720
- } catch (e) {
721
- // Log disconnection errors but continue cleanup
722
- this.logger?.warn('Error disconnecting audioWorkletNode:', e)
723
- }
724
- }
725
-
726
- // Safely disconnect source if it exists
727
- if (this.source) {
728
- try {
729
- this.source.disconnect()
730
- } catch (e) {
731
- // Log disconnection errors but continue cleanup
732
- this.logger?.warn('Error disconnecting source:', e)
733
- }
734
- }
735
-
736
- // Always stop media stream tracks to release hardware resources
737
- this.stopMediaStreamTracks()
738
-
739
- // Mark as disconnected to prevent future errors
740
- this._isDeviceDisconnected = true
741
- }
742
-
743
- /**
744
- * Pauses the audio recording process
745
- * Disconnects audio nodes and pauses the media recorder
746
- */
747
- pause() {
748
- try {
749
- // Note: We're just pausing, not disconnecting the device
750
- // Simply disconnect nodes temporarily without marking device as disconnected
751
- this.source.disconnect(this.audioWorkletNode)
752
- this.audioWorkletNode.disconnect(this.audioContext.destination)
753
- this.audioWorkletNode.port.postMessage({ command: 'pause' })
754
-
755
- if (this.compressedMediaRecorder?.state === 'recording') {
756
- this.compressedMediaRecorder.pause()
757
- }
758
-
759
- this.logger?.debug('Recording paused successfully')
760
- } catch (error) {
761
- this.logger?.error('Error in pause(): ', error)
762
- // Already disconnected, just ignore and continue
763
- }
764
- }
765
-
766
- /**
767
- * Stops all media stream tracks to release hardware resources
768
- * Ensures recording indicators (like microphone icon) are turned off
769
- */
770
- public stopMediaStreamTracks() {
771
- // Stop all audio tracks to stop the recording icon
772
- if (this.mediaStream) {
773
- const tracks = this.mediaStream.getTracks()
774
- tracks.forEach((track) => track.stop())
775
- } else if (this.source?.mediaStream) {
776
- const tracks = this.source.mediaStream.getTracks()
777
- tracks.forEach((track) => track.stop())
778
- }
779
- }
780
-
781
- /**
782
- * Determines the audio format capabilities of the current audio context
783
- * @param sampleRate - The sample rate to check
784
- * @returns Object containing format information (sample rate, bit depth, channels)
785
- */
786
- private checkAudioContextFormat({ sampleRate }: { sampleRate: number }) {
787
- // Create a silent AudioBuffer
788
- const frameCount = sampleRate * 1.0 // 1 second buffer
789
- const audioBuffer = this.audioContext.createBuffer(
790
- 1,
791
- frameCount,
792
- sampleRate
793
- )
794
-
795
- // Check the format
796
- const channelData = audioBuffer.getChannelData(0)
797
- const bitDepth = channelData.BYTES_PER_ELEMENT * 8 // 4 bytes per element means 32-bit
798
-
799
- return {
800
- sampleRate: audioBuffer.sampleRate,
801
- bitDepth,
802
- numberOfChannels: audioBuffer.numberOfChannels,
803
- }
804
- }
805
-
806
- /**
807
- * Resumes a paused recording
808
- * Reconnects audio nodes and resumes the media recorder
809
- */
810
- resume() {
811
- // If device was disconnected, we can't resume
812
- if (this._isDeviceDisconnected) {
813
- this.logger?.warn('Cannot resume recording: device disconnected')
814
- return
815
- }
816
-
817
- try {
818
- this.source.connect(this.audioWorkletNode)
819
- this.audioWorkletNode.connect(this.audioContext.destination)
820
- this.audioWorkletNode.port.postMessage({ command: 'resume' })
821
- this.compressedMediaRecorder?.resume()
822
- } catch (error: unknown) {
823
- this.logger?.error('Error in resume(): ', error)
824
- // Rethrow the error to inform callers
825
- throw new Error(
826
- `Failed to resume recording: ${error instanceof Error ? error.message : 'unknown error'}`
827
- )
828
- }
829
- }
830
-
831
- /**
832
- * Initializes the compressed media recorder if compression is enabled
833
- * Sets up event handlers for compressed audio data
834
- */
835
- private initializeCompressedRecorder() {
836
- try {
837
- const mimeType = 'audio/webm;codecs=opus'
838
- if (!MediaRecorder.isTypeSupported(mimeType)) {
839
- this.logger?.warn(
840
- 'Opus compression not supported in this browser'
841
- )
842
- return
843
- }
844
-
845
- this.compressedMediaRecorder = new MediaRecorder(
846
- this.source.mediaStream,
847
- {
848
- mimeType,
849
- audioBitsPerSecond:
850
- this.config.output?.compressed?.bitrate ?? 128000,
851
- }
852
- )
853
-
854
- this.compressedMediaRecorder.ondataavailable = (event) => {
855
- if (event.data.size > 0) {
856
- // Store the compressed chunk for final blob creation
857
- this.compressedChunks.push(event.data)
858
- this.compressedSize += event.data.size
859
-
860
- // Store the pending compressed chunk for the next PCM chunk to use
861
- this.pendingCompressedChunk = event.data
862
- }
863
- }
864
- } catch (error) {
865
- this.logger?.error(
866
- 'Failed to initialize compressed recorder:',
867
- error
868
- )
869
- // Setting to null to indicate initialization failed
870
- this.compressedMediaRecorder = null
871
- }
872
- }
873
-
874
- /**
875
- * Processes features if enabled
876
- */
877
- processFeatures(
878
- chunk: Float32Array,
879
- sampleRate: number,
880
- chunkPosition: number,
881
- startPosition: number,
882
- endPosition: number,
883
- samples: number
884
- ) {
885
- if (this.config.enableProcessing && this.featureExtractorWorker) {
886
- this.featureExtractorWorker.postMessage({
887
- command: 'process',
888
- channelData: chunk,
889
- sampleRate,
890
- segmentDurationMs:
891
- this.config.segmentDurationMs ??
892
- DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
893
- bitDepth: this.bitDepth,
894
- fullAudioDurationMs: chunkPosition * 1000,
895
- numberOfChannels: this.numberOfChannels,
896
- features: this.config.features,
897
- intervalAnalysis: this.config.intervalAnalysis,
898
- startPosition,
899
- endPosition,
900
- samples,
901
- })
902
- }
903
- }
904
-
905
- /**
906
- * Sets up detection for device disconnection events
907
- */
908
- private setupDeviceDisconnectionDetection() {
909
- if (!this.mediaStream) return
910
-
911
- // Function to handle track ending (which happens on device disconnection)
912
- const handleTrackEnded = () => {
913
- this.logger?.warn('Audio track ended - device disconnected')
914
- this._isDeviceDisconnected = true
915
-
916
- // Use the callback to notify parent component about device disconnection
917
- if (this.onInterruptionCallback) {
918
- this.onInterruptionCallback({
919
- reason: 'deviceDisconnected',
920
- isPaused: true,
921
- timestamp: Date.now(),
922
- })
923
- this.logger?.debug('Notified about device disconnection')
924
- }
925
-
926
- // Ensure we disconnect nodes to prevent zombie recordings
927
- if (this.audioWorkletNode) {
928
- this.audioWorkletNode.port.postMessage({
929
- command: 'deviceDisconnected',
930
- })
931
-
932
- try {
933
- this.source.disconnect(this.audioWorkletNode)
934
- this.audioWorkletNode.disconnect()
935
- } catch (e) {
936
- // Ignore disconnection errors as the track might already be gone
937
- this.logger?.warn(
938
- 'Error disconnecting audioWorkletNode:',
939
- e
940
- )
941
- }
942
- }
943
- }
944
-
945
- // Add listeners to all audio tracks
946
- const tracks = this.mediaStream.getAudioTracks()
947
- tracks.forEach((track) => {
948
- track.addEventListener('ended', handleTrackEnded)
949
- })
950
-
951
- // Store the handler for cleanup
952
- this.deviceDisconnectionHandler = () => {
953
- tracks.forEach((track) => {
954
- track.removeEventListener('ended', handleTrackEnded)
955
- })
956
- }
957
- }
958
-
959
- /**
960
- * Explicitly set the position for continuous recording across device switches
961
- * @param position The position in seconds to continue from
962
- */
963
- setPosition(position: number): void {
964
- if (position >= 0) {
965
- this.position = position
966
- this.logger?.debug(`Position explicitly set to ${position} seconds`)
967
- } else {
968
- this.logger?.warn(`Invalid position value: ${position}, ignoring`)
969
- }
970
- }
971
-
972
- /**
973
- * Get the current position in seconds
974
- * @returns The current position
975
- */
976
- getPosition(): number {
977
- return this.position
978
- }
979
-
980
- /**
981
- * Gets the current compressed chunks
982
- * @returns Array of current compressed audio chunks
983
- */
984
- getCompressedChunks(): Blob[] {
985
- return [...this.compressedChunks]
986
- }
987
-
988
- /**
989
- * Sets the compressed chunks from a previous recorder
990
- * @param chunks Array of compressed chunks from a previous recorder
991
- */
992
- setCompressedChunks(chunks: Blob[]): void {
993
- if (chunks && chunks.length > 0) {
994
- this.logger?.debug(
995
- `Adding ${chunks.length} compressed chunks from previous device`
996
- )
997
- this.compressedChunks = [...chunks, ...this.compressedChunks]
998
- // Update size
999
- this.compressedSize = this.compressedChunks.reduce(
1000
- (size, chunk) => size + chunk.size,
1001
- 0
1002
- )
1003
- }
1004
- }
1005
- }