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.
- package/.env.example +39 -0
- package/README.md +2 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +13 -5
- package/docs/integrations.md +4 -1
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +23057 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_integrations.dart +931 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3473 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +831 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/manager.js +231 -7
- package/package.json +3 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65262 -64422
- package/server/routes/integrations.js +86 -0
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +172 -0
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/home_assistant/provider.js +306 -26
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/trello/provider.js +744 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- 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
|
+
|