neoagent 2.3.1-beta.2 → 2.3.1-beta.21

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 (264) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +23057 -0
  66. package/flutter_app/lib/main_app_shell.dart +1682 -0
  67. package/flutter_app/lib/main_integrations.dart +931 -0
  68. package/flutter_app/lib/main_launcher.dart +959 -0
  69. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  70. package/flutter_app/lib/main_models.dart +3473 -0
  71. package/flutter_app/lib/main_shared.dart +2861 -0
  72. package/flutter_app/lib/main_theme.dart +204 -0
  73. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  74. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  75. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  76. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  77. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  78. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  79. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  80. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  81. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  82. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  83. package/flutter_app/lib/src/backend_client.dart +1833 -0
  84. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  85. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  86. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  87. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  88. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  89. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  90. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  91. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  92. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  93. package/flutter_app/lib/src/health_bridge.dart +136 -0
  94. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  95. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  96. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  97. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  98. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  99. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  100. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  101. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  102. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  103. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  104. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  105. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  106. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  107. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  108. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  109. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  110. package/flutter_app/lib/src/theme/palette.dart +81 -0
  111. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  112. package/flutter_app/linux/CMakeLists.txt +128 -0
  113. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  114. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  115. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  116. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  117. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  118. package/flutter_app/linux/runner/main.cc +6 -0
  119. package/flutter_app/linux/runner/my_application.cc +144 -0
  120. package/flutter_app/linux/runner/my_application.h +18 -0
  121. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  122. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  123. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  124. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  125. package/flutter_app/macos/Podfile +42 -0
  126. package/flutter_app/macos/Podfile.lock +87 -0
  127. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  131. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  132. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  133. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  134. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  135. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  136. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  137. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  138. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  139. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  140. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  141. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  142. package/flutter_app/macos/Runner/Info.plist +36 -0
  143. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  144. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  145. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  146. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  147. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  148. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  149. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  150. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  151. package/flutter_app/patch_strings.py +12 -0
  152. package/flutter_app/pubspec.lock +1088 -0
  153. package/flutter_app/pubspec.yaml +53 -0
  154. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  155. package/flutter_app/test/recording_payloads_test.dart +53 -0
  156. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  157. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  161. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  162. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  163. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  164. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  165. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  168. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  169. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  170. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  171. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  172. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  173. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  174. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  175. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  176. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  177. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  178. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  184. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  185. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  186. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  187. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  188. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  189. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  190. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  191. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  192. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  193. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  194. package/flutter_app/web/favicon.png +0 -0
  195. package/flutter_app/web/favicon.svg +12 -0
  196. package/flutter_app/web/icons/Icon-192.png +0 -0
  197. package/flutter_app/web/icons/Icon-512.png +0 -0
  198. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  199. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  200. package/flutter_app/web/index.html +39 -0
  201. package/flutter_app/web/manifest.json +35 -0
  202. package/flutter_app/windows/CMakeLists.txt +108 -0
  203. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  204. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  205. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  206. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  207. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  208. package/flutter_app/windows/runner/Runner.rc +121 -0
  209. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  210. package/flutter_app/windows/runner/flutter_window.h +37 -0
  211. package/flutter_app/windows/runner/main.cpp +53 -0
  212. package/flutter_app/windows/runner/resource.h +16 -0
  213. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  214. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  215. package/flutter_app/windows/runner/utils.cpp +65 -0
  216. package/flutter_app/windows/runner/utils.h +19 -0
  217. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  218. package/flutter_app/windows/runner/win32_window.h +102 -0
  219. package/lib/manager.js +231 -7
  220. package/package.json +3 -1
  221. package/server/db/database.js +68 -0
  222. package/server/http/middleware.js +50 -0
  223. package/server/http/routes.js +3 -1
  224. package/server/index.js +1 -0
  225. package/server/public/.last_build_id +1 -1
  226. package/server/public/assets/NOTICES +61 -0
  227. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  228. package/server/public/flutter_bootstrap.js +1 -1
  229. package/server/public/main.dart.js +65262 -64422
  230. package/server/routes/integrations.js +86 -0
  231. package/server/routes/memory.js +11 -2
  232. package/server/routes/screenHistory.js +46 -0
  233. package/server/routes/triggers.js +81 -0
  234. package/server/services/ai/models.js +30 -0
  235. package/server/services/ai/providers/githubCopilot.js +97 -0
  236. package/server/services/ai/providers/openai.js +2 -1
  237. package/server/services/ai/providers/openaiCodex.js +31 -0
  238. package/server/services/ai/settings.js +20 -0
  239. package/server/services/ai/systemPrompt.js +1 -1
  240. package/server/services/ai/tools.js +35 -6
  241. package/server/services/browser/controller.js +47 -3
  242. package/server/services/desktop/screenRecorder.js +172 -0
  243. package/server/services/integrations/env.js +5 -0
  244. package/server/services/integrations/github/common.js +106 -0
  245. package/server/services/integrations/github/provider.js +499 -0
  246. package/server/services/integrations/github/repos.js +1124 -0
  247. package/server/services/integrations/home_assistant/provider.js +306 -26
  248. package/server/services/integrations/manager.js +63 -7
  249. package/server/services/integrations/oauth_provider.js +13 -6
  250. package/server/services/integrations/provider_config_store.js +76 -0
  251. package/server/services/integrations/registry.js +4 -0
  252. package/server/services/integrations/trello/provider.js +744 -0
  253. package/server/services/integrations/whatsapp/provider.js +6 -2
  254. package/server/services/manager.js +22 -0
  255. package/server/services/memory/manager.js +39 -2
  256. package/server/services/skills/base_catalog.js +33 -0
  257. package/server/services/tasks/adapters/index.js +1 -0
  258. package/server/services/tasks/adapters/manual.js +12 -0
  259. package/server/services/tasks/runtime.js +1 -1
  260. package/server/services/voice/openaiClient.js +4 -1
  261. package/server/services/voice/providers.js +2 -1
  262. package/server/services/widgets/service.js +49 -4
  263. package/server/utils/local_secrets.js +56 -0
  264. package/server/utils/logger.js +37 -9
