Compare commits

..
Author SHA1 Message Date
Edison Jwa e0060f3c19 feat(voice): unified mobile voice bar with gesture-isolated PTT row
Replace separate VoiceStatusChip + VoicePttButton with a single
CompactVoiceBar widget that combines both into a two-row layout:

- Control row (tap): status text, mute, deafen, settings chevron
- PTT row (hold): full-width hold-to-talk, shown only in PTT mode

Gesture isolation prevents mis-touch between rows: the control row
uses tap-only InkWell/IconButton while the PTT row uses a raw
Listener for pointer-down/up events.

Key changes:
- Add CompactVoiceBar widget with state-colored container (normal,
  muted, talk-power-blocked)
- Remove mute/deafen IconButtons from AppBar headerActions
- Restructure voice details sheet into primary section + collapsible
  ExpansionTiles (audio processing, PTT capability, debug)
- Optimistic state updates for mute/deafen to eliminate tap delay
- Instant PTT visual feedback (no AnimatedContainer fade)
- Constant geometry across all states (no layout shift on toggle)
2026-06-04 22:46:19 +09:00
Edison Jwa 8cb5a7a258 feat(macos): add macOS permissions service for Input Monitoring, Local Network, and Notifications
Add MacOSPermissionsService (Dart) + native MethodChannel handler (Swift)
for macOS-specific permissions not covered by permission_handler:

- Input Monitoring (CGPreflightListenEventAccess /
  CGRequestListenEventAccess) for global PTT via Event Tap
- Local Network Privacy prompt (NWBrowser for _ts3._tcp, macOS 15+)
- Notifications (UNUserNotificationCenter authorization)

Trace: SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091

Changes:
- Info.plist: add NSBonjourServices array with _ts3._tcp
- macos_permissions_service.dart: Dart service with MethodChannel,
  ValueNotifier states, PTT capability derivation (L0Focused /
  L1MacOSEventTap), non-macOS short-circuit
- MainFlutterWindow.swift: native handler registered as FlutterPlugin,
  Input Monitoring check/request/polling, NWBrowser trigger with
  denial detection, UNUserNotificationCenter request
- main.dart: wire service into bootstrap lifecycle, listen for PTT
  capability changes from Input Monitoring state
- macos_permissions_service_test.dart: 17 unit tests covering inbound
  state changes, outbound calls, lifecycle, error handling, platform
  behavior (179/179 full suite pass)
2026-06-04 08:21:07 +09:00
Edison Jwa 484dad1072 fix(audio): eliminate Android output stutter via Oboe config + lock-free callback
Phase 1 — Oboe configuration:
- Change output stream from Usage::VoiceCommunication to Usage::Game with
  ContentType::Sonification to avoid forcing the Legacy (OpenSL ES) data
  path on most devices (Oboe issue #2075)
- Switch output format from i16 Mono to f32 Stereo, matching Qint's proven
  configuration and eliminating per-callback downmix conversion
- Set buffer size to 2x burst after stream open, reducing default buffer
  from 8-20x burst to 2x burst for lower latency
- Remove scratch Mutex<Vec<f32>>; callback writes directly to Oboe buffer

Phase 2 — Lock-free output callback:
- Add audio_event_queue.rs: lock-free SPSC bridge using crossbeam ArrayQueue
  with separate packet (lossy) and control (reliable) channels
- OutputCallback now owns AudioHandler directly (no Arc<Mutex<>> on Android)
- Inbound forwarder pushes packets via AudioEventProducer (no mutex)
- set_client_volume pushes control commands via event queue on Android
- iOS/desktop Arc<Mutex<AudioHandler>> path unchanged
2026-06-04 00:51:25 +09:00
46 changed files with 1034 additions and 1284 deletions
-6
View File
@@ -117,12 +117,6 @@ opencode.json
# iOS framework build artifacts produced by chanora_bridge.podspec # iOS framework build artifacts produced by chanora_bridge.podspec
/apps/chanora_flutter/ios/Frameworks/ /apps/chanora_flutter/ios/Frameworks/
# macOS framework build artifacts produced by chanora_bridge.podspec
# (prepare_command + script_phase rm -rf and regenerate this tree on
# every pod install AND every Xcode build, so tracking it in git is
# pure waste — the committed binary was ~40 MB per commit).
/apps/chanora_flutter/macos/Frameworks/
.opencode/ .opencode/
AGENTS.md AGENTS.md
Screenshot 2026-05-17 at 22.23.07.png Screenshot 2026-05-17 at 22.23.07.png
Generated
+1 -1
View File
@@ -419,12 +419,12 @@ name = "chanora_audio"
version = "0.2.0-beta.1" version = "0.2.0-beta.1"
dependencies = [ dependencies = [
"audiopus", "audiopus",
"bytemuck",
"chanora_protocol", "chanora_protocol",
"coreaudio-rs", "coreaudio-rs",
"cpal", "cpal",
"criterion", "criterion",
"crossbeam", "crossbeam",
"crossbeam-utils",
"dhat", "dhat",
"dispatch2", "dispatch2",
"futures-util", "futures-util",
@@ -63,10 +63,8 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlin { kotlinOptions {
compilerOptions { jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
} }
defaultConfig { defaultConfig {
@@ -1,6 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -19,8 +19,8 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.13.1" apply false id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false
} }
include(":app") include(":app")
+37
View File
@@ -1,32 +1,69 @@
PODS: PODS:
- audio_session (0.0.1):
- Flutter
- chanora_bridge (1.0.0) - chanora_bridge (1.0.0)
- connectivity_plus (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_foreground_task (0.0.1): - flutter_foreground_task (0.0.1):
- Flutter - Flutter
- haptic_kit (1.0.0): - haptic_kit (1.0.0):
- Flutter - Flutter
- package_info_plus (0.4.5):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- chanora_bridge (from `.`) - chanora_bridge (from `.`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- haptic_kit (from `.symlinks/plugins/haptic_kit/ios`) - haptic_kit (from `.symlinks/plugins/haptic_kit/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
chanora_bridge: chanora_bridge:
:path: "." :path: "."
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_foreground_task: flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios" :path: ".symlinks/plugins/flutter_foreground_task/ios"
haptic_kit: haptic_kit:
:path: ".symlinks/plugins/haptic_kit/ios" :path: ".symlinks/plugins/haptic_kit/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758 haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2 PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2
@@ -20,7 +20,6 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; }; FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -73,7 +72,6 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -81,7 +79,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */, 8C5000042DD0000000000001 /* SileroCoreML in Frameworks */,
1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */, 1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */,
); );
@@ -131,7 +128,6 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -220,7 +216,6 @@
); );
name = Runner; name = Runner;
packageProductDependencies = ( packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
8C5000032DD0000000000001 /* SileroCoreML */, 8C5000032DD0000000000001 /* SileroCoreML */,
); );
productName = Runner; productName = Runner;
@@ -257,7 +252,6 @@
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */, 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
); );
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
@@ -766,10 +760,6 @@
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = ../../../silero-coreml; relativePath = ../../../silero-coreml;
}; };
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -778,10 +768,6 @@
package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */; package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
productName = SileroCoreML; productName = SileroCoreML;
}; };
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
@@ -5,24 +5,6 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
+1 -2
View File
@@ -298,6 +298,5 @@
"clientVolumeMuteAction": "Mute user", "clientVolumeMuteAction": "Mute user",
"clientVolumeUnmuteAction": "Unmute user", "clientVolumeUnmuteAction": "Unmute user",
"clientVolumeResetAction": "Reset to default", "clientVolumeResetAction": "Reset to default",
"permissionDenied": "Permission Denied", "permissionDenied": "Permission Denied"
"voiceTalkPowerBlocked": "Insufficient talk power to speak in this channel"
} }
+1 -2
View File
@@ -241,6 +241,5 @@
"clientVolumeMuteAction": "静音该用户", "clientVolumeMuteAction": "静音该用户",
"clientVolumeUnmuteAction": "取消静音", "clientVolumeUnmuteAction": "取消静音",
"clientVolumeResetAction": "恢复默认", "clientVolumeResetAction": "恢复默认",
"permissionDenied": "权限被拒绝", "permissionDenied": "权限被拒绝"
"voiceTalkPowerBlocked": "发言权限不足,无法在此频道发言"
} }
@@ -1246,12 +1246,6 @@ abstract class AppL10n {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Permission Denied'** /// **'Permission Denied'**
String get permissionDenied; String get permissionDenied;
/// No description provided for @voiceTalkPowerBlocked.
///
/// In en, this message translates to:
/// **'Insufficient talk power to speak in this channel'**
String get voiceTalkPowerBlocked;
} }
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> { class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
@@ -653,8 +653,4 @@ class AppL10nEn extends AppL10n {
@override @override
String get permissionDenied => 'Permission Denied'; String get permissionDenied => 'Permission Denied';
@override
String get voiceTalkPowerBlocked =>
'Insufficient talk power to speak in this channel';
} }
@@ -641,7 +641,4 @@ class AppL10nZh extends AppL10n {
@override @override
String get permissionDenied => '权限被拒绝'; String get permissionDenied => '权限被拒绝';
@override
String get voiceTalkPowerBlocked => '发言权限不足,无法在此频道发言';
} }
+46 -105
View File
@@ -312,7 +312,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
rust.BridgeSnapshot? _snapshot; rust.BridgeSnapshot? _snapshot;
final List<String> _uiDiagnostics = []; final List<String> _uiDiagnostics = [];
rust.BridgeAudioStats? _audioStats; rust.BridgeAudioStats? _audioStats;
double? _inputLevel;
Timer? _statsTimer; Timer? _statsTimer;
bool _snapshotRefreshInFlight = false; bool _snapshotRefreshInFlight = false;
bool _snapshotRefreshQueued = false; bool _snapshotRefreshQueued = false;
@@ -320,7 +319,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
bool _snapshotRefreshQueuedReportErrors = false; bool _snapshotRefreshQueuedReportErrors = false;
int _connectionEpoch = 0; int _connectionEpoch = 0;
StreamSubscription<rust.BridgeEvent>? _eventsSub; StreamSubscription<rust.BridgeEvent>? _eventsSub;
StreamSubscription<double>? _inputLevelSub;
late final PrefetchDebouncer _prefetch; late final PrefetchDebouncer _prefetch;
// v1 voice subsystem state (SDD-094/095/096/097). Driven by // v1 voice subsystem state (SDD-094/095/096/097). Driven by
@@ -385,7 +383,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
// and Notifications permission service. On non-macOS hosts the service // and Notifications permission service. On non-macOS hosts the service
// short-circuits to "granted" / "unsupported" and never wires the // short-circuits to "granted" / "unsupported" and never wires the
// MethodChannel. // MethodChannel.
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService(); final MacOSPermissionsService _macOSPermissions =
MacOSPermissionsService();
final UiPreferencesService _uiPreferences = const UiPreferencesService(); final UiPreferencesService _uiPreferences = const UiPreferencesService();
@override @override
@@ -396,9 +395,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer); _prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer);
_hostCtl.addListener(_onHostEdited); _hostCtl.addListener(_onHostEdited);
_eventsSub = rust.eventsStream().listen(_onEvent); _eventsSub = rust.eventsStream().listen(_onEvent);
_inputLevelSub = rust.inputLevelStream().listen((level) {
if (mounted) setState(() => _inputLevel = level);
});
// SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged // SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged
// events as early as possible so the listen-only banner reflects // events as early as possible so the listen-only banner reflects
// the system state on first frame. // the system state on first frame.
@@ -416,10 +412,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
// capability badge upgrades from L0Focused → L1MacOSEventTap // capability badge upgrades from L0Focused → L1MacOSEventTap
// when the user grants the permission in System Settings. // when the user grants the permission in System Settings.
_macOSPermissions.start(); _macOSPermissions.start();
_macOSPermissions.checkInitialStates();
_macOSPermissions.pttCapabilityState.addListener( _macOSPermissions.pttCapabilityState.addListener(
_onMacOSPttCapabilityChanged, _onMacOSPttCapabilityChanged,
); );
_macOSPermissions.checkInitialStates();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_requestRecordAudioOnStartup()); unawaited(_requestRecordAudioOnStartup());
}); });
@@ -534,8 +530,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_pttLevel = level; _pttLevel = level;
if (level == 'L1MacOSEventTap') { if (level == 'L1MacOSEventTap') {
_pttBackendId = 'macos-event-tap'; _pttBackendId = 'macos-event-tap';
} else if (level == 'L0Focused') {
_pttBackendId = 'focused';
} }
}); });
} }
@@ -676,10 +670,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_reconnectDelay = null; _reconnectDelay = null;
}); });
case rust.BridgeEvent_Reconnecting(:final attempt, :final delaySecs): case rust.BridgeEvent_Reconnecting(:final attempt, :final delaySecs):
_recordUiDiagnostic( _recordUiDiagnostic('connection', 'reconnecting attempt=$attempt delay=${delaySecs}s');
'connection',
'reconnecting attempt=$attempt delay=${delaySecs}s',
);
setState(() { setState(() {
_phase = ConnectionPhase.reconnecting; _phase = ConnectionPhase.reconnecting;
_reconnectAttempt = attempt; _reconnectAttempt = attempt;
@@ -854,41 +845,23 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
); );
return updated; return updated;
}); });
case rust.BridgeEvent_ClientJoined( case rust.BridgeEvent_ClientJoined(:final clientId, :final channelId, :final name, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
:final clientId,
:final channelId,
:final name,
:final inputMuted,
:final outputMuted,
:final isServerQuery,
:final talkPower,
:final talkPowerGranted,
):
if (!isServerQuery) { if (!isServerQuery) {
_applyClientAdd( _applyClientAdd(rust.BridgeClient(
rust.BridgeClient( id: clientId,
id: clientId, channel: channelId,
channel: channelId, name: name,
name: name, inputMuted: inputMuted,
inputMuted: inputMuted, outputMuted: outputMuted,
outputMuted: outputMuted, isSpeaking: false,
isSpeaking: false, isServerQuery: isServerQuery,
isServerQuery: isServerQuery, talkPower: talkPower,
talkPower: talkPower, talkPowerGranted: talkPowerGranted,
talkPowerGranted: talkPowerGranted, ));
),
);
} }
case rust.BridgeEvent_ClientLeft(:final clientId): case rust.BridgeEvent_ClientLeft(:final clientId):
_applyClientRemove(clientId); _applyClientRemove(clientId);
case rust.BridgeEvent_ClientUpdated( case rust.BridgeEvent_ClientUpdated(:final clientId, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
:final clientId,
:final inputMuted,
:final outputMuted,
:final isServerQuery,
:final talkPower,
:final talkPowerGranted,
):
_applyClientDelta((c) => c.id == clientId, (c) { _applyClientDelta((c) => c.id == clientId, (c) {
final updated = rust.BridgeClient( final updated = rust.BridgeClient(
id: c.id, id: c.id,
@@ -903,32 +876,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
); );
return updated; return updated;
}); });
case rust.BridgeEvent_ChannelAdded( case rust.BridgeEvent_ChannelAdded(:final id, :final parent, :final name, :final order, :final hasPassword, :final neededTalkPower):
:final id, _applyChannelAdd(rust.BridgeChannel(
:final parent, id: id,
:final name, parent: parent,
:final order, name: name,
:final hasPassword, order: order,
:final neededTalkPower, hasPassword: hasPassword,
): neededTalkPower: neededTalkPower,
_applyChannelAdd( ));
rust.BridgeChannel(
id: id,
parent: parent,
name: name,
order: order,
hasPassword: hasPassword,
neededTalkPower: neededTalkPower,
),
);
case rust.BridgeEvent_ChannelRemoved(:final id): case rust.BridgeEvent_ChannelRemoved(:final id):
_applyChannelRemove(id); _applyChannelRemove(id);
case rust.BridgeEvent_ChannelUpdated( case rust.BridgeEvent_ChannelUpdated(:final id, :final name, :final hasPassword, :final neededTalkPower):
:final id,
:final name,
:final hasPassword,
:final neededTalkPower,
):
_applyChannelDelta((ch) => ch.id == id, (ch) { _applyChannelDelta((ch) => ch.id == id, (ch) {
return rust.BridgeChannel( return rust.BridgeChannel(
id: ch.id, id: ch.id,
@@ -966,8 +925,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
final now = DateTime.now(); final now = DateTime.now();
if (_inChannel && if (_inChannel &&
(_lastSpeakingRefresh == null || (_lastSpeakingRefresh == null ||
now.difference(_lastSpeakingRefresh!) >= now.difference(_lastSpeakingRefresh!) >= _speakingRefreshInterval)) {
_speakingRefreshInterval)) {
_lastSpeakingRefresh = now; _lastSpeakingRefresh = now;
unawaited(_refreshSnapshot(recordActivity: false, reportErrors: false)); unawaited(_refreshSnapshot(recordActivity: false, reportErrors: false));
} }
@@ -997,7 +955,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey); HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
_eventsSub?.cancel(); _eventsSub?.cancel();
_inputLevelSub?.cancel();
_statsTimer?.cancel(); _statsTimer?.cancel();
_snapshotRefreshInFlight = false; _snapshotRefreshInFlight = false;
_snapshotRefreshQueued = false; _snapshotRefreshQueued = false;
@@ -1169,6 +1126,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
Future<void> _toggleOutputMute() async { Future<void> _toggleOutputMute() async {
final next = !_outputMuted; final next = !_outputMuted;
// Optimistic: flip the UI immediately.
setState(() { setState(() {
_outputMuted = next; _outputMuted = next;
}); });
@@ -1176,6 +1134,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
await rust.setOutputMuted(muted: next); await rust.setOutputMuted(muted: next);
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
// Roll back on failure.
setState(() { setState(() {
_outputMuted = !next; _outputMuted = !next;
}); });
@@ -1305,15 +1264,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
return; return;
} }
final next = !_hardMute; final next = !_hardMute;
final previousInputMuted = _inputMuted; // Optimistic: flip the UI immediately so the icon responds
final previousHardMute = _hardMute; // before the two bridge calls round-trip through FFI.
final previousPermissionMute = _hardMuteByPermission;
setState(() { setState(() {
_inputMuted = next; _inputMuted = next;
_hardMute = next; _hardMute = next;
if (next) { _hardMuteByPermission = false;
_hardMuteByPermission = false;
}
}); });
try { try {
// Hard-mute is two coordinated effects: // Hard-mute is two coordinated effects:
@@ -1329,10 +1285,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
await rust.setInputMuted(muted: next); await rust.setInputMuted(muted: next);
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
// Roll back on failure.
setState(() { setState(() {
_inputMuted = previousInputMuted; _inputMuted = !next;
_hardMute = previousHardMute; _hardMute = !next;
_hardMuteByPermission = previousPermissionMute;
}); });
_showUiError('hard mute', e); _showUiError('hard mute', e);
} }
@@ -1791,10 +1747,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
}; };
} }
void _applyClientDelta( void _applyClientDelta(bool Function(rust.BridgeClient) test, rust.BridgeClient Function(rust.BridgeClient) update) {
bool Function(rust.BridgeClient) test,
rust.BridgeClient Function(rust.BridgeClient) update,
) {
final snap = _snapshot; final snap = _snapshot;
if (snap == null) return; if (snap == null) return;
final clients = snap.clients.map((c) => test(c) ? update(c) : c).toList(); final clients = snap.clients.map((c) => test(c) ? update(c) : c).toList();
@@ -1880,15 +1833,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
}); });
} }
void _applyChannelDelta( void _applyChannelDelta(bool Function(rust.BridgeChannel) test, rust.BridgeChannel Function(rust.BridgeChannel) update) {
bool Function(rust.BridgeChannel) test,
rust.BridgeChannel Function(rust.BridgeChannel) update,
) {
final snap = _snapshot; final snap = _snapshot;
if (snap == null) return; if (snap == null) return;
final channels = snap.channels final channels = snap.channels.map((ch) => test(ch) ? update(ch) : ch).toList();
.map((ch) => test(ch) ? update(ch) : ch)
.toList();
setState(() { setState(() {
_snapshot = rust.BridgeSnapshot( _snapshot = rust.BridgeSnapshot(
serverName: snap.serverName, serverName: snap.serverName,
@@ -2418,7 +2366,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_currentVoiceChannelId, _currentVoiceChannelId,
), ),
audioStats: _audioStats, audioStats: _audioStats,
inputLevel: _inputLevel,
pttLevel: _pttLevel, pttLevel: _pttLevel,
pttBackendId: _pttBackendId, pttBackendId: _pttBackendId,
pttBoundInputClass: _pttBoundInputClass, pttBoundInputClass: _pttBoundInputClass,
@@ -2492,7 +2439,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
Expanded(child: snapshotView), Expanded(child: snapshotView),
const SizedBox(height: 8), const SizedBox(height: 8),
permissionBanner, permissionBanner,
VoiceStatusChip( CompactVoiceBar(
inChannel: _inChannel,
transmitMode: _transmitMode, transmitMode: _transmitMode,
releaseTailMs: _releaseTailMs, releaseTailMs: _releaseTailMs,
pttBoundKeyLabel: _pttBoundKeyLabel, pttBoundKeyLabel: _pttBoundKeyLabel,
@@ -2500,22 +2448,15 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
isTouchOnly: isTouchOnlyPttHost, isTouchOnly: isTouchOnlyPttHost,
inputMuted: _hardMute, inputMuted: _hardMute,
outputMuted: _outputMuted, outputMuted: _outputMuted,
hardMuteByTalkPower: _hardMuteByTalkPower, onToggleInputMute: _onToggleHardMute,
onToggleOutputMute: _toggleOutputMute,
onOpenDetails: () => _onOpenVoiceDetailsSheet(),
onPttHeldChanged: _onOnscreenPttHeldChanged,
talkPower: ownClientState?.talkPower, talkPower: ownClientState?.talkPower,
neededTalkPower: ownClientState?.neededTalkPower, neededTalkPower: ownClientState?.neededTalkPower,
talkPowerGranted: ownClientState?.talkPowerGranted, talkPowerGranted: ownClientState?.talkPowerGranted,
onTap: () => _onOpenVoiceDetailsSheet(), hardMuteByTalkPower: _hardMuteByTalkPower,
onToggleInputMute: _onToggleHardMute,
onToggleOutputMute: _toggleOutputMute,
), ),
if (_inChannel &&
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
const SizedBox(height: 8),
VoicePttButton(
active: _audioStats?.pttActive ?? false,
onHeldChanged: _onOnscreenPttHeldChanged,
),
],
], ],
); );
}, },
@@ -309,14 +309,12 @@ class MacOSPermissionsService {
_inputMonitoringState.value = state; _inputMonitoringState.value = state;
_pttCapabilityState.value = _pttCapabilityLevel(state); _pttCapabilityState.value = _pttCapabilityLevel(state);
} }
break;
case methodLocalNetworkStateChanged: case methodLocalNetworkStateChanged:
final args = call.arguments; final args = call.arguments;
if (args is Map) { if (args is Map) {
_localNetworkState.value = _localNetworkState.value =
_parseLocalNetworkState(args['state'] as String?); _parseLocalNetworkState(args['state'] as String?);
} }
break;
default: default:
break; break;
} }
+2 -16
View File
@@ -261,12 +261,6 @@ Stream<BridgeEvent> eventsStream() =>
Future<BridgeAudioStats> audioStats() => Future<BridgeAudioStats> audioStats() =>
RustLib.instance.api.crateApiAudioStats(); RustLib.instance.api.crateApiAudioStats();
/// Subscribe to real-time microphone input level at ~30 Hz.
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
/// when the Dart subscriber cancels or the session is dropped.
Stream<double> inputLevelStream() =>
RustLib.instance.api.crateApiInputLevelStream();
/// Apply the P1 audio-processing config. /// Apply the P1 audio-processing config.
Future<void> setAudioProcessingConfig({ Future<void> setAudioProcessingConfig({
required BridgeAudioProcessingConfig config, required BridgeAudioProcessingConfig config,
@@ -687,22 +681,15 @@ class BridgeAudioStats {
/// Current push-to-talk state. /// Current push-to-talk state.
final bool pttActive; final bool pttActive;
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
final double inputLevel;
const BridgeAudioStats({ const BridgeAudioStats({
required this.framesSent, required this.framesSent,
required this.framesReceived, required this.framesReceived,
required this.pttActive, required this.pttActive,
required this.inputLevel,
}); });
@override @override
int get hashCode => int get hashCode =>
framesSent.hashCode ^ framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
framesReceived.hashCode ^
pttActive.hashCode ^
inputLevel.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -711,8 +698,7 @@ class BridgeAudioStats {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
framesSent == other.framesSent && framesSent == other.framesSent &&
framesReceived == other.framesReceived && framesReceived == other.framesReceived &&
pttActive == other.pttActive && pttActive == other.pttActive;
inputLevel == other.inputLevel;
} }
/// Bookmark DTO mirroring [`chanora_core::Bookmark`]. /// Bookmark DTO mirroring [`chanora_core::Bookmark`].
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => -20394775; int get rustContentHash => 281698435;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@@ -123,8 +123,6 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiInitStorage({required String dir}); Future<void> crateApiInitStorage({required String dir});
Stream<double> crateApiInputLevelStream();
Future<bool> crateApiIsConnected(); Future<bool> crateApiIsConnected();
Future<BridgeAudioDeviceList> crateApiListAudioDevices(); Future<BridgeAudioDeviceList> crateApiListAudioDevices();
@@ -764,38 +762,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiInitStorageConstMeta => TaskConstMeta get kCrateApiInitStorageConstMeta =>
const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]); const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]);
@override
Stream<double> crateApiInputLevelStream() {
final sink = RustStreamSink<double>();
unawaited(
handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_StreamSink_f_32_Sse(sink, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 21,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiInputLevelStreamConstMeta,
argValues: [sink],
apiImpl: this,
),
),
);
return sink.stream;
}
TaskConstMeta get kCrateApiInputLevelStreamConstMeta =>
const TaskConstMeta(debugName: "input_level_stream", argNames: ["sink"]);
@override @override
Future<bool> crateApiIsConnected() { Future<bool> crateApiIsConnected() {
return handler.executeNormal( return handler.executeNormal(
@@ -805,7 +771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 22, funcId: 21,
port: port_, port: port_,
); );
}, },
@@ -832,7 +798,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 23, funcId: 22,
port: port_, port: port_,
); );
}, },
@@ -859,7 +825,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 24, funcId: 23,
port: port_, port: port_,
); );
}, },
@@ -883,7 +849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask( SyncTask(
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -913,7 +879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 26, funcId: 25,
port: port_, port: port_,
); );
}, },
@@ -943,7 +909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 27, funcId: 26,
port: port_, port: port_,
); );
}, },
@@ -970,7 +936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 28, funcId: 27,
port: port_, port: port_,
); );
}, },
@@ -995,7 +961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(state, serializer); sse_encode_String(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1028,7 +994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 30, funcId: 29,
port: port_, port: port_,
); );
}, },
@@ -1055,7 +1021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_audio_route(route, serializer); sse_encode_bridge_audio_route(route, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1089,7 +1055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 32, funcId: 31,
port: port_, port: port_,
); );
}, },
@@ -1124,7 +1090,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 33, funcId: 32,
port: port_, port: port_,
); );
}, },
@@ -1154,7 +1120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 34, funcId: 33,
port: port_, port: port_,
); );
}, },
@@ -1182,7 +1148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 35, funcId: 34,
port: port_, port: port_,
); );
}, },
@@ -1210,7 +1176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 36, funcId: 35,
port: port_, port: port_,
); );
}, },
@@ -1240,7 +1206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 37, funcId: 36,
port: port_, port: port_,
); );
}, },
@@ -1268,7 +1234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_network_state(state, serializer); sse_encode_bridge_network_state(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1294,7 +1260,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 39, funcId: 38,
port: port_, port: port_,
); );
}, },
@@ -1322,7 +1288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 40, funcId: 39,
port: port_, port: port_,
); );
}, },
@@ -1350,7 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 41, funcId: 40,
port: port_, port: port_,
); );
}, },
@@ -1378,7 +1344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 42, funcId: 41,
port: port_, port: port_,
); );
}, },
@@ -1410,7 +1376,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 43, funcId: 42,
port: port_, port: port_,
); );
}, },
@@ -1440,7 +1406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 44, funcId: 43,
port: port_, port: port_,
); );
}, },
@@ -1468,7 +1434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 45, funcId: 44,
port: port_, port: port_,
); );
}, },
@@ -1496,7 +1462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 46, funcId: 45,
port: port_, port: port_,
); );
}, },
@@ -1523,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 47, funcId: 46,
port: port_, port: port_,
); );
}, },
@@ -1551,7 +1517,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 48, funcId: 47,
port: port_, port: port_,
); );
}, },
@@ -1583,7 +1549,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 49, funcId: 48,
port: port_, port: port_,
); );
}, },
@@ -1612,7 +1578,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 50, funcId: 49,
port: port_, port: port_,
); );
}, },
@@ -1644,12 +1610,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError(); throw UnimplementedError();
} }
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
throw UnimplementedError();
}
@protected @protected
String dco_decode_String(dynamic raw) { String dco_decode_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1821,13 +1781,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) { BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 4) if (arr.length != 3)
throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
return BridgeAudioStats( return BridgeAudioStats(
framesSent: dco_decode_u_32(arr[0]), framesSent: dco_decode_u_32(arr[0]),
framesReceived: dco_decode_u_32(arr[1]), framesReceived: dco_decode_u_32(arr[1]),
pttActive: dco_decode_bool(arr[2]), pttActive: dco_decode_bool(arr[2]),
inputLevel: dco_decode_f_32(arr[3]),
); );
} }
@@ -2312,14 +2271,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError('Unreachable ()'); throw UnimplementedError('Unreachable ()');
} }
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
throw UnimplementedError('Unreachable ()');
}
@protected @protected
String sse_decode_String(SseDeserializer deserializer) { String sse_decode_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -2537,12 +2488,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_framesSent = sse_decode_u_32(deserializer); var var_framesSent = sse_decode_u_32(deserializer);
var var_framesReceived = sse_decode_u_32(deserializer); var var_framesReceived = sse_decode_u_32(deserializer);
var var_pttActive = sse_decode_bool(deserializer); var var_pttActive = sse_decode_bool(deserializer);
var var_inputLevel = sse_decode_f_32(deserializer);
return BridgeAudioStats( return BridgeAudioStats(
framesSent: var_framesSent, framesSent: var_framesSent,
framesReceived: var_framesReceived, framesReceived: var_framesReceived,
pttActive: var_pttActive, pttActive: var_pttActive,
inputLevel: var_inputLevel,
); );
} }
@@ -3250,23 +3199,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
} }
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(
self.setupAndSerialize(
codec: SseCodec(
decodeSuccessData: sse_decode_f_32,
decodeErrorData: sse_decode_AnyhowException,
),
),
serializer,
);
}
@protected @protected
void sse_encode_String(String self, SseSerializer serializer) { void sse_encode_String(String self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -3448,7 +3380,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_u_32(self.framesSent, serializer); sse_encode_u_32(self.framesSent, serializer);
sse_encode_u_32(self.framesReceived, serializer); sse_encode_u_32(self.framesReceived, serializer);
sse_encode_bool(self.pttActive, serializer); sse_encode_bool(self.pttActive, serializer);
sse_encode_f_32(self.inputLevel, serializer);
} }
@protected @protected
@@ -27,9 +27,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@@ -213,11 +210,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@@ -447,12 +439,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@@ -29,9 +29,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@@ -215,11 +212,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@@ -449,12 +441,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@@ -33,7 +33,6 @@ class VoiceBar extends StatelessWidget {
required this.onConfigure, required this.onConfigure,
required this.onPttHeldChanged, required this.onPttHeldChanged,
this.talkPowerBlocked = false, this.talkPowerBlocked = false,
this.inputLevel,
}); });
final bool inChannel; final bool inChannel;
@@ -54,10 +53,6 @@ class VoiceBar extends StatelessWidget {
/// level meter. Pass `null` to render an idle meter. /// level meter. Pass `null` to render an idle meter.
final rust.BridgeAudioStats? audioStats; final rust.BridgeAudioStats? audioStats;
/// Real-time input level from the 30 Hz stream (dBFS).
/// When non-null, takes precedence over `audioStats.inputLevel`.
final double? inputLevel;
/// PTT capability badge inputs — passed through to /// PTT capability badge inputs — passed through to
/// [`PttCapabilityBadge`]. /// [`PttCapabilityBadge`].
final String pttLevel; final String pttLevel;
@@ -201,7 +196,7 @@ class VoiceBar extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// Row 4: level meter // Row 4: level meter
VoiceLevelMeter(active: levelActive, level: inputLevel ?? stats?.inputLevel), VoiceLevelMeter(active: levelActive),
const SizedBox(height: 4), const SizedBox(height: 4),
if (stats != null) if (stats != null)
Text( Text(
File diff suppressed because it is too large Load Diff
@@ -1,78 +1,28 @@
import 'dart:math' show max;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Shared compact level meter used by voice surfaces. /// Shared compact level meter used by voice surfaces.
/// class VoiceLevelMeter extends StatelessWidget {
/// When [level] is null (no stats available yet), falls back to [active] const VoiceLevelMeter({super.key, required this.active});
/// for a binary indicator. When [level] is provided it is interpreted as
/// dBFS and mapped to a 01 fill fraction via [dbfsToFraction] (floors
/// at -60 dBFS).
class VoiceLevelMeter extends StatefulWidget {
const VoiceLevelMeter({super.key, this.active = false, this.level});
/// Binary fallback when no dBFS value is available.
final bool active; final bool active;
/// Real input level in dBFS (-120 = silence, 0 = clipping).
/// Null means stats are not yet available; [active] is used instead.
final double? level;
/// Map dBFS [-60, 0] → [0.0, 1.0].
static double dbfsToFraction(double dbfs) {
const floor = -60.0;
if (dbfs <= floor) return 0.0;
if (dbfs >= 0.0) return 1.0;
return (dbfs - floor) / -floor;
}
@override
State<VoiceLevelMeter> createState() => _VoiceLevelMeterState();
}
class _VoiceLevelMeterState extends State<VoiceLevelMeter> {
double _previousFill = 0.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final double fill;
final Color color;
if (widget.level != null) {
fill = VoiceLevelMeter.dbfsToFraction(widget.level!);
color = fill > 0.0
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant;
} else {
fill = widget.active ? 0.75 : 0.05;
color = widget.active
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant;
}
final begin = _previousFill;
_previousFill = fill;
return Container( return Container(
height: 8, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: TweenAnimationBuilder<double>( child: FractionallySizedBox(
tween: Tween<double>(begin: begin, end: fill), alignment: AlignmentDirectional.centerStart,
duration: const Duration(milliseconds: 120), widthFactor: active ? 0.75 : 0.05,
curve: Curves.easeOut,
builder: (context, animatedFill, child) {
return FractionallySizedBox(
alignment: AlignmentDirectional.centerStart,
widthFactor: max(animatedFill, 0.02),
child: child,
);
},
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: active
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key><string>chanora_bridge</string>
<key>CFBundleIdentifier</key><string>app.chanora.bridge</string>
<key>CFBundleName</key><string>chanora_bridge</string>
<key>CFBundlePackageType</key><string>FMWK</string>
<key>CFBundleShortVersionString</key><string>1.0.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>CFBundleSupportedPlatforms</key><array><string>MacOSX</string></array>
<key>MinimumOSVersion</key><string>10.15</string>
</dict>
</plist>
@@ -0,0 +1 @@
Versions/Current/chanora_bridge
+37
View File
@@ -1,20 +1,57 @@
PODS: PODS:
- audio_session (0.0.1):
- FlutterMacOS
- chanora_bridge (1.0.0) - chanora_bridge (1.0.0)
- connectivity_plus (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- package_info_plus (0.0.1):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`) - chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
chanora_bridge: chanora_bridge:
:path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos" :path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos"
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103 chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb
@@ -32,7 +32,6 @@
45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; }; 45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; };
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; }; 9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; };
C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; }; C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -94,7 +93,6 @@
ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -110,7 +108,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
8C6000042DD0000000000001 /* SileroCoreML in Frameworks */, 8C6000042DD0000000000001 /* SileroCoreML in Frameworks */,
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */, 9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */,
); );
@@ -173,7 +170,6 @@
33CEB47122A05771004F2AC0 /* Flutter */ = { 33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
@@ -260,7 +256,6 @@
); );
name = Runner; name = Runner;
packageProductDependencies = ( packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
8C6000032DD0000000000001 /* SileroCoreML */, 8C6000032DD0000000000001 /* SileroCoreML */,
); );
productName = Runner; productName = Runner;
@@ -307,7 +302,6 @@
); );
mainGroup = 33CC10E42044A3C60003C045; mainGroup = 33CC10E42044A3C60003C045;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */, 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
); );
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
@@ -871,10 +865,6 @@
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = ../../../silero-coreml; relativePath = ../../../silero-coreml;
}; };
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -883,10 +873,6 @@
package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */; package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
productName = SileroCoreML; productName = SileroCoreML;
}; };
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 33CC10E52044A3C60003C045 /* Project object */; rootObject = 33CC10E52044A3C60003C045 /* Project object */;
@@ -5,24 +5,6 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "chanora_flutter.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -48,7 +48,6 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
// -- Input Monitoring --------------------------------------------------- // -- Input Monitoring ---------------------------------------------------
case "checkInputMonitoring": case "checkInputMonitoring":
result(inputMonitoringStateString()) result(inputMonitoringStateString())
startInputMonitoringPolling()
case "requestInputMonitoring": case "requestInputMonitoring":
requestInputMonitoring(result: result) requestInputMonitoring(result: result)
+4 -4
View File
@@ -457,10 +457,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -790,10 +790,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -493,7 +493,6 @@ void main() {
framesSent: 1, framesSent: 1,
framesReceived: 0, framesReceived: 0,
pttActive: true, pttActive: true,
inputLevel: -30.0,
), ),
), ),
); );
+2 -42
View File
@@ -253,87 +253,49 @@ pub enum SessionEvent {
}, },
/// Audio route changed (speaker/earpiece/BT/wired headset). /// Audio route changed (speaker/earpiece/BT/wired headset).
AudioRouteChanged { AudioRouteChanged {
/// New audio output route.
route: AudioRoute, route: AudioRoute,
}, },
/// A client moved to a different channel.
ClientMoved { ClientMoved {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Destination channel.
new_channel_id: u64, new_channel_id: u64,
}, },
/// A new client connected.
ClientJoined { ClientJoined {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Channel the client joined.
channel_id: u64, channel_id: u64,
/// Display nickname.
name: String, name: String,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// Whether this is a server query (bot) client.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A client disconnected.
ClientLeft { ClientLeft {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Display nickname at time of disconnect.
name: String, name: String,
}, },
/// Client properties changed.
ClientUpdated { ClientUpdated {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// Whether this is a server query (bot) client.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A new channel appeared.
ChannelAdded { ChannelAdded {
/// Unique channel identifier.
id: u64, id: u64,
/// Parent channel ID.
parent: u64, parent: u64,
/// Channel name.
name: String, name: String,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
order: i64, order: i64,
/// Whether the channel requires a password.
has_password: bool, has_password: bool,
/// Talk power required to speak, or `None` when unrestricted.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
/// A channel was deleted.
ChannelRemoved { ChannelRemoved {
/// Channel identifier.
id: u64, id: u64,
}, },
/// Channel properties changed.
ChannelUpdated { ChannelUpdated {
/// Unique channel identifier.
id: u64, id: u64,
/// Channel name.
name: String, name: String,
/// Whether the channel requires a password.
has_password: bool, has_password: bool,
/// Talk power required to speak, or `None` when unrestricted.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
} }
@@ -1345,8 +1307,8 @@ impl ChanoraSession {
Ok(()) Ok(())
} }
/// Read audio engine statistics: (frames_sent, frames_received, transmit_active, input_level_dbfs). /// Read audio engine statistics: (frames_sent, frames_received, transmit_active).
pub async fn audio_stats(&self) -> Result<(u32, u32, bool, f32), CoreError> { pub async fn audio_stats(&self) -> Result<(u32, u32, bool), CoreError> {
let guard = self.inner.lock().await; let guard = self.inner.lock().await;
let state = guard.as_ref().ok_or(CoreError::NotConnected)?; let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?; let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?;
@@ -1354,7 +1316,6 @@ impl ChanoraSession {
audio.frames_sent(), audio.frames_sent(),
audio.frames_received(), audio.frames_received(),
audio.transmit_active(), audio.transmit_active(),
audio.input_level(),
)) ))
} }
@@ -2438,7 +2399,6 @@ async fn supervisor_loop(ctx: SupervisorContext) {
/// the UI would render. Two snapshots with identical channel /// the UI would render. Two snapshots with identical channel
/// memberships, names, and orderings produce the same signature; /// memberships, names, and orderings produce the same signature;
/// any in-channel move, rename, or reorder produces a different one. /// any in-channel move, rename, or reorder produces a different one.
#[cfg(test)]
fn snapshot_signature(snap: &ServerSnapshot) -> u64 { fn snapshot_signature(snap: &ServerSnapshot) -> u64 {
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
+1 -5
View File
@@ -30,6 +30,7 @@ tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] } tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
rustfft = "6.2.0" rustfft = "6.2.0"
crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] } crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] }
crossbeam-utils = { version = "0.8", default-features = false }
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies] [target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
# Desktop audio I/O for Windows capture/playback and Linux capture. # Desktop audio I/O for Windows capture/playback and Linux capture.
@@ -73,11 +74,6 @@ ndk-context = "0.1"
# - deduplicated macro impls # - deduplicated macro impls
# - PowerSavingOffloaded PerformanceMode variant # - PowerSavingOffloaded PerformanceMode variant
oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" } oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" }
# Safe slice reinterpret for the oboe stereo output callback.
# bytemuck::cast_slice_mut replaces the raw-pointer cast from
# `&mut [(f32, f32)]` to `&mut [f32]` with a provenance-correct
# and UB-free transmute backed by `NoUninit`.
bytemuck = { version = "1", features = ["derive"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices # Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices
+11 -9
View File
@@ -523,7 +523,7 @@ impl AudioInputCallback for InputCallback {
struct OutputCallback { struct OutputCallback {
handler: AudioHandler<SessionAudioId>, handler: AudioHandler<SessionAudioId>,
event_consumer: crate::audio_event_queue::AudioEventConsumer, event_queue: Arc<AudioEventQueue>,
output_gain: Arc<AtomicU32>, output_gain: Arc<AtomicU32>,
output_muted: Arc<AtomicBool>, output_muted: Arc<AtomicBool>,
event_tx: BackendEventTx, event_tx: BackendEventTx,
@@ -542,12 +542,14 @@ impl AudioOutputCallback for OutputCallback {
frames: &mut [(f32, f32)], frames: &mut [(f32, f32)],
) -> DataCallbackResult { ) -> DataCallbackResult {
let _ = catch_unwind(AssertUnwindSafe(|| { let _ = catch_unwind(AssertUnwindSafe(|| {
let buf: &mut [f32] = let buf: &mut [f32] = unsafe {
bytemuck::cast_slice_mut::<(f32, f32), f32>(frames); std::slice::from_raw_parts_mut(frames.as_mut_ptr() as *mut f32, frames.len() * 2)
};
for s in buf.iter_mut() { for s in buf.iter_mut() {
*s = 0.0; *s = 0.0;
} }
for cmd in self.event_consumer.drain_controls() { let consumer = AudioEventQueue::consumer(&self.event_queue);
for cmd in consumer.drain_controls() {
match cmd { match cmd {
AudioCommand::SetVolume(id, vol) => { AudioCommand::SetVolume(id, vol) => {
if let Some(q) = self.handler.get_mut_queues().get_mut(&id) { if let Some(q) = self.handler.get_mut_queues().get_mut(&id) {
@@ -560,7 +562,7 @@ impl AudioOutputCallback for OutputCallback {
} }
} }
for pkt in self.event_consumer.drain_packets(50) { for pkt in consumer.drain_packets(50) {
if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) { if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) {
debug!(target: "chanora_audio", error = %e, "decode failed"); debug!(target: "chanora_audio", error = %e, "decode failed");
} }
@@ -791,7 +793,7 @@ impl AndroidVoiceUnit {
let event_queue = params.event_producer.queue(); let event_queue = params.event_producer.queue();
let output_cb = OutputCallback { let output_cb = OutputCallback {
handler: params.handler, handler: params.handler,
event_consumer: AudioEventQueue::consumer(&event_queue), event_queue: event_queue.clone(),
output_gain: params.output_gain.clone(), output_gain: params.output_gain.clone(),
output_muted: params.output_muted.clone(), output_muted: params.output_muted.clone(),
event_tx: event_tx.clone(), event_tx: event_tx.clone(),
@@ -814,7 +816,7 @@ impl AndroidVoiceUnit {
cfg, cfg,
&event_tx, &event_tx,
AudioHandler::new(), AudioHandler::new(),
AudioEventQueue::consumer(&event_queue), event_queue.clone(),
params.output_gain.clone(), params.output_gain.clone(),
params.output_muted.clone(), params.output_muted.clone(),
audio_processing_stats.clone(), audio_processing_stats.clone(),
@@ -1067,7 +1069,7 @@ impl AndroidVoiceUnit {
cfg: &AndroidVoiceStreamConfig, cfg: &AndroidVoiceStreamConfig,
event_tx: &BackendEventTx, event_tx: &BackendEventTx,
handler: AudioHandler<SessionAudioId>, handler: AudioHandler<SessionAudioId>,
event_consumer: crate::audio_event_queue::AudioEventConsumer, event_queue: Arc<AudioEventQueue>,
output_gain: Arc<AtomicU32>, output_gain: Arc<AtomicU32>,
output_muted: Arc<AtomicBool>, output_muted: Arc<AtomicBool>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>, audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
@@ -1075,7 +1077,7 @@ impl AndroidVoiceUnit {
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> { ) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
let cb = OutputCallback { let cb = OutputCallback {
handler, handler,
event_consumer, event_queue,
output_gain, output_gain,
output_muted, output_muted,
event_tx: event_tx.clone(), event_tx: event_tx.clone(),
@@ -22,8 +22,6 @@ pub enum AudioCommand {
/// Set a client's output volume. /// Set a client's output volume.
SetVolume(SessionAudioId, f32), SetVolume(SessionAudioId, f32),
/// Remove a client's decode queue. /// Remove a client's decode queue.
// TODO: Wire to client disconnect path; handled in callback but no
// producer currently pushes this command.
RemoveClient(SessionAudioId), RemoveClient(SessionAudioId),
} }
@@ -404,19 +404,6 @@ impl Default for SharedAudioProcessingStats {
} }
impl SharedAudioProcessingStats { impl SharedAudioProcessingStats {
/// Store the raw input dBFS level (desktop capture path).
/// Mobile platforms use [`Self::update_capture`] instead, which
/// also records VAD state; this lighter method is for the cpal
/// capture path that has no VAD pipeline.
pub fn set_input_dbfs(&self, dbfs: f32) {
self.input_dbfs.store(dbfs.to_bits(), Ordering::Relaxed);
}
/// Read the current input dBFS level.
pub fn input_dbfs(&self) -> f32 {
f32::from_bits(self.input_dbfs.load(Ordering::Relaxed))
}
/// Store capture levels and VAD state. /// Store capture levels and VAD state.
pub fn update_capture( pub fn update_capture(
&self, &self,
+19 -39
View File
@@ -838,7 +838,6 @@ impl AudioEngine {
transmit_flag_for_capture, transmit_flag_for_capture,
frames_sent.clone(), frames_sent.clone(),
cfg.mic_gain, cfg.mic_gain,
audio_processing_stats.clone(),
); );
let (input_stream, capture_active) = match capture_result { let (input_stream, capture_active) = match capture_result {
Ok(s) => (Some(s), true), Ok(s) => (Some(s), true),
@@ -1598,11 +1597,6 @@ impl AudioEngine {
self.frames_received.load(Ordering::Relaxed) self.frames_received.load(Ordering::Relaxed)
} }
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
pub fn input_level(&self) -> f32 {
self.audio_processing_stats.input_dbfs()
}
/// Current audio-processing config snapshot. /// Current audio-processing config snapshot.
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig { pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
self.audio_processing_config.lock().unwrap().clone() self.audio_processing_config.lock().unwrap().clone()
@@ -1670,21 +1664,15 @@ impl AudioEngine {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped); let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
for _ in 0..64 { loop {
match self.audio_event_producer.push_control(cmd) { match self.audio_event_producer.push_control(cmd) {
Ok(()) => return, Ok(()) => break,
Err(returned) => { Err(returned) => {
cmd = returned; cmd = returned;
std::thread::yield_now(); std::thread::yield_now();
} }
} }
} }
tracing::warn!(
target: "chanora_audio",
client_id,
volume = clamped,
"set_client_volume: control queue full after 64 retries — volume not applied"
);
} }
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
{ {
@@ -1723,7 +1711,6 @@ fn try_open_capture(
transmit_active: Arc<AtomicBool>, transmit_active: Arc<AtomicBool>,
frames_sent: Arc<AtomicU32>, frames_sent: Arc<AtomicU32>,
mic_gain: f32, mic_gain: f32,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
) -> Result<cpal::Stream, AudioError> { ) -> Result<cpal::Stream, AudioError> {
let in_cfg = in_dev let in_cfg = in_dev
.default_input_config() .default_input_config()
@@ -1759,7 +1746,6 @@ fn try_open_capture(
voice_out_tx, voice_out_tx,
transmit_active, transmit_active,
frames_sent, frames_sent,
audio_processing_stats,
))); )));
let stream = match in_format { let stream = match in_format {
@@ -1811,7 +1797,6 @@ struct CaptureState {
/// capacity so the drain-into-frame path skips the allocator /// capacity so the drain-into-frame path skips the allocator
/// after warmup. Same precedent as `mono_scratch` above. /// after warmup. Same precedent as `mono_scratch` above.
frame_scratch: Vec<f32>, frame_scratch: Vec<f32>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
} }
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
@@ -1824,7 +1809,6 @@ impl CaptureState {
voice_out_tx: mpsc::Sender<OutPacket>, voice_out_tx: mpsc::Sender<OutPacket>,
transmit_active: Arc<AtomicBool>, transmit_active: Arc<AtomicBool>,
frames_sent: Arc<AtomicU32>, frames_sent: Arc<AtomicU32>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
) -> Self { ) -> Self {
Self { Self {
encoder, encoder,
@@ -1838,9 +1822,12 @@ impl CaptureState {
voice_out_tx, voice_out_tx,
transmit_active, transmit_active,
frames_sent, frames_sent,
// Generous upper bound for typical cpal periods
// (commonly 256..1024 frames); `clear()` retains the
// backing allocation across callbacks. See struct doc.
mono_scratch: Vec::with_capacity(4096), mono_scratch: Vec::with_capacity(4096),
// Exact upper bound: drain pulls FRAME_SAMPLES at a time.
frame_scratch: Vec::with_capacity(FRAME_SAMPLES), frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
audio_processing_stats,
} }
} }
@@ -1848,8 +1835,17 @@ impl CaptureState {
/// 48 kHz mono frames; encode and send when `transmit_active` /// 48 kHz mono frames; encode and send when `transmit_active`
/// is true (PTT engaged). /// is true (PTT engaged).
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) { fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
// 1. Down-mix to mono (pre-gain). Always performed so the level if !self.transmit_active.load(Ordering::Relaxed) {
// meter reflects real mic input even when PTT is released. // Drain accumulator while muted so we don't pop on PTT release.
self.pcm_accum.clear();
return;
}
// 1. Down-mix to mono + gain.
// Reuse `self.mono_scratch` to avoid a per-callback Vec
// allocation on the realtime audio thread; see struct
// doc and the engine.rs:1389-1397 precedent for why this
// matters for user-perceptible audio popping.
let in_channels = self.in_channels; let in_channels = self.in_channels;
let mic_gain = self.mic_gain; let mic_gain = self.mic_gain;
self.mono_scratch.clear(); self.mono_scratch.clear();
@@ -1857,23 +1853,8 @@ impl CaptureState {
self.mono_scratch.reserve(frame_count); self.mono_scratch.reserve(frame_count);
for frame in buf.chunks(in_channels) { for frame in buf.chunks(in_channels) {
let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum(); let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum();
self.mono_scratch.push(sum / frame.len() as f32); self.mono_scratch
} .push((sum / frame.len() as f32) * mic_gain);
// Measure dBFS from pre-gain samples so the level meter
// reflects the raw mic input, not the amplified signal.
self.audio_processing_stats
.set_input_dbfs(crate::frame::dbfs(&self.mono_scratch));
if mic_gain != 1.0 {
for s in &mut self.mono_scratch {
*s *= mic_gain;
}
}
if !self.transmit_active.load(Ordering::Relaxed) {
self.pcm_accum.clear();
return;
} }
// 2. Resample to 48 kHz if needed. We re-borrow // 2. Resample to 48 kHz if needed. We re-borrow
@@ -2501,7 +2482,6 @@ pub mod bench_seam {
tx, tx,
transmit_active.clone(), transmit_active.clone(),
frames_sent, frames_sent,
Arc::new(crate::SharedAudioProcessingStats::default()),
); );
Self { Self {
state, state,
@@ -22,6 +22,9 @@ use std::sync::atomic::{AtomicBool, AtomicU32};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[cfg(not(target_os = "android"))]
use tsclientlib::audio::AudioHandler;
#[cfg(target_os = "android")]
use tsclientlib::audio::AudioHandler; use tsclientlib::audio::AudioHandler;
use crate::engine::SessionAudioId; use crate::engine::SessionAudioId;
+1 -74
View File
@@ -1038,8 +1038,6 @@ pub struct BridgeAudioStats {
pub frames_received: u32, pub frames_received: u32,
/// Current push-to-talk state. /// Current push-to-talk state.
pub ptt_active: bool, pub ptt_active: bool,
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
pub input_level: f32,
} }
/// Bridge route class for P1 audio-processing policy. /// Bridge route class for P1 audio-processing policy.
@@ -1703,87 +1701,49 @@ pub enum BridgeEvent {
}, },
/// Audio route changed (speaker/earpiece/BT/wired). /// Audio route changed (speaker/earpiece/BT/wired).
AudioRouteChanged { AudioRouteChanged {
/// New audio output route.
route: BridgeAudioRoute, route: BridgeAudioRoute,
}, },
/// A client moved to a different channel.
ClientMoved { ClientMoved {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Destination channel.
new_channel_id: u64, new_channel_id: u64,
}, },
/// A new client connected.
ClientJoined { ClientJoined {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Channel the client joined.
channel_id: u64, channel_id: u64,
/// Display nickname.
name: String, name: String,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// True for server query (bot) clients.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A client disconnected.
ClientLeft { ClientLeft {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Display nickname at time of disconnect.
name: String, name: String,
}, },
/// Client properties changed.
ClientUpdated { ClientUpdated {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// True for server query (bot) clients.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A new channel appeared.
ChannelAdded { ChannelAdded {
/// Unique channel identifier.
id: u64, id: u64,
/// Parent channel ID.
parent: u64, parent: u64,
/// Channel name.
name: String, name: String,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
order: i64, order: i64,
/// Whether the channel requires a password.
has_password: bool, has_password: bool,
/// Talk power required to speak; `None` means no restriction.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
/// A channel was deleted.
ChannelRemoved { ChannelRemoved {
/// Channel identifier.
id: u64, id: u64,
}, },
/// Channel properties changed.
ChannelUpdated { ChannelUpdated {
/// Unique channel identifier.
id: u64, id: u64,
/// Channel name.
name: String, name: String,
/// Whether the channel requires a password.
has_password: bool, has_password: bool,
/// Talk power required to speak; `None` means no restriction.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
} }
@@ -2098,7 +2058,7 @@ pub fn events_stream(sink: StreamSink<BridgeEvent>) -> Result<(), BridgeError> {
/// Read audio statistics. Errors if no connection or audio not started. /// Read audio statistics. Errors if no connection or audio not started.
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> { pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
let (s, r, p, lvl) = runtime() let (s, r, p) = runtime()
.spawn(async { session().audio_stats().await }) .spawn(async { session().audio_stats().await })
.await .await
.map_err(|e| task_join_error("audio_stats", e))??; .map_err(|e| task_join_error("audio_stats", e))??;
@@ -2106,42 +2066,9 @@ pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
frames_sent: s, frames_sent: s,
frames_received: r, frames_received: r,
ptt_active: p, ptt_active: p,
input_level: lvl,
}) })
} }
/// Subscribe to real-time microphone input level at ~30 Hz.
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
/// when the Dart subscriber cancels, the session is dropped, or
/// the session becomes persistently unavailable.
pub fn input_level_stream(sink: StreamSink<f32>) -> Result<(), BridgeError> {
runtime().spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(33));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut consecutive_errors = 0u32;
loop {
interval.tick().await;
let level = match session().audio_stats().await {
Ok((_, _, _, lvl)) => {
consecutive_errors = 0;
lvl
}
Err(_) => {
consecutive_errors += 1;
if consecutive_errors >= 10 {
return;
}
-120.0
}
};
if sink.add(level).is_err() {
return;
}
}
});
Ok(())
}
/// Apply the P1 audio-processing config. /// Apply the P1 audio-processing config.
pub async fn set_audio_processing_config( pub async fn set_audio_processing_config(
config: BridgeAudioProcessingConfig, config: BridgeAudioProcessingConfig,
+30 -86
View File
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -20394775; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 281698435;
// Section: executor // Section: executor
@@ -738,42 +738,6 @@ fn wire__crate__api__init_storage_impl(
}, },
) )
} }
fn wire__crate__api__input_level_stream_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "input_level_stream",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_sink =
<StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec>>::sse_decode(
&mut deserializer,
);
deserializer.end();
move |context| {
transform_result_sse::<_, crate::BridgeError>((move || {
let output_ok = crate::api::input_level_stream(api_sink)?;
Ok(output_ok)
})())
}
},
)
}
fn wire__crate__api__is_connected_impl( fn wire__crate__api__is_connected_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -1826,14 +1790,6 @@ impl SseDecode
} }
} }
impl SseDecode for StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <String>::sse_decode(deserializer);
return StreamSink::deserialize(inner);
}
}
impl SseDecode for String { impl SseDecode for String {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -2006,12 +1962,10 @@ impl SseDecode for crate::api::BridgeAudioStats {
let mut var_framesSent = <u32>::sse_decode(deserializer); let mut var_framesSent = <u32>::sse_decode(deserializer);
let mut var_framesReceived = <u32>::sse_decode(deserializer); let mut var_framesReceived = <u32>::sse_decode(deserializer);
let mut var_pttActive = <bool>::sse_decode(deserializer); let mut var_pttActive = <bool>::sse_decode(deserializer);
let mut var_inputLevel = <f32>::sse_decode(deserializer);
return crate::api::BridgeAudioStats { return crate::api::BridgeAudioStats {
frames_sent: var_framesSent, frames_sent: var_framesSent,
frames_received: var_framesReceived, frames_received: var_framesReceived,
ptt_active: var_pttActive, ptt_active: var_pttActive,
input_level: var_inputLevel,
}; };
} }
} }
@@ -2801,34 +2755,33 @@ fn pde_ffi_dispatcher_primary_impl(
14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), 14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len), 15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len), 20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len), 21 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len), 22 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len), 23 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len), 25 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len), 26 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len), 27 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len), 29 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len), 31 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len), 32 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len), 33 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len), 34 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len), 35 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len), 36 => {
37 => {
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len) wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
} }
39 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len), 38 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len), 39 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len), 40 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len), 41 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len), 42 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len), 43 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len), 44 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len), 45 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len), 46 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len), 47 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len), 48 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len), 49 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -2850,10 +2803,10 @@ fn pde_ffi_dispatcher_sync_impl(
data_len, data_len,
), ),
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len), 19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
25 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len), 24 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
29 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len), 28 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
31 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len), 30 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
38 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len), 37 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -3031,7 +2984,6 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioStats {
self.frames_sent.into_into_dart().into_dart(), self.frames_sent.into_into_dart().into_dart(),
self.frames_received.into_into_dart().into_dart(), self.frames_received.into_into_dart().into_dart(),
self.ptt_active.into_into_dart().into_dart(), self.ptt_active.into_into_dart().into_dart(),
self.input_level.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
} }
@@ -3700,13 +3652,6 @@ impl SseEncode
} }
} }
impl SseEncode for StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
unimplemented!("")
}
}
impl SseEncode for String { impl SseEncode for String {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -3836,7 +3781,6 @@ impl SseEncode for crate::api::BridgeAudioStats {
<u32>::sse_encode(self.frames_sent, serializer); <u32>::sse_encode(self.frames_sent, serializer);
<u32>::sse_encode(self.frames_received, serializer); <u32>::sse_encode(self.frames_received, serializer);
<bool>::sse_encode(self.ptt_active, serializer); <bool>::sse_encode(self.ptt_active, serializer);
<f32>::sse_encode(self.input_level, serializer);
} }
} }
+81 -52
View File
@@ -56,13 +56,6 @@ type PendingMoves = HashMap<
), ),
>; >;
struct EventChannels {
voice_in: mpsc::Sender<InboundVoice>,
chat: mpsc::Sender<ChatMessage>,
activity: mpsc::Sender<ServerActivity>,
delta: mpsc::Sender<ProtocolDelta>,
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum SendTimeoutError<T> { enum SendTimeoutError<T> {
Timeout(T), Timeout(T),
@@ -285,12 +278,10 @@ impl ProtocolClient {
cfg.clone(), cfg.clone(),
rx, rx,
voice_out_rx, voice_out_rx,
EventChannels { voice_in_tx,
voice_in: voice_in_tx, chat_tx,
chat: chat_tx, activity_tx,
activity: activity_tx, delta_tx,
delta: delta_tx,
},
ready_tx, ready_tx,
lost_tx, lost_tx,
)); ));
@@ -480,9 +471,6 @@ impl ProtocolClient {
} }
} }
/// Takes ownership of the delta receiver channel. Returns `None` if already
/// taken. Must be called exactly once during initialization to subscribe to
/// incremental state changes.
pub fn take_delta_rx(&self) -> Option<mpsc::Receiver<ProtocolDelta>> { pub fn take_delta_rx(&self) -> Option<mpsc::Receiver<ProtocolDelta>> {
self.delta_rx.lock().ok().and_then(|mut g| g.take()) self.delta_rx.lock().ok().and_then(|mut g| g.take())
} }
@@ -511,7 +499,10 @@ async fn connection_task(
cfg: ConnectConfig, cfg: ConnectConfig,
mut rx: mpsc::Receiver<Request>, mut rx: mpsc::Receiver<Request>,
mut voice_out_rx: mpsc::Receiver<OutPacket>, mut voice_out_rx: mpsc::Receiver<OutPacket>,
channels: EventChannels, voice_in_tx: mpsc::Sender<InboundVoice>,
chat_tx: mpsc::Sender<ChatMessage>,
activity_tx: mpsc::Sender<ServerActivity>,
delta_tx: mpsc::Sender<ProtocolDelta>,
ready_tx: oneshot::Sender<Result<(), ProtocolError>>, ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
lost_tx: oneshot::Sender<DisconnectReason>, lost_tx: oneshot::Sender<DisconnectReason>,
) { ) {
@@ -692,14 +683,14 @@ async fn connection_task(
Ok(Some(Ok(item))) => { Ok(Some(Ok(item))) => {
match item { match item {
StreamItem::Audio(buf) => { StreamItem::Audio(buf) => {
handle_audio_stream_item(&channels.voice_in, &mut voice_activity, buf).await; handle_audio_stream_item(&voice_in_tx, &mut voice_activity, buf).await;
} }
other => handle_non_audio_stream_item( other => handle_non_audio_stream_item(
&con, &con,
other, other,
&channels.chat, &chat_tx,
&channels.activity, &activity_tx,
&channels.delta, &delta_tx,
&mut pending_moves, &mut pending_moves,
), ),
} }
@@ -736,8 +727,10 @@ async fn connection_task(
}) })
.collect(); .collect();
for handle in expired { for handle in expired {
if let Some((_target_channel, Some(reply), _)) = pending_moves.remove(&handle) { if let Some((_target_channel, reply, _)) = pending_moves.remove(&handle) {
let _ = reply.send(Ok(())); if let Some(reply) = reply {
let _ = reply.send(Ok(()));
}
} }
} }
} }
@@ -798,7 +791,10 @@ async fn connection_task(
let r = fetch_client_profile( let r = fetch_client_profile(
&mut con, &mut con,
client_id, client_id,
&channels, &voice_in_tx,
&chat_tx,
&activity_tx,
&delta_tx,
&mut pending_moves, &mut pending_moves,
&mut voice_activity, &mut voice_activity,
) )
@@ -873,7 +869,7 @@ fn handle_non_audio_stream_item(
} = &ev } = &ev
{ {
let own_client = con.get_state().ok().map(|state| state.own_client); let own_client = con.get_state().ok().map(|state| state.own_client);
if let Ok(state) = con.get_state() { if let Some(state) = con.get_state().ok() {
if let Some(client) = state.clients.get(client_id) { if let Some(client) = state.clients.get(client_id) {
let _ = delta_tx.try_send(ProtocolDelta::ClientMoved { let _ = delta_tx.try_send(ProtocolDelta::ClientMoved {
client_id: client_id.0 as u64, client_id: client_id.0 as u64,
@@ -1119,7 +1115,10 @@ fn send_text_to_mode(
async fn fetch_client_profile( async fn fetch_client_profile(
con: &mut Connection, con: &mut Connection,
client_id: u64, client_id: u64,
channels: &EventChannels, voice_in_tx: &mpsc::Sender<InboundVoice>,
chat_tx: &mpsc::Sender<ChatMessage>,
activity_tx: &mpsc::Sender<ServerActivity>,
delta_tx: &mpsc::Sender<ProtocolDelta>,
pending_moves: &mut PendingMoves, pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>, voice_activity: &mut HashMap<u64, Instant>,
) -> Result<ClientProfile, ProtocolError> { ) -> Result<ClientProfile, ProtocolError> {
@@ -1156,7 +1155,10 @@ async fn fetch_client_profile(
let _ = request_messages( let _ = request_messages(
con, con,
build_command("servergrouplist", &[], &[]), build_command("servergrouplist", &[], &[]),
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1166,7 +1168,10 @@ async fn fetch_client_profile(
let _ = request_messages( let _ = request_messages(
con, con,
build_command("channelgrouplist", &[], &[]), build_command("channelgrouplist", &[], &[]),
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1180,7 +1185,10 @@ async fn fetch_client_profile(
&[("clid", client_id.to_string())], &[("clid", client_id.to_string())],
&[], &[],
), ),
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1198,7 +1206,10 @@ async fn fetch_client_profile(
if let Err(e) = request_messages( if let Err(e) = request_messages(
con, con,
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]), build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1217,7 +1228,10 @@ async fn fetch_client_profile(
request_client_db_info( request_client_db_info(
con, con,
database_id, database_id,
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1375,7 +1389,10 @@ fn client_profile_refresh_plan(
async fn request_messages( async fn request_messages(
con: &mut Connection, con: &mut Connection,
command: OutCommand, command: OutCommand,
channels: &EventChannels, voice_in_tx: &mpsc::Sender<InboundVoice>,
chat_tx: &mpsc::Sender<ChatMessage>,
activity_tx: &mpsc::Sender<ServerActivity>,
delta_tx: &mpsc::Sender<ProtocolDelta>,
pending_moves: &mut PendingMoves, pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>, voice_activity: &mut HashMap<u64, Instant>,
) -> Result<Vec<InMessage>, ProtocolError> { ) -> Result<Vec<InMessage>, ProtocolError> {
@@ -1411,14 +1428,14 @@ async fn request_messages(
return Ok(messages); return Ok(messages);
} }
StreamItem::Audio(buf) => { StreamItem::Audio(buf) => {
handle_audio_stream_item(&channels.voice_in, voice_activity, buf).await; handle_audio_stream_item(voice_in_tx, voice_activity, buf).await;
} }
other => handle_non_audio_stream_item( other => handle_non_audio_stream_item(
con, con,
other, other,
&channels.chat, chat_tx,
&channels.activity, activity_tx,
&channels.delta, delta_tx,
pending_moves, pending_moves,
), ),
} }
@@ -1428,14 +1445,20 @@ async fn request_messages(
async fn request_client_db_info( async fn request_client_db_info(
con: &mut Connection, con: &mut Connection,
dbid: tsclientlib::ClientDbId, dbid: tsclientlib::ClientDbId,
channels: &EventChannels, voice_in_tx: &mpsc::Sender<InboundVoice>,
chat_tx: &mpsc::Sender<ChatMessage>,
activity_tx: &mpsc::Sender<ServerActivity>,
delta_tx: &mpsc::Sender<ProtocolDelta>,
pending_moves: &mut PendingMoves, pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>, voice_activity: &mut HashMap<u64, Instant>,
) -> Result<InClientDbInfoPart, ProtocolError> { ) -> Result<InClientDbInfoPart, ProtocolError> {
let messages = request_messages( let messages = request_messages(
con, con,
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]), build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
channels, voice_in_tx,
chat_tx,
activity_tx,
delta_tx,
pending_moves, pending_moves,
voice_activity, voice_activity,
) )
@@ -1758,7 +1781,9 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) ->
extra, extra,
.. ..
} => { } => {
extra.reason?; if extra.reason.is_none() {
return None;
}
let client = activity_client(con, *client_id)?; let client = activity_client(con, *client_id)?;
let channel = activity_channel_name(con, client.channel)?; let channel = activity_channel_name(con, client.channel)?;
Some(format!( Some(format!(
@@ -2136,7 +2161,7 @@ fn forward_delta(
id: PropertyId::Client(client_id), id: PropertyId::Client(client_id),
.. ..
} => { } => {
if let Ok(state) = con.get_state() { if let Some(state) = con.get_state().ok() {
if let Some(client) = state.clients.get(client_id) { if let Some(client) = state.clients.get(client_id) {
let _ = delta_tx.try_send(ProtocolDelta::ClientJoined { let _ = delta_tx.try_send(ProtocolDelta::ClientJoined {
client_id: client_id.0 as u64, client_id: client_id.0 as u64,
@@ -2153,13 +2178,15 @@ fn forward_delta(
} }
Event::PropertyRemoved { Event::PropertyRemoved {
id: PropertyId::Client(_), id: PropertyId::Client(_),
old: PropertyValue::Client(client), old,
.. ..
} => { } => {
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft { if let PropertyValue::Client(client) = old {
client_id: client.id.0 as u64, let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
name: client.name.clone(), client_id: client.id.0 as u64,
}); name: client.name.clone(),
});
}
} }
Event::PropertyChanged { Event::PropertyChanged {
id: PropertyId::ClientChannel(_), id: PropertyId::ClientChannel(_),
@@ -2169,7 +2196,7 @@ fn forward_delta(
id: PropertyId::Client(client_id), id: PropertyId::Client(client_id),
.. ..
} => { } => {
if let Ok(state) = con.get_state() { if let Some(state) = con.get_state().ok() {
if let Some(client) = state.clients.get(client_id) { if let Some(client) = state.clients.get(client_id) {
let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated { let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated {
client_id: client_id.0 as u64, client_id: client_id.0 as u64,
@@ -2186,7 +2213,7 @@ fn forward_delta(
id: PropertyId::Channel(channel_id), id: PropertyId::Channel(channel_id),
.. ..
} => { } => {
if let Ok(state) = con.get_state() { if let Some(state) = con.get_state().ok() {
if let Some(channel) = state.channels.get(channel_id) { if let Some(channel) = state.channels.get(channel_id) {
let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded { let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded {
id: channel_id.0, id: channel_id.0,
@@ -2201,18 +2228,20 @@ fn forward_delta(
} }
Event::PropertyRemoved { Event::PropertyRemoved {
id: PropertyId::Channel(_), id: PropertyId::Channel(_),
old: PropertyValue::Channel(channel), old,
.. ..
} => { } => {
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved { if let PropertyValue::Channel(channel) = old {
id: channel.id.0, let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
}); id: channel.id.0,
});
}
} }
Event::PropertyChanged { Event::PropertyChanged {
id: PropertyId::Channel(channel_id), id: PropertyId::Channel(channel_id),
.. ..
} => { } => {
if let Ok(state) = con.get_state() { if let Some(state) = con.get_state().ok() {
if let Some(channel) = state.channels.get(channel_id) { if let Some(channel) = state.channels.get(channel_id) {
let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated { let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated {
id: channel_id.0, id: channel_id.0,
-41
View File
@@ -168,93 +168,52 @@ pub struct ServerSnapshot {
} }
impl ChannelId { impl ChannelId {
/// Root/top-level channel identifier.
pub const ROOT: ChannelId = ChannelId(0); pub const ROOT: ChannelId = ChannelId(0);
} }
/// Incremental state change emitted by the protocol adapter when the server tree changes.
///
/// Covers client joins, leaves, moves, and channel additions, removals, and updates.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProtocolDelta { pub enum ProtocolDelta {
/// A client moved to a different channel.
ClientMoved { ClientMoved {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Destination channel identifier.
new_channel_id: u64, new_channel_id: u64,
}, },
/// A new client appeared on the server.
ClientJoined { ClientJoined {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Channel the client joined.
channel_id: u64, channel_id: u64,
/// Display nickname.
name: String, name: String,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// Whether the client is a server query client.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A client disconnected.
ClientLeft { ClientLeft {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Display nickname.
name: String, name: String,
}, },
/// A client's properties changed.
ClientUpdated { ClientUpdated {
/// Unique client identifier.
client_id: u64, client_id: u64,
/// Microphone muted state.
input_muted: bool, input_muted: bool,
/// Speaker muted state.
output_muted: bool, output_muted: bool,
/// Whether the client is a server query client.
is_server_query: bool, is_server_query: bool,
/// Client's talk power value.
talk_power: i32, talk_power: i32,
/// Whether the server granted temporary talk power.
talk_power_granted: bool, talk_power_granted: bool,
}, },
/// A new channel appeared.
ChannelAdded { ChannelAdded {
/// Channel identifier.
id: u64, id: u64,
/// Parent channel identifier.
parent: u64, parent: u64,
/// Channel name.
name: String, name: String,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
order: i64, order: i64,
/// Whether the channel has a password.
has_password: bool, has_password: bool,
/// Needed talk power, or `None` when unrestricted.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
/// A channel was deleted.
ChannelRemoved { ChannelRemoved {
/// Channel identifier.
id: u64, id: u64,
}, },
/// Channel properties changed.
ChannelUpdated { ChannelUpdated {
/// Channel identifier.
id: u64, id: u64,
/// Channel name.
name: String, name: String,
/// Whether the channel has a password.
has_password: bool, has_password: bool,
/// Needed talk power, or `None` when unrestricted.
needed_talk_power: Option<i32>, needed_talk_power: Option<i32>,
}, },
} }
-6
View File
@@ -628,12 +628,6 @@ fn read_file_dek(path: &Path) -> Result<[u8; 32], StorageError> {
/// headless / sandboxed environments force the file-fallback path /// headless / sandboxed environments force the file-fallback path
/// without poking a real OS keyring (which would either prompt the /// without poking a real OS keyring (which would either prompt the
/// user or block on a missing D-Bus session). /// user or block on a missing D-Bus session).
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "ios"
))]
fn keyring_disabled() -> bool { fn keyring_disabled() -> bool {
matches!( matches!(
std::env::var("CHANORA_DISABLE_KEYRING").as_deref(), std::env::var("CHANORA_DISABLE_KEYRING").as_deref(),
+1 -210
View File
@@ -16,7 +16,7 @@ those terms.
| License | Crate count | | License | Crate count |
|---------|-------------| |---------|-------------|
| `Apache License 2.0` | 333 | | `Apache License 2.0` | 331 |
| `MIT License` | 74 | | `MIT License` | 74 |
| `Unicode License v3` | 19 | | `Unicode License v3` | 19 |
| `BSD 3-Clause &quot;New&quot; or &quot;Revised&quot; License` | 14 | | `BSD 3-Clause &quot;New&quot; or &quot;Revised&quot; License` | 14 |
@@ -117,7 +117,6 @@ those terms.
| pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> | | pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> |
| ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> | | ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> |
| rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> | | rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> |
| crossbeam | 0.8.4 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
| ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> | | ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> |
| rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> | | rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> |
| keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> | | keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> |
@@ -145,7 +144,6 @@ those terms.
| critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> | | critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> |
| crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> | | crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> | | crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
| crossbeam-queue | 0.3.12 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
| crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> | | crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
| dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> | | dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> |
| displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> | | displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> |
@@ -4995,213 +4993,6 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets &quot;[]&quot;
replaced with your own identifying information. (Don&#x27;t include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same &quot;printed page&quot; as the copyright notice for easier
identification within third-party archives.
Copyright 2019 The Crossbeam Project Developers
Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
### Apache License 2.0
```
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
&quot;License&quot; shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
&quot;Licensor&quot; shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
&quot;Legal Entity&quot; shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
&quot;control&quot; means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
&quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
exercising permissions granted by this License.
&quot;Source&quot; form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
&quot;Object&quot; form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
&quot;Work&quot; shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
&quot;Derivative Works&quot; shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
&quot;Contribution&quot; shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, &quot;submitted&quot;
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
&quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a &quot;NOTICE&quot; text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work. APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following To apply the Apache License to your work, attach the following