@@ -0,0 +1,878 @@
1
+ #include "include/audio_capture/mic_capture_plugin.h"
2
+
3
+ #include <flutter_linux/flutter_linux.h>
4
+ #include <glib-object.h>
5
+ #include <glib.h>
6
+ #include <pulse/error.h>
7
+ #include <pulse/simple.h>
8
+ #include <pulse/pulseaudio.h>
9
+
10
+ #include <algorithm>
11
+ #include <cmath>
12
+ #include <cstdint>
13
+ #include <memory>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ namespace {
18
+
19
+ constexpr char kMethodChannelName[] = "com.mic_audio_transcriber/mic_capture";
20
+ constexpr char kEventChannelName[] = "com.mic_audio_transcriber/mic_stream";
21
+ constexpr char kStatusEventChannelName[] = "com.mic_audio_transcriber/mic_status";
22
+ constexpr char kDecibelEventChannelName[] = "com.mic_audio_transcriber/mic_decibel";
23
+
24
+ constexpr int kDefaultSampleRate = 16000;
25
+ constexpr int kDefaultChannels = 1;
26
+ constexpr int kDefaultBitsPerSample = 16;
27
+ constexpr float kDefaultGainBoost = 2.5f;
28
+ constexpr float kDefaultInputVolume = 1.0f;
29
+ constexpr size_t kBufferSizeFrames = 4096;
30
+
31
+ struct AudioChunkPayload {
32
+ AudioChunkPayload(MicCapturePlugin* plugin, GBytes* bytes, double decibel)
33
+ : plugin(plugin), bytes(bytes), decibel(decibel) {}
34
+
35
+ MicCapturePlugin* plugin;
36
+ GBytes* bytes;
37
+ double decibel;
38
+ };
39
+
40
+ struct CaptureThreadContext {
41
+ MicCapturePlugin* plugin;
42
+ pa_simple* stream;
43
+ size_t chunk_size;
44
+ int sample_rate;
45
+ int channels;
46
+ int bits_per_sample;
47
+ float gain_boost;
48
+ float input_volume;
49
+ };
50
+
51
+ gboolean EmitAudioOnMainThread(gpointer user_data);
52
+ gpointer CaptureThread(gpointer user_data);
53
+ double CalculateDecibel(const int16_t* samples, size_t sample_count);
54
+ std::string GetCurrentDeviceName();
55
+ bool IsBluetoothDevice();
56
+ void CleanupExistingCapture(MicCapturePlugin* plugin);
57
+ bool OpenPulseStreamWithRetry(int sample_rate, int channels, int bits_per_sample,
58
+ size_t chunk_size, bool is_bluetooth,
59
+ pa_simple** out_stream, std::string* error_message);
60
+
61
+ } // namespace
62
+
63
+ struct _MicCapturePlugin {
64
+ GObject parent_instance;
65
+
66
+ FlMethodChannel* method_channel;
67
+ FlEventChannel* event_channel;
68
+ FlEventChannel* status_event_channel;
69
+ FlEventChannel* decibel_event_channel;
70
+ GMainContext* main_context;
71
+
72
+ GMutex lock;
73
+ gint should_stop;
74
+ gboolean is_capturing;
75
+ gboolean has_listener;
76
+ gboolean has_status_listener;
77
+ gboolean has_decibel_listener;
78
+
79
+ GThread* capture_thread;
80
+ gchar* current_device_name;
81
+ };
82
+
83
+ G_DEFINE_TYPE(MicCapturePlugin, mic_capture_plugin, G_TYPE_OBJECT)
84
+
85
+ namespace {
86
+
87
+ bool CheckMicSupport() {
88
+ pa_simple* stream = nullptr;
89
+ pa_sample_spec spec;
90
+ spec.rate = kDefaultSampleRate;
91
+ spec.channels = static_cast<uint8_t>(kDefaultChannels);
92
+ spec.format = PA_SAMPLE_S16LE;
93
+
94
+ pa_buffer_attr attr;
95
+ attr.maxlength = static_cast<uint32_t>(-1);
96
+ attr.tlength = static_cast<uint32_t>(-1);
97
+ attr.prebuf = static_cast<uint32_t>(-1);
98
+ attr.minreq = static_cast<uint32_t>(-1);
99
+ attr.fragsize = static_cast<uint32_t>(-1);
100
+
101
+ int error = 0;
102
+ // Try to open default source (microphone)
103
+ stream = pa_simple_new(nullptr, "Voxa", PA_STREAM_RECORD, nullptr,
104
+ "Mic Check", &spec, nullptr, &attr, &error);
105
+
106
+ if (stream == nullptr) {
107
+ return false;
108
+ }
109
+
110
+ pa_simple_free(stream);
111
+ return true;
112
+ }
113
+
114
+ size_t CalculateChunkSize(int sample_rate, int channels, int bits_per_sample) {
115
+ const int bytes_per_sample = std::max(bits_per_sample / 8, 1);
116
+ const size_t frame_size = static_cast<size_t>(channels) * bytes_per_sample;
117
+ return kBufferSizeFrames * frame_size;
118
+ }
119
+
120
+ bool OpenPulseStream(int sample_rate, int channels, int bits_per_sample,
121
+ size_t chunk_size, pa_simple** out_stream,
122
+ std::string* error_message) {
123
+ pa_sample_spec spec;
124
+ spec.rate = sample_rate;
125
+ spec.channels = static_cast<uint8_t>(channels);
126
+ if (bits_per_sample == 16) {
127
+ spec.format = PA_SAMPLE_S16LE;
128
+ } else {
129
+ spec.format = PA_SAMPLE_S16LE;
130
+ }
131
+
132
+ pa_buffer_attr attr;
133
+ attr.maxlength = static_cast<uint32_t>(chunk_size * 4);
134
+ attr.tlength = static_cast<uint32_t>(-1);
135
+ attr.prebuf = static_cast<uint32_t>(-1);
136
+ attr.minreq = static_cast<uint32_t>(-1);
137
+ attr.fragsize = static_cast<uint32_t>(chunk_size);
138
+
139
+ int error = 0;
140
+ // Use nullptr to get default source (microphone)
141
+ pa_simple* stream = pa_simple_new(nullptr, "Voxa", PA_STREAM_RECORD, nullptr,
142
+ "Mic Capture", &spec, nullptr, &attr, &error);
143
+
144
+ if (stream == nullptr) {
145
+ if (error_message != nullptr) {
146
+ *error_message = pa_strerror(error);
147
+ }
148
+ return false;
149
+ }
150
+
151
+ *out_stream = stream;
152
+ return true;
153
+ }
154
+
155
+ std::string GetCurrentDeviceName() {
156
+ // Try to get device name from PulseAudio
157
+ // For simplicity, we'll use a default name
158
+ // In a full implementation, you could use pa_context to query source info
159
+ return "Default Microphone";
160
+ }
161
+
162
+ bool IsBluetoothDevice() {
163
+ // Check device name for Bluetooth keywords
164
+ std::string device_name = GetCurrentDeviceName();
165
+ std::transform(device_name.begin(), device_name.end(), device_name.begin(), ::tolower);
166
+
167
+ const char* bluetooth_keywords[] = {
168
+ "bluetooth", "airpods", "beats", "jabra", "sony", "bose", "jbl", "bluez"
169
+ };
170
+
171
+ for (size_t i = 0; i < sizeof(bluetooth_keywords) / sizeof(bluetooth_keywords[0]); ++i) {
172
+ if (device_name.find(bluetooth_keywords[i]) != std::string::npos) {
173
+ g_debug("🔵 Detected Bluetooth device via name: %s", device_name.c_str());
174
+ return true;
175
+ }
176
+ }
177
+
178
+ return false;
179
+ }
180
+
181
+ void CleanupExistingCapture(MicCapturePlugin* plugin) {
182
+ g_mutex_lock(&plugin->lock);
183
+
184
+ if (plugin->is_capturing && plugin->capture_thread != nullptr) {
185
+ // Signal stop
186
+ g_atomic_int_set(&plugin->should_stop, 1);
187
+ GThread* thread = plugin->capture_thread;
188
+ g_mutex_unlock(&plugin->lock);
189
+
190
+ // Wait for thread to finish
191
+ if (thread != nullptr) {
192
+ g_thread_join(thread);
193
+ }
194
+
195
+ g_mutex_lock(&plugin->lock);
196
+ plugin->capture_thread = nullptr;
197
+ plugin->is_capturing = FALSE;
198
+ }
199
+
200
+ // Clear device name
201
+ if (plugin->current_device_name != nullptr) {
202
+ g_free(plugin->current_device_name);
203
+ plugin->current_device_name = nullptr;
204
+ }
205
+
206
+ g_mutex_unlock(&plugin->lock);
207
+
208
+ // Small delay for cleanup to complete
209
+ g_usleep(500000); // 0.5 seconds
210
+ }
211
+
212
+ bool OpenPulseStreamWithRetry(int sample_rate, int channels, int bits_per_sample,
213
+ size_t chunk_size, bool is_bluetooth,
214
+ pa_simple** out_stream, std::string* error_message) {
215
+ const int max_retries = is_bluetooth ? 5 : 3;
216
+ const double initial_wait = is_bluetooth ? 1.5 : 0.3;
217
+ const double retry_delays_bluetooth[] = {0.5, 1.0, 1.5, 2.0, 2.5};
218
+ const double retry_delays_normal[] = {0.3, 0.6, 1.0, 0.0, 0.0};
219
+ const double* retry_delays = is_bluetooth ? retry_delays_bluetooth : retry_delays_normal;
220
+
221
+ if (is_bluetooth) {
222
+ g_debug("🔵 Bluetooth device detected - using extended wait times");
223
+ }
224
+
225
+ // Initial wait for device to be ready
226
+ g_debug("⏳ Waiting %.1fs for device to be ready...", initial_wait);
227
+ g_usleep(static_cast<guint64>(initial_wait * 1000000));
228
+
229
+ for (int attempt = 1; attempt <= max_retries; ++attempt) {
230
+ if (OpenPulseStream(sample_rate, channels, bits_per_sample, chunk_size,
231
+ out_stream, error_message)) {
232
+ g_debug("✅ PulseAudio stream opened successfully on attempt %d", attempt);
233
+ return true;
234
+ }
235
+
236
+ if (attempt < max_retries) {
237
+ double wait_time = retry_delays[attempt - 1];
238
+ if (wait_time > 0.0) {
239
+ g_debug("⚠️ Attempt %d/%d failed: %s", attempt, max_retries,
240
+ error_message != nullptr ? error_message->c_str() : "unknown error");
241
+ g_debug(" ⏳ Waiting %.1fs before retry...", wait_time);
242
+ g_usleep(static_cast<guint64>(wait_time * 1000000));
243
+ }
244
+ }
245
+ }
246
+
247
+ g_warning("❌ Failed to open PulseAudio stream after %d attempts", max_retries);
248
+ return false;
249
+ }
250
+
251
+ void ApplyGainBoostAndConvertToMono(const int16_t* input, int16_t* output,
252
+ size_t frame_count, int input_channels,
253
+ float gain_boost) {
254
+ const float max_value = 32767.0f;
255
+ const float min_value = -32768.0f;
256
+
257
+ if (input_channels == 1) {
258
+ // Mono: just apply gain boost
259
+ for (size_t i = 0; i < frame_count; ++i) {
260
+ float sample = static_cast<float>(input[i]) * gain_boost;
261
+ sample = std::max(min_value, std::min(max_value, sample));
262
+ output[i] = static_cast<int16_t>(sample);
263
+ }
264
+ } else {
265
+ // Stereo: convert to mono and apply gain boost
266
+ for (size_t i = 0; i < frame_count; ++i) {
267
+ float left = static_cast<float>(input[i * 2]);
268
+ float right = static_cast<float>(input[i * 2 + 1]);
269
+ float mono = (left + right) / 2.0f * gain_boost;
270
+ mono = std::max(min_value, std::min(max_value, mono));
271
+ output[i] = static_cast<int16_t>(mono);
272
+ }
273
+ }
274
+ }
275
+
276
+ double CalculateDecibel(const int16_t* samples, size_t sample_count) {
277
+ if (sample_count == 0) {
278
+ return -120.0;
279
+ }
280
+
281
+ // Calculate RMS (Root Mean Square)
282
+ double sum_of_squares = 0.0;
283
+ for (size_t i = 0; i < sample_count; ++i) {
284
+ double value = static_cast<double>(samples[i]);
285
+ sum_of_squares += value * value;
286
+ }
287
+ double mean_square = sum_of_squares / static_cast<double>(sample_count);
288
+ double rms = sqrt(mean_square);
289
+
290
+ // Calculate decibel: dB = 20 * log10(RMS / max_value)
291
+ // For Int16, max_value is 32767.0
292
+ const double max_value = 32767.0;
293
+ if (rms <= 0.0) {
294
+ return -120.0; // Avoid log(0)
295
+ }
296
+
297
+ double decibel = 20.0 * log10(rms / max_value);
298
+
299
+ // Clamp to reasonable range (-120 dB to 0 dB)
300
+ return std::max(-120.0, std::min(0.0, decibel));
301
+ }
302
+
303
+ gboolean EmitAudioOnMainThread(gpointer user_data) {
304
+ std::unique_ptr<AudioChunkPayload> payload(
305
+ static_cast<AudioChunkPayload*>(user_data));
306
+ MicCapturePlugin* plugin = payload->plugin;
307
+
308
+ gsize length = 0;
309
+ const guint8* data =
310
+ static_cast<const guint8*>(g_bytes_get_data(payload->bytes, &length));
311
+
312
+ g_mutex_lock(&plugin->lock);
313
+ const gboolean can_emit =
314
+ plugin->event_channel != nullptr && plugin->has_listener;
315
+ const gboolean can_emit_decibel =
316
+ plugin->decibel_event_channel != nullptr && plugin->has_decibel_listener;
317
+ g_mutex_unlock(&plugin->lock);
318
+
319
+ if (can_emit && length > 0) {
320
+ g_autoptr(FlValue) value = fl_value_new_uint8_list(data, length);
321
+ g_autoptr(GError) error = nullptr;
322
+
323
+ if (!fl_event_channel_send(plugin->event_channel, value, nullptr, &error)) {
324
+ g_warning("Failed to send audio chunk: %s",
325
+ error != nullptr ? error->message : "unknown error");
326
+ }
327
+ }
328
+
329
+ // Send decibel data
330
+ if (can_emit_decibel) {
331
+ g_autoptr(FlValue) decibel_map = fl_value_new_map();
332
+ fl_value_set_string_take(decibel_map, "decibel", fl_value_new_float(payload->decibel));
333
+ fl_value_set_string_take(decibel_map, "timestamp", fl_value_new_float(g_get_real_time() / 1000000.0));
334
+
335
+ g_autoptr(GError) error = nullptr;
336
+ if (!fl_event_channel_send(plugin->decibel_event_channel, decibel_map, nullptr, &error)) {
337
+ g_warning("Failed to send decibel data: %s",
338
+ error != nullptr ? error->message : "unknown error");
339
+ }
340
+ }
341
+
342
+ g_bytes_unref(payload->bytes);
343
+ g_object_unref(plugin);
344
+
345
+ return G_SOURCE_REMOVE;
346
+ }
347
+
348
+ gpointer CaptureThread(gpointer user_data) {
349
+ std::unique_ptr<CaptureThreadContext> context(
350
+ static_cast<CaptureThreadContext*>(user_data));
351
+ MicCapturePlugin* plugin = context->plugin;
352
+
353
+ // Read raw audio from PulseAudio
354
+ const size_t raw_chunk_size = CalculateChunkSize(
355
+ context->sample_rate, context->channels, context->bits_per_sample);
356
+ std::vector<uint8_t> raw_buffer(raw_chunk_size);
357
+
358
+ // Output buffer for processed audio (mono)
359
+ const size_t output_frame_count = kBufferSizeFrames;
360
+ std::vector<int16_t> output_buffer(output_frame_count);
361
+
362
+ while (!g_atomic_int_get(&plugin->should_stop)) {
363
+ int error = 0;
364
+ if (pa_simple_read(context->stream, raw_buffer.data(), raw_buffer.size(),
365
+ &error) < 0) {
366
+ g_warning("PulseAudio read error: %s", pa_strerror(error));
367
+ break;
368
+ }
369
+
370
+ if (g_atomic_int_get(&plugin->should_stop)) {
371
+ break;
372
+ }
373
+
374
+ // Apply input volume
375
+ if (context->input_volume < 1.0f) {
376
+ int16_t* samples = reinterpret_cast<int16_t*>(raw_buffer.data());
377
+ const size_t sample_count = raw_buffer.size() / sizeof(int16_t);
378
+ for (size_t i = 0; i < sample_count; ++i) {
379
+ samples[i] = static_cast<int16_t>(
380
+ static_cast<float>(samples[i]) * context->input_volume);
381
+ }
382
+ }
383
+
384
+ // Convert to mono and apply gain boost
385
+ const int16_t* input_samples =
386
+ reinterpret_cast<const int16_t*>(raw_buffer.data());
387
+ const size_t input_frame_count =
388
+ raw_buffer.size() / (sizeof(int16_t) * context->channels);
389
+ const size_t frames_to_process =
390
+ std::min(input_frame_count, output_frame_count);
391
+
392
+ ApplyGainBoostAndConvertToMono(input_samples, output_buffer.data(),
393
+ frames_to_process, context->channels,
394
+ context->gain_boost);
395
+
396
+ // Create output bytes
397
+ const size_t output_bytes = frames_to_process * sizeof(int16_t);
398
+
399
+ // Calculate decibel from output buffer
400
+ double decibel = CalculateDecibel(output_buffer.data(), frames_to_process);
401
+
402
+ GBytes* bytes = g_bytes_new(output_buffer.data(), output_bytes);
403
+ auto* payload = new AudioChunkPayload(plugin, bytes, decibel);
404
+ g_object_ref(plugin);
405
+
406
+ g_main_context_invoke_full(plugin->main_context, G_PRIORITY_DEFAULT,
407
+ EmitAudioOnMainThread, payload, nullptr);
408
+ }
409
+
410
+ pa_simple_free(context->stream);
411
+
412
+ g_mutex_lock(&plugin->lock);
413
+ plugin->is_capturing = FALSE;
414
+ plugin->capture_thread = nullptr;
415
+ g_mutex_unlock(&plugin->lock);
416
+
417
+ // Send status update
418
+ g_mutex_lock(&plugin->lock);
419
+ const gboolean has_status_listener = plugin->has_status_listener;
420
+ g_mutex_unlock(&plugin->lock);
421
+
422
+ if (has_status_listener && plugin->status_event_channel != nullptr) {
423
+ g_autoptr(FlValue) status_map = fl_value_new_map();
424
+ fl_value_set_string_take(status_map, "isActive", fl_value_new_bool(FALSE));
425
+ fl_value_set_string_take(status_map, "timestamp", fl_value_new_float(g_get_real_time() / 1000000.0));
426
+
427
+ g_autoptr(GError) error = nullptr;
428
+ fl_event_channel_send(plugin->status_event_channel, status_map, nullptr, &error);
429
+ }
430
+
431
+ g_object_unref(plugin);
432
+ return nullptr;
433
+ }
434
+
435
+ static FlMethodErrorResponse* OnListenHandler(FlEventChannel* channel,
436
+ FlValue* arguments,
437
+ gpointer user_data) {
438
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
439
+ (void)channel;
440
+ (void)arguments;
441
+ g_mutex_lock(&plugin->lock);
442
+ plugin->has_listener = TRUE;
443
+ g_mutex_unlock(&plugin->lock);
444
+ return nullptr;
445
+ }
446
+
447
+ static FlMethodErrorResponse* OnCancelHandler(FlEventChannel* channel,
448
+ FlValue* arguments,
449
+ gpointer user_data) {
450
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
451
+ (void)channel;
452
+ (void)arguments;
453
+ g_mutex_lock(&plugin->lock);
454
+ plugin->has_listener = FALSE;
455
+ g_mutex_unlock(&plugin->lock);
456
+ return nullptr;
457
+ }
458
+
459
+ static FlMethodErrorResponse* OnStatusListenHandler(FlEventChannel* channel,
460
+ FlValue* arguments,
461
+ gpointer user_data) {
462
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
463
+ (void)channel;
464
+ (void)arguments;
465
+ g_mutex_lock(&plugin->lock);
466
+ plugin->has_status_listener = TRUE;
467
+ const gboolean is_active = plugin->is_capturing;
468
+ g_mutex_unlock(&plugin->lock);
469
+
470
+ // Send current status immediately
471
+ g_autoptr(FlValue) status_map = fl_value_new_map();
472
+ fl_value_set_string_take(status_map, "isActive", fl_value_new_bool(is_active));
473
+ fl_value_set_string_take(status_map, "timestamp", fl_value_new_float(g_get_real_time() / 1000000.0));
474
+
475
+ g_autoptr(GError) error = nullptr;
476
+ fl_event_channel_send(plugin->status_event_channel, status_map, nullptr, &error);
477
+
478
+ return nullptr;
479
+ }
480
+
481
+ static FlMethodErrorResponse* OnStatusCancelHandler(FlEventChannel* channel,
482
+ FlValue* arguments,
483
+ gpointer user_data) {
484
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
485
+ (void)channel;
486
+ (void)arguments;
487
+ g_mutex_lock(&plugin->lock);
488
+ plugin->has_status_listener = FALSE;
489
+ g_mutex_unlock(&plugin->lock);
490
+ return nullptr;
491
+ }
492
+
493
+ static FlMethodErrorResponse* OnDecibelListenHandler(FlEventChannel* channel,
494
+ FlValue* arguments,
495
+ gpointer user_data) {
496
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
497
+ (void)channel;
498
+ (void)arguments;
499
+ g_mutex_lock(&plugin->lock);
500
+ plugin->has_decibel_listener = TRUE;
501
+ g_mutex_unlock(&plugin->lock);
502
+ return nullptr;
503
+ }
504
+
505
+ static FlMethodErrorResponse* OnDecibelCancelHandler(FlEventChannel* channel,
506
+ FlValue* arguments,
507
+ gpointer user_data) {
508
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
509
+ (void)channel;
510
+ (void)arguments;
511
+ g_mutex_lock(&plugin->lock);
512
+ plugin->has_decibel_listener = FALSE;
513
+ g_mutex_unlock(&plugin->lock);
514
+ return nullptr;
515
+ }
516
+
517
+ bool StartCapture(MicCapturePlugin* plugin, FlValue* args) {
518
+ // Always cleanup any existing capture first to ensure clean start
519
+ // This is important even if isCapturing is false (state might be out of sync)
520
+ CleanupExistingCapture(plugin);
521
+
522
+ int sample_rate = kDefaultSampleRate;
523
+ int channels = kDefaultChannels;
524
+ int bits_per_sample = kDefaultBitsPerSample;
525
+ float gain_boost = kDefaultGainBoost;
526
+ float input_volume = kDefaultInputVolume;
527
+
528
+ if (args != nullptr && fl_value_get_type(args) == FL_VALUE_TYPE_MAP) {
529
+ FlValue* value = nullptr;
530
+
531
+ value = fl_value_lookup_string(args, "sampleRate");
532
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_INT) {
533
+ sample_rate = fl_value_get_int(value);
534
+ }
535
+
536
+ value = fl_value_lookup_string(args, "channels");
537
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_INT) {
538
+ channels = fl_value_get_int(value);
539
+ }
540
+
541
+ value = fl_value_lookup_string(args, "bitDepth");
542
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_INT) {
543
+ bits_per_sample = fl_value_get_int(value);
544
+ }
545
+
546
+ value = fl_value_lookup_string(args, "gainBoost");
547
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_FLOAT) {
548
+ gain_boost = fl_value_get_float(value);
549
+ gain_boost = std::max(0.1f, std::min(10.0f, gain_boost));
550
+ }
551
+
552
+ value = fl_value_lookup_string(args, "inputVolume");
553
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_FLOAT) {
554
+ input_volume = fl_value_get_float(value);
555
+ input_volume = std::max(0.0f, std::min(1.0f, input_volume));
556
+ }
557
+ }
558
+
559
+ // Clamp values
560
+ sample_rate = std::max(sample_rate, 8000);
561
+ channels = std::max(1, std::min(channels, 2));
562
+ bits_per_sample = 16; // Force 16-bit
563
+ gain_boost = std::max(0.1f, std::min(10.0f, gain_boost));
564
+ input_volume = std::max(0.0f, std::min(1.0f, input_volume));
565
+
566
+ size_t chunk_size =
567
+ CalculateChunkSize(sample_rate, channels, bits_per_sample);
568
+
569
+ // Detect if device is Bluetooth and adjust wait times accordingly
570
+ bool is_bluetooth = IsBluetoothDevice();
571
+
572
+ g_debug("🎤 Starting capture with config:");
573
+ g_debug(" Sample Rate: %d Hz", sample_rate);
574
+ g_debug(" Channels: %d", channels);
575
+ g_debug(" Bits Per Sample: %d", bits_per_sample);
576
+ g_debug(" Gain Boost: %.2fx", gain_boost);
577
+ g_debug(" Input Volume: %.2f", input_volume);
578
+ g_debug(" Is Bluetooth: %s", is_bluetooth ? "yes" : "no");
579
+
580
+ pa_simple* stream = nullptr;
581
+ std::string error_message;
582
+
583
+ // Open stream with retry mechanism
584
+ if (!OpenPulseStreamWithRetry(sample_rate, channels, bits_per_sample, chunk_size,
585
+ is_bluetooth, &stream, &error_message)) {
586
+ g_warning("Failed to open PulseAudio stream: %s", error_message.c_str());
587
+ return false;
588
+ }
589
+
590
+ // Get device name
591
+ std::string device_name = GetCurrentDeviceName();
592
+
593
+ g_mutex_lock(&plugin->lock);
594
+ if (plugin->is_capturing) {
595
+ g_mutex_unlock(&plugin->lock);
596
+ pa_simple_free(stream);
597
+ g_warning("⚠️ State mismatch: isCapturing=true after cleanup, aborting");
598
+ return false;
599
+ }
600
+
601
+ g_atomic_int_set(&plugin->should_stop, 0);
602
+ plugin->is_capturing = TRUE;
603
+
604
+ // Store device name
605
+ if (plugin->current_device_name != nullptr) {
606
+ g_free(plugin->current_device_name);
607
+ }
608
+ plugin->current_device_name = g_strdup(device_name.c_str());
609
+
610
+ auto* context = new CaptureThreadContext{
611
+ plugin, stream, chunk_size, sample_rate, channels,
612
+ bits_per_sample, gain_boost, input_volume};
613
+
614
+ g_object_ref(plugin);
615
+ plugin->capture_thread =
616
+ g_thread_new("voxa-mic-capture", CaptureThread, context);
617
+ g_mutex_unlock(&plugin->lock);
618
+
619
+ if (plugin->capture_thread == nullptr) {
620
+ g_warning("Failed to create capture thread");
621
+ g_mutex_lock(&plugin->lock);
622
+ plugin->is_capturing = FALSE;
623
+ if (plugin->current_device_name != nullptr) {
624
+ g_free(plugin->current_device_name);
625
+ plugin->current_device_name = nullptr;
626
+ }
627
+ g_mutex_unlock(&plugin->lock);
628
+ pa_simple_free(stream);
629
+ g_object_unref(plugin);
630
+ delete context;
631
+ return false;
632
+ }
633
+
634
+ // Wait a bit to ensure thread has started
635
+ g_usleep(200000); // 0.2 seconds
636
+
637
+ // Send status update with device name
638
+ g_mutex_lock(&plugin->lock);
639
+ const gboolean has_status_listener = plugin->has_status_listener;
640
+ const gchar* device_name_cstr = plugin->current_device_name;
641
+ g_mutex_unlock(&plugin->lock);
642
+
643
+ if (has_status_listener && plugin->status_event_channel != nullptr) {
644
+ g_autoptr(FlValue) status_map = fl_value_new_map();
645
+ fl_value_set_string_take(status_map, "isActive", fl_value_new_bool(TRUE));
646
+ fl_value_set_string_take(status_map, "timestamp", fl_value_new_float(g_get_real_time() / 1000000.0));
647
+ if (device_name_cstr != nullptr) {
648
+ fl_value_set_string_take(status_map, "deviceName", fl_value_new_string(device_name_cstr));
649
+ }
650
+
651
+ g_autoptr(GError) error = nullptr;
652
+ fl_event_channel_send(plugin->status_event_channel, status_map, nullptr, &error);
653
+ }
654
+
655
+ g_debug("✅ Microphone capture started successfully!");
656
+ if (device_name_cstr != nullptr) {
657
+ g_debug(" Device: %s", device_name_cstr);
658
+ }
659
+
660
+ return true;
661
+ }
662
+
663
+ bool StopCapture(MicCapturePlugin* plugin) {
664
+ g_mutex_lock(&plugin->lock);
665
+ if (!plugin->is_capturing) {
666
+ g_mutex_unlock(&plugin->lock);
667
+ return false;
668
+ }
669
+ g_atomic_int_set(&plugin->should_stop, 1);
670
+ GThread* thread = plugin->capture_thread;
671
+ g_mutex_unlock(&plugin->lock);
672
+
673
+ if (thread != nullptr) {
674
+ g_thread_join(thread);
675
+ }
676
+
677
+ g_mutex_lock(&plugin->lock);
678
+ plugin->capture_thread = nullptr;
679
+ plugin->is_capturing = FALSE;
680
+ const gboolean has_status_listener = plugin->has_status_listener;
681
+ g_mutex_unlock(&plugin->lock);
682
+
683
+ // Wait a bit to ensure thread has fully stopped
684
+ g_usleep(100000); // 0.1 seconds
685
+
686
+ // Send status update
687
+ g_mutex_lock(&plugin->lock);
688
+ if (plugin->current_device_name != nullptr) {
689
+ g_free(plugin->current_device_name);
690
+ plugin->current_device_name = nullptr;
691
+ }
692
+ g_mutex_unlock(&plugin->lock);
693
+
694
+ if (has_status_listener && plugin->status_event_channel != nullptr) {
695
+ g_autoptr(FlValue) status_map = fl_value_new_map();
696
+ fl_value_set_string_take(status_map, "isActive", fl_value_new_bool(FALSE));
697
+ fl_value_set_string_take(status_map, "timestamp", fl_value_new_float(g_get_real_time() / 1000000.0));
698
+
699
+ g_autoptr(GError) error = nullptr;
700
+ fl_event_channel_send(plugin->status_event_channel, status_map, nullptr, &error);
701
+ }
702
+
703
+ return true;
704
+ }
705
+
706
+ bool HasInputDevice() {
707
+ return CheckMicSupport();
708
+ }
709
+
710
+ FlValue* GetAvailableInputDevices() {
711
+ // For now, return a simple list with default device
712
+ // In a full implementation, you could use pa_context to query all sources
713
+ g_autoptr(FlValue) device_list = fl_value_new_list();
714
+
715
+ // Get default device info
716
+ std::string device_name = GetCurrentDeviceName();
717
+ bool is_bluetooth = IsBluetoothDevice();
718
+
719
+ g_autoptr(FlValue) device_map = fl_value_new_map();
720
+ fl_value_set_string_take(device_map, "id", fl_value_new_string("default"));
721
+ fl_value_set_string_take(device_map, "name", fl_value_new_string(device_name.c_str()));
722
+ fl_value_set_string_take(device_map, "type", fl_value_new_string(is_bluetooth ? "bluetooth" : "external"));
723
+ fl_value_set_string_take(device_map, "channelCount", fl_value_new_int(1));
724
+ fl_value_set_string_take(device_map, "isDefault", fl_value_new_bool(TRUE));
725
+
726
+ fl_value_append_take(device_list, device_map);
727
+
728
+ return g_steal_pointer(&device_list);
729
+ }
730
+
731
+ void HandleMethodCall(MicCapturePlugin* plugin, FlMethodCall* method_call) {
732
+ const gchar* method = fl_method_call_get_name(method_call);
733
+ g_autoptr(FlMethodResponse) response = nullptr;
734
+
735
+ if (strcmp(method, "requestPermissions") == 0) {
736
+ // On Linux, permissions are typically handled by the system
737
+ // PulseAudio will handle access automatically
738
+ g_autoptr(FlValue) result = fl_value_new_bool(TRUE);
739
+ response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
740
+ } else if (strcmp(method, "hasInputDevice") == 0) {
741
+ const bool has_device = HasInputDevice();
742
+ g_autoptr(FlValue) result = fl_value_new_bool(has_device);
743
+ response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
744
+ } else if (strcmp(method, "getAvailableInputDevices") == 0) {
745
+ g_autoptr(FlValue) devices = GetAvailableInputDevices();
746
+ response = FL_METHOD_RESPONSE(fl_method_success_response_new(devices));
747
+ } else if (strcmp(method, "startCapture") == 0) {
748
+ FlValue* args = fl_method_call_get_args(method_call);
749
+ const bool started = StartCapture(plugin, args);
750
+ g_autoptr(FlValue) result = fl_value_new_bool(started);
751
+ response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
752
+ } else if (strcmp(method, "stopCapture") == 0) {
753
+ const bool stopped = StopCapture(plugin);
754
+ g_autoptr(FlValue) result = fl_value_new_bool(stopped);
755
+ response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
756
+ } else {
757
+ response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
758
+ }
759
+
760
+ g_autoptr(GError) error = nullptr;
761
+ if (!fl_method_call_respond(method_call, response, &error)) {
762
+ g_warning("Failed to send method call response: %s", error->message);
763
+ }
764
+ }
765
+
766
+ static void MethodCallHandler(FlMethodChannel* channel,
767
+ FlMethodCall* method_call,
768
+ gpointer user_data) {
769
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(user_data);
770
+ HandleMethodCall(plugin, method_call);
771
+ }
772
+
773
+ } // namespace
774
+
775
+ static void mic_capture_plugin_dispose(GObject* object) {
776
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(object);
777
+
778
+ StopCapture(plugin);
779
+
780
+ if (plugin->method_channel != nullptr) {
781
+ g_clear_object(&plugin->method_channel);
782
+ }
783
+
784
+ if (plugin->event_channel != nullptr) {
785
+ g_clear_object(&plugin->event_channel);
786
+ }
787
+
788
+ if (plugin->status_event_channel != nullptr) {
789
+ g_clear_object(&plugin->status_event_channel);
790
+ }
791
+
792
+ if (plugin->decibel_event_channel != nullptr) {
793
+ g_clear_object(&plugin->decibel_event_channel);
794
+ }
795
+
796
+ if (plugin->current_device_name != nullptr) {
797
+ g_free(plugin->current_device_name);
798
+ plugin->current_device_name = nullptr;
799
+ }
800
+
801
+ if (plugin->main_context != nullptr) {
802
+ g_main_context_unref(plugin->main_context);
803
+ plugin->main_context = nullptr;
804
+ }
805
+
806
+ g_mutex_clear(&plugin->lock);
807
+
808
+ G_OBJECT_CLASS(mic_capture_plugin_parent_class)->dispose(object);
809
+ }
810
+
811
+ static void mic_capture_plugin_class_init(MicCapturePluginClass* klass) {
812
+ GObjectClass* object_class = G_OBJECT_CLASS(klass);
813
+ object_class->dispose = mic_capture_plugin_dispose;
814
+ }
815
+
816
+ static void mic_capture_plugin_init(MicCapturePlugin* plugin) {
817
+ g_mutex_init(&plugin->lock);
818
+ plugin->main_context = g_main_context_ref_thread_default();
819
+ plugin->is_capturing = FALSE;
820
+ plugin->has_listener = FALSE;
821
+ plugin->has_status_listener = FALSE;
822
+ plugin->has_decibel_listener = FALSE;
823
+ plugin->method_channel = nullptr;
824
+ plugin->event_channel = nullptr;
825
+ plugin->status_event_channel = nullptr;
826
+ plugin->decibel_event_channel = nullptr;
827
+ plugin->capture_thread = nullptr;
828
+ plugin->current_device_name = nullptr;
829
+ g_atomic_int_set(&plugin->should_stop, 0);
830
+ }
831
+
832
+ void mic_capture_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
833
+ FlBinaryMessenger* messenger = fl_plugin_registrar_get_messenger(registrar);
834
+ mic_capture_plugin_register_with_messenger(messenger);
835
+ }
836
+
837
+ void mic_capture_plugin_register_with_messenger(FlBinaryMessenger* messenger) {
838
+ MicCapturePlugin* plugin = MIC_CAPTURE_PLUGIN(
839
+ g_object_new(mic_capture_plugin_get_type(), nullptr));
840
+
841
+ g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
842
+
843
+ plugin->method_channel = fl_method_channel_new(
844
+ messenger, kMethodChannelName, FL_METHOD_CODEC(codec));
845
+ fl_method_channel_set_method_call_handler(plugin->method_channel,
846
+ MethodCallHandler, g_object_ref(plugin),
847
+ g_object_unref);
848
+
849
+ plugin->event_channel = fl_event_channel_new(messenger, kEventChannelName,
850
+ FL_METHOD_CODEC(codec));
851
+
852
+ fl_event_channel_set_stream_handlers(plugin->event_channel, OnListenHandler,
853
+ OnCancelHandler, g_object_ref(plugin),
854
+ g_object_unref);
855
+
856
+ // Register status event channel
857
+ plugin->status_event_channel = fl_event_channel_new(
858
+ messenger, kStatusEventChannelName, FL_METHOD_CODEC(codec));
859
+ fl_event_channel_set_stream_handlers(
860
+ plugin->status_event_channel,
861
+ OnStatusListenHandler,
862
+ OnStatusCancelHandler,
863
+ g_object_ref(plugin),
864
+ g_object_unref);
865
+
866
+ // Register decibel event channel
867
+ plugin->decibel_event_channel = fl_event_channel_new(
868
+ messenger, kDecibelEventChannelName, FL_METHOD_CODEC(codec));
869
+ fl_event_channel_set_stream_handlers(
870
+ plugin->decibel_event_channel,
871
+ OnDecibelListenHandler,
872
+ OnDecibelCancelHandler,
873
+ g_object_ref(plugin),
874
+ g_object_unref);
875
+
876
+ g_object_unref(plugin);
877
+ }
878
+