Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
861e915b81 | ||
|
|
5f1423c349 | ||
|
|
82441f3d97 | ||
|
|
2c7b68e21e | ||
|
|
12e3a1f4ee | ||
|
|
2902a8bcd5 | ||
|
|
fb2a8e0a80 |
@@ -117,6 +117,12 @@ opencode.json
|
||||
|
||||
# iOS framework build artifacts produced by chanora_bridge.podspec
|
||||
/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/
|
||||
AGENTS.md
|
||||
Screenshot 2026-05-17 at 22.23.07.png
|
||||
|
||||
Generated
+1
-1
@@ -419,12 +419,12 @@ name = "chanora_audio"
|
||||
version = "0.2.0-beta.1"
|
||||
dependencies = [
|
||||
"audiopus",
|
||||
"bytemuck",
|
||||
"chanora_protocol",
|
||||
"coreaudio-rs",
|
||||
"cpal",
|
||||
"criterion",
|
||||
"crossbeam",
|
||||
"crossbeam-utils",
|
||||
"dhat",
|
||||
"dispatch2",
|
||||
"futures-util",
|
||||
|
||||
@@ -63,8 +63,10 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
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 {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("com.android.application") version "8.13.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -1,69 +1,32 @@
|
||||
PODS:
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- chanora_bridge (1.0.0)
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_foreground_task (0.0.1):
|
||||
- Flutter
|
||||
- haptic_kit (1.0.0):
|
||||
- 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:
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- chanora_bridge (from `.`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/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:
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
chanora_bridge:
|
||||
:path: "."
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_foreground_task:
|
||||
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
||||
haptic_kit:
|
||||
: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:
|
||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||
chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
|
||||
PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -72,6 +73,7 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -79,6 +81,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
||||
1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
@@ -128,6 +131,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -216,6 +220,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
8C5000032DD0000000000001 /* SileroCoreML */,
|
||||
);
|
||||
productName = Runner;
|
||||
@@ -252,6 +257,7 @@
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
@@ -760,6 +766,10 @@
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = ../../../silero-coreml;
|
||||
};
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -768,6 +778,10 @@
|
||||
package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
||||
productName = SileroCoreML;
|
||||
};
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -298,5 +298,6 @@
|
||||
"clientVolumeMuteAction": "Mute user",
|
||||
"clientVolumeUnmuteAction": "Unmute user",
|
||||
"clientVolumeResetAction": "Reset to default",
|
||||
"permissionDenied": "Permission Denied"
|
||||
"permissionDenied": "Permission Denied",
|
||||
"voiceTalkPowerBlocked": "Insufficient talk power to speak in this channel"
|
||||
}
|
||||
|
||||
@@ -241,5 +241,6 @@
|
||||
"clientVolumeMuteAction": "静音该用户",
|
||||
"clientVolumeUnmuteAction": "取消静音",
|
||||
"clientVolumeResetAction": "恢复默认",
|
||||
"permissionDenied": "权限被拒绝"
|
||||
"permissionDenied": "权限被拒绝",
|
||||
"voiceTalkPowerBlocked": "发言权限不足,无法在此频道发言"
|
||||
}
|
||||
|
||||
@@ -1246,6 +1246,12 @@ abstract class AppL10n {
|
||||
/// In en, this message translates to:
|
||||
/// **'Permission Denied'**
|
||||
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> {
|
||||
|
||||
@@ -653,4 +653,8 @@ class AppL10nEn extends AppL10n {
|
||||
|
||||
@override
|
||||
String get permissionDenied => 'Permission Denied';
|
||||
|
||||
@override
|
||||
String get voiceTalkPowerBlocked =>
|
||||
'Insufficient talk power to speak in this channel';
|
||||
}
|
||||
|
||||
@@ -641,4 +641,7 @@ class AppL10nZh extends AppL10n {
|
||||
|
||||
@override
|
||||
String get permissionDenied => '权限被拒绝';
|
||||
|
||||
@override
|
||||
String get voiceTalkPowerBlocked => '发言权限不足,无法在此频道发言';
|
||||
}
|
||||
|
||||
@@ -312,6 +312,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
rust.BridgeSnapshot? _snapshot;
|
||||
final List<String> _uiDiagnostics = [];
|
||||
rust.BridgeAudioStats? _audioStats;
|
||||
double? _inputLevel;
|
||||
Timer? _statsTimer;
|
||||
bool _snapshotRefreshInFlight = false;
|
||||
bool _snapshotRefreshQueued = false;
|
||||
@@ -319,6 +320,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
bool _snapshotRefreshQueuedReportErrors = false;
|
||||
int _connectionEpoch = 0;
|
||||
StreamSubscription<rust.BridgeEvent>? _eventsSub;
|
||||
StreamSubscription<double>? _inputLevelSub;
|
||||
late final PrefetchDebouncer _prefetch;
|
||||
|
||||
// v1 voice subsystem state (SDD-094/095/096/097). Driven by
|
||||
@@ -383,8 +385,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// and Notifications permission service. On non-macOS hosts the service
|
||||
// short-circuits to "granted" / "unsupported" and never wires the
|
||||
// MethodChannel.
|
||||
final MacOSPermissionsService _macOSPermissions =
|
||||
MacOSPermissionsService();
|
||||
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService();
|
||||
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
||||
|
||||
@override
|
||||
@@ -395,6 +396,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer);
|
||||
_hostCtl.addListener(_onHostEdited);
|
||||
_eventsSub = rust.eventsStream().listen(_onEvent);
|
||||
_inputLevelSub = rust.inputLevelStream().listen((level) {
|
||||
if (mounted) setState(() => _inputLevel = level);
|
||||
});
|
||||
// SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged
|
||||
// events as early as possible so the listen-only banner reflects
|
||||
// the system state on first frame.
|
||||
@@ -412,10 +416,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// capability badge upgrades from L0Focused → L1MacOSEventTap
|
||||
// when the user grants the permission in System Settings.
|
||||
_macOSPermissions.start();
|
||||
_macOSPermissions.checkInitialStates();
|
||||
_macOSPermissions.pttCapabilityState.addListener(
|
||||
_onMacOSPttCapabilityChanged,
|
||||
);
|
||||
_macOSPermissions.checkInitialStates();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_requestRecordAudioOnStartup());
|
||||
});
|
||||
@@ -530,6 +534,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_pttLevel = level;
|
||||
if (level == 'L1MacOSEventTap') {
|
||||
_pttBackendId = 'macos-event-tap';
|
||||
} else if (level == 'L0Focused') {
|
||||
_pttBackendId = 'focused';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -670,7 +676,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_reconnectDelay = null;
|
||||
});
|
||||
case rust.BridgeEvent_Reconnecting(:final attempt, :final delaySecs):
|
||||
_recordUiDiagnostic('connection', 'reconnecting attempt=$attempt delay=${delaySecs}s');
|
||||
_recordUiDiagnostic(
|
||||
'connection',
|
||||
'reconnecting attempt=$attempt delay=${delaySecs}s',
|
||||
);
|
||||
setState(() {
|
||||
_phase = ConnectionPhase.reconnecting;
|
||||
_reconnectAttempt = attempt;
|
||||
@@ -845,23 +854,41 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
);
|
||||
return updated;
|
||||
});
|
||||
case rust.BridgeEvent_ClientJoined(:final clientId, :final channelId, :final name, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
|
||||
case rust.BridgeEvent_ClientJoined(
|
||||
:final clientId,
|
||||
:final channelId,
|
||||
:final name,
|
||||
:final inputMuted,
|
||||
:final outputMuted,
|
||||
:final isServerQuery,
|
||||
:final talkPower,
|
||||
:final talkPowerGranted,
|
||||
):
|
||||
if (!isServerQuery) {
|
||||
_applyClientAdd(rust.BridgeClient(
|
||||
id: clientId,
|
||||
channel: channelId,
|
||||
name: name,
|
||||
inputMuted: inputMuted,
|
||||
outputMuted: outputMuted,
|
||||
isSpeaking: false,
|
||||
isServerQuery: isServerQuery,
|
||||
talkPower: talkPower,
|
||||
talkPowerGranted: talkPowerGranted,
|
||||
));
|
||||
_applyClientAdd(
|
||||
rust.BridgeClient(
|
||||
id: clientId,
|
||||
channel: channelId,
|
||||
name: name,
|
||||
inputMuted: inputMuted,
|
||||
outputMuted: outputMuted,
|
||||
isSpeaking: false,
|
||||
isServerQuery: isServerQuery,
|
||||
talkPower: talkPower,
|
||||
talkPowerGranted: talkPowerGranted,
|
||||
),
|
||||
);
|
||||
}
|
||||
case rust.BridgeEvent_ClientLeft(:final clientId):
|
||||
_applyClientRemove(clientId);
|
||||
case rust.BridgeEvent_ClientUpdated(:final clientId, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
|
||||
case rust.BridgeEvent_ClientUpdated(
|
||||
:final clientId,
|
||||
:final inputMuted,
|
||||
:final outputMuted,
|
||||
:final isServerQuery,
|
||||
:final talkPower,
|
||||
:final talkPowerGranted,
|
||||
):
|
||||
_applyClientDelta((c) => c.id == clientId, (c) {
|
||||
final updated = rust.BridgeClient(
|
||||
id: c.id,
|
||||
@@ -876,18 +903,32 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
);
|
||||
return updated;
|
||||
});
|
||||
case rust.BridgeEvent_ChannelAdded(:final id, :final parent, :final name, :final order, :final hasPassword, :final neededTalkPower):
|
||||
_applyChannelAdd(rust.BridgeChannel(
|
||||
id: id,
|
||||
parent: parent,
|
||||
name: name,
|
||||
order: order,
|
||||
hasPassword: hasPassword,
|
||||
neededTalkPower: neededTalkPower,
|
||||
));
|
||||
case rust.BridgeEvent_ChannelAdded(
|
||||
:final id,
|
||||
:final parent,
|
||||
:final name,
|
||||
:final order,
|
||||
:final hasPassword,
|
||||
:final neededTalkPower,
|
||||
):
|
||||
_applyChannelAdd(
|
||||
rust.BridgeChannel(
|
||||
id: id,
|
||||
parent: parent,
|
||||
name: name,
|
||||
order: order,
|
||||
hasPassword: hasPassword,
|
||||
neededTalkPower: neededTalkPower,
|
||||
),
|
||||
);
|
||||
case rust.BridgeEvent_ChannelRemoved(:final id):
|
||||
_applyChannelRemove(id);
|
||||
case rust.BridgeEvent_ChannelUpdated(:final id, :final name, :final hasPassword, :final neededTalkPower):
|
||||
case rust.BridgeEvent_ChannelUpdated(
|
||||
:final id,
|
||||
:final name,
|
||||
:final hasPassword,
|
||||
:final neededTalkPower,
|
||||
):
|
||||
_applyChannelDelta((ch) => ch.id == id, (ch) {
|
||||
return rust.BridgeChannel(
|
||||
id: ch.id,
|
||||
@@ -925,7 +966,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
final now = DateTime.now();
|
||||
if (_inChannel &&
|
||||
(_lastSpeakingRefresh == null ||
|
||||
now.difference(_lastSpeakingRefresh!) >= _speakingRefreshInterval)) {
|
||||
now.difference(_lastSpeakingRefresh!) >=
|
||||
_speakingRefreshInterval)) {
|
||||
_lastSpeakingRefresh = now;
|
||||
unawaited(_refreshSnapshot(recordActivity: false, reportErrors: false));
|
||||
}
|
||||
@@ -955,6 +997,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
||||
_eventsSub?.cancel();
|
||||
_inputLevelSub?.cancel();
|
||||
_statsTimer?.cancel();
|
||||
_snapshotRefreshInFlight = false;
|
||||
_snapshotRefreshQueued = false;
|
||||
@@ -1126,14 +1169,16 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
|
||||
Future<void> _toggleOutputMute() async {
|
||||
final next = !_outputMuted;
|
||||
setState(() {
|
||||
_outputMuted = next;
|
||||
});
|
||||
try {
|
||||
await rust.setOutputMuted(muted: next);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_outputMuted = next;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_outputMuted = !next;
|
||||
});
|
||||
_showUiError('output mute', e);
|
||||
}
|
||||
}
|
||||
@@ -1260,6 +1305,16 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
return;
|
||||
}
|
||||
final next = !_hardMute;
|
||||
final previousInputMuted = _inputMuted;
|
||||
final previousHardMute = _hardMute;
|
||||
final previousPermissionMute = _hardMuteByPermission;
|
||||
setState(() {
|
||||
_inputMuted = next;
|
||||
_hardMute = next;
|
||||
if (next) {
|
||||
_hardMuteByPermission = false;
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Hard-mute is two coordinated effects:
|
||||
// * setHardMute — local TransmitGate clamp; we stop sending
|
||||
@@ -1272,14 +1327,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// through. Drive them together.
|
||||
await rust.setHardMute(muted: next);
|
||||
await rust.setInputMuted(muted: next);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_inputMuted = next;
|
||||
_hardMute = next;
|
||||
_hardMuteByPermission = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_inputMuted = previousInputMuted;
|
||||
_hardMute = previousHardMute;
|
||||
_hardMuteByPermission = previousPermissionMute;
|
||||
});
|
||||
_showUiError('hard mute', e);
|
||||
}
|
||||
}
|
||||
@@ -1737,7 +1791,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
};
|
||||
}
|
||||
|
||||
void _applyClientDelta(bool Function(rust.BridgeClient) test, rust.BridgeClient Function(rust.BridgeClient) update) {
|
||||
void _applyClientDelta(
|
||||
bool Function(rust.BridgeClient) test,
|
||||
rust.BridgeClient Function(rust.BridgeClient) update,
|
||||
) {
|
||||
final snap = _snapshot;
|
||||
if (snap == null) return;
|
||||
final clients = snap.clients.map((c) => test(c) ? update(c) : c).toList();
|
||||
@@ -1823,10 +1880,15 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
void _applyChannelDelta(bool Function(rust.BridgeChannel) test, rust.BridgeChannel Function(rust.BridgeChannel) update) {
|
||||
void _applyChannelDelta(
|
||||
bool Function(rust.BridgeChannel) test,
|
||||
rust.BridgeChannel Function(rust.BridgeChannel) update,
|
||||
) {
|
||||
final snap = _snapshot;
|
||||
if (snap == null) return;
|
||||
final channels = snap.channels.map((ch) => test(ch) ? update(ch) : ch).toList();
|
||||
final channels = snap.channels
|
||||
.map((ch) => test(ch) ? update(ch) : ch)
|
||||
.toList();
|
||||
setState(() {
|
||||
_snapshot = rust.BridgeSnapshot(
|
||||
serverName: snap.serverName,
|
||||
@@ -2145,26 +2207,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final headerActions = [
|
||||
if (_serverReachable && _inChannel) ...[
|
||||
IconButton(
|
||||
tooltip: _hardMuteByTalkPower
|
||||
? 'Insufficient talk power to speak in this channel'
|
||||
: l10n.voiceHardMuteLabel,
|
||||
icon: Icon(_hardMute ? Icons.mic_off : Icons.mic),
|
||||
isSelected: _hardMute,
|
||||
selectedIcon: const Icon(Icons.mic_off),
|
||||
color: _hardMute ? theme.colorScheme.error : null,
|
||||
onPressed: _hardMuteByTalkPower ? null : _onToggleHardMute,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceOutputMuteLabel,
|
||||
icon: Icon(_outputMuted ? Icons.headset_off : Icons.headset),
|
||||
isSelected: _outputMuted,
|
||||
selectedIcon: const Icon(Icons.headset_off),
|
||||
color: _outputMuted ? theme.colorScheme.error : null,
|
||||
onPressed: _toggleOutputMute,
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
tooltip: l10n.aboutAction,
|
||||
icon: const Icon(Icons.info_outline),
|
||||
@@ -2376,6 +2418,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_currentVoiceChannelId,
|
||||
),
|
||||
audioStats: _audioStats,
|
||||
inputLevel: _inputLevel,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
@@ -2457,10 +2500,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
isTouchOnly: isTouchOnlyPttHost,
|
||||
inputMuted: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
hardMuteByTalkPower: _hardMuteByTalkPower,
|
||||
talkPower: ownClientState?.talkPower,
|
||||
neededTalkPower: ownClientState?.neededTalkPower,
|
||||
talkPowerGranted: ownClientState?.talkPowerGranted,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
onToggleInputMute: _onToggleHardMute,
|
||||
onToggleOutputMute: _toggleOutputMute,
|
||||
),
|
||||
if (_inChannel &&
|
||||
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
|
||||
|
||||
@@ -309,12 +309,14 @@ class MacOSPermissionsService {
|
||||
_inputMonitoringState.value = state;
|
||||
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
||||
}
|
||||
break;
|
||||
case methodLocalNetworkStateChanged:
|
||||
final args = call.arguments;
|
||||
if (args is Map) {
|
||||
_localNetworkState.value =
|
||||
_parseLocalNetworkState(args['state'] as String?);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -261,6 +261,12 @@ Stream<BridgeEvent> eventsStream() =>
|
||||
Future<BridgeAudioStats> audioStats() =>
|
||||
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.
|
||||
Future<void> setAudioProcessingConfig({
|
||||
required BridgeAudioProcessingConfig config,
|
||||
@@ -681,15 +687,22 @@ class BridgeAudioStats {
|
||||
/// Current push-to-talk state.
|
||||
final bool pttActive;
|
||||
|
||||
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
|
||||
final double inputLevel;
|
||||
|
||||
const BridgeAudioStats({
|
||||
required this.framesSent,
|
||||
required this.framesReceived,
|
||||
required this.pttActive,
|
||||
required this.inputLevel,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
|
||||
framesSent.hashCode ^
|
||||
framesReceived.hashCode ^
|
||||
pttActive.hashCode ^
|
||||
inputLevel.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -698,7 +711,8 @@ class BridgeAudioStats {
|
||||
runtimeType == other.runtimeType &&
|
||||
framesSent == other.framesSent &&
|
||||
framesReceived == other.framesReceived &&
|
||||
pttActive == other.pttActive;
|
||||
pttActive == other.pttActive &&
|
||||
inputLevel == other.inputLevel;
|
||||
}
|
||||
|
||||
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
|
||||
|
||||
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => 281698435;
|
||||
int get rustContentHash => -20394775;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -123,6 +123,8 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
Future<void> crateApiInitStorage({required String dir});
|
||||
|
||||
Stream<double> crateApiInputLevelStream();
|
||||
|
||||
Future<bool> crateApiIsConnected();
|
||||
|
||||
Future<BridgeAudioDeviceList> crateApiListAudioDevices();
|
||||
@@ -762,6 +764,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiInitStorageConstMeta =>
|
||||
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
|
||||
Future<bool> crateApiIsConnected() {
|
||||
return handler.executeNormal(
|
||||
@@ -771,7 +805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 21,
|
||||
funcId: 22,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -798,7 +832,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 22,
|
||||
funcId: 23,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -825,7 +859,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 23,
|
||||
funcId: 24,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -849,7 +883,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -879,7 +913,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 25,
|
||||
funcId: 26,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -909,7 +943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 26,
|
||||
funcId: 27,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -936,7 +970,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 27,
|
||||
funcId: 28,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -961,7 +995,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(state, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -994,7 +1028,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 29,
|
||||
funcId: 30,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1021,7 +1055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bridge_audio_route(route, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -1055,7 +1089,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 31,
|
||||
funcId: 32,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1090,7 +1124,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 32,
|
||||
funcId: 33,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1120,7 +1154,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 33,
|
||||
funcId: 34,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1148,7 +1182,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 34,
|
||||
funcId: 35,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1176,7 +1210,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 35,
|
||||
funcId: 36,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1206,7 +1240,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 36,
|
||||
funcId: 37,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1234,7 +1268,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bridge_network_state(state, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -1260,7 +1294,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 38,
|
||||
funcId: 39,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1288,7 +1322,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 39,
|
||||
funcId: 40,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1316,7 +1350,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 40,
|
||||
funcId: 41,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1344,7 +1378,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 41,
|
||||
funcId: 42,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1376,7 +1410,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 42,
|
||||
funcId: 43,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1406,7 +1440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 43,
|
||||
funcId: 44,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1434,7 +1468,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 44,
|
||||
funcId: 45,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1462,7 +1496,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 45,
|
||||
funcId: 46,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1489,7 +1523,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 46,
|
||||
funcId: 47,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1517,7 +1551,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 47,
|
||||
funcId: 48,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1549,7 +1583,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 48,
|
||||
funcId: 49,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1578,7 +1612,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 49,
|
||||
funcId: 50,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1610,6 +1644,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
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
|
||||
String dco_decode_String(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -1781,12 +1821,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 3)
|
||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return BridgeAudioStats(
|
||||
framesSent: dco_decode_u_32(arr[0]),
|
||||
framesReceived: dco_decode_u_32(arr[1]),
|
||||
pttActive: dco_decode_bool(arr[2]),
|
||||
inputLevel: dco_decode_f_32(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2271,6 +2312,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
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
|
||||
String sse_decode_String(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -2488,10 +2537,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
var var_framesSent = sse_decode_u_32(deserializer);
|
||||
var var_framesReceived = sse_decode_u_32(deserializer);
|
||||
var var_pttActive = sse_decode_bool(deserializer);
|
||||
var var_inputLevel = sse_decode_f_32(deserializer);
|
||||
return BridgeAudioStats(
|
||||
framesSent: var_framesSent,
|
||||
framesReceived: var_framesReceived,
|
||||
pttActive: var_pttActive,
|
||||
inputLevel: var_inputLevel,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3199,6 +3250,23 @@ 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
|
||||
void sse_encode_String(String self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -3380,6 +3448,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_u_32(self.framesSent, serializer);
|
||||
sse_encode_u_32(self.framesReceived, serializer);
|
||||
sse_encode_bool(self.pttActive, serializer);
|
||||
sse_encode_f_32(self.inputLevel, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
|
||||
@@ -27,6 +27,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@@ -210,6 +213,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@@ -439,6 +447,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_f_32_Sse(
|
||||
RustStreamSink<double> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@@ -212,6 +215,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@@ -441,6 +449,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_f_32_Sse(
|
||||
RustStreamSink<double> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class VoiceBar extends StatelessWidget {
|
||||
required this.onConfigure,
|
||||
required this.onPttHeldChanged,
|
||||
this.talkPowerBlocked = false,
|
||||
this.inputLevel,
|
||||
});
|
||||
|
||||
final bool inChannel;
|
||||
@@ -53,6 +54,10 @@ class VoiceBar extends StatelessWidget {
|
||||
/// level meter. Pass `null` to render an idle meter.
|
||||
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
|
||||
/// [`PttCapabilityBadge`].
|
||||
final String pttLevel;
|
||||
@@ -196,7 +201,7 @@ class VoiceBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Row 4: level meter
|
||||
VoiceLevelMeter(active: levelActive),
|
||||
VoiceLevelMeter(active: levelActive, level: inputLevel ?? stats?.inputLevel),
|
||||
const SizedBox(height: 4),
|
||||
if (stats != null)
|
||||
Text(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// release-tail are surfaced inline (radio buttons + slider) inside
|
||||
// the modal.
|
||||
|
||||
import 'dart:async' show Timer, unawaited;
|
||||
import 'dart:async' show StreamSubscription, Timer, unawaited;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
@@ -53,8 +53,11 @@ class VoiceStatusChip extends StatelessWidget {
|
||||
required this.audioStats,
|
||||
required this.isTouchOnly,
|
||||
required this.onTap,
|
||||
required this.onToggleInputMute,
|
||||
required this.onToggleOutputMute,
|
||||
this.inputMuted = false,
|
||||
this.outputMuted = false,
|
||||
this.hardMuteByTalkPower = false,
|
||||
this.talkPower,
|
||||
this.neededTalkPower,
|
||||
this.talkPowerGranted,
|
||||
@@ -81,6 +84,9 @@ class VoiceStatusChip extends StatelessWidget {
|
||||
/// True when local speaker is muted.
|
||||
final bool outputMuted;
|
||||
|
||||
/// True when the server talk-power gate forces local hard mute.
|
||||
final bool hardMuteByTalkPower;
|
||||
|
||||
/// Own client's talk power.
|
||||
final int? talkPower;
|
||||
|
||||
@@ -93,6 +99,12 @@ class VoiceStatusChip extends StatelessWidget {
|
||||
/// Open the voice details modal.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// Toggle local input hard mute.
|
||||
final VoidCallback onToggleInputMute;
|
||||
|
||||
/// Toggle local output mute/deafen.
|
||||
final VoidCallback onToggleOutputMute;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -113,49 +125,48 @@ class VoiceStatusChip extends StatelessWidget {
|
||||
);
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
|
||||
hint: l10n.voiceSettingsTitle,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: ExcludeSemantics(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: summary.talkPowerBlocked
|
||||
? Colors.amber.withValues(alpha: 0.18)
|
||||
: summary.muted
|
||||
? theme.colorScheme.errorContainer.withValues(alpha: 0.35)
|
||||
: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: summary.talkPowerBlocked
|
||||
? Colors.amber.shade700
|
||||
: summary.muted
|
||||
? theme.colorScheme.error
|
||||
: theme.colorScheme.outlineVariant,
|
||||
width: summary.talkPowerBlocked || summary.muted ? 1.5 : 0.5,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: summary.talkPowerBlocked
|
||||
? Colors.amber.withValues(alpha: 0.18)
|
||||
: summary.muted
|
||||
? theme.colorScheme.errorContainer.withValues(alpha: 0.35)
|
||||
: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: summary.talkPowerBlocked
|
||||
? Colors.amber.shade700
|
||||
: summary.muted
|
||||
? theme.colorScheme.error
|
||||
: theme.colorScheme.outlineVariant,
|
||||
width: summary.talkPowerBlocked || summary.muted ? 1.5 : 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
summary.micOn
|
||||
? Icons.fiber_manual_record
|
||||
: Icons.fiber_manual_record_outlined,
|
||||
size: 12,
|
||||
color: summary.micOn
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
summary.micOn
|
||||
? Icons.fiber_manual_record
|
||||
: Icons.fiber_manual_record_outlined,
|
||||
size: 12,
|
||||
color: summary.micOn
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -179,15 +190,60 @@ class VoiceStatusChip extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.expand_less,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: hardMuteByTalkPower
|
||||
? l10n.voiceTalkPowerBlocked
|
||||
: l10n.voiceHardMuteLabel,
|
||||
icon: Icon(inputMuted ? Icons.mic_off : Icons.mic),
|
||||
color: inputMuted ? theme.colorScheme.error : null,
|
||||
onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceOutputMuteLabel,
|
||||
icon: Icon(outputMuted ? Icons.headset_off : Icons.headset),
|
||||
color: outputMuted ? theme.colorScheme.error : null,
|
||||
onPressed: onToggleOutputMute,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceSettingsTitle,
|
||||
icon: Icon(
|
||||
Icons.expand_less,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -455,6 +511,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
int _rateTickCount = 0;
|
||||
|
||||
late final AudioProcessingConfigState _audioProcessing;
|
||||
double? _streamLevel;
|
||||
StreamSubscription<double>? _levelSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -463,8 +521,12 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
widget.initialAudioConfig,
|
||||
);
|
||||
|
||||
// Poll audio stats at 250 ms so TX/RX counters and the level meter
|
||||
// update in real time while the sheet is open, independent of the parent.
|
||||
_levelSub = rust.inputLevelStream().listen((level) {
|
||||
if (mounted) setState(() => _streamLevel = level);
|
||||
});
|
||||
|
||||
// Poll audio stats at 250 ms so TX/RX counters update in real time
|
||||
// while the sheet is open.
|
||||
_statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async {
|
||||
try {
|
||||
final s = await rust.audioStats();
|
||||
@@ -472,7 +534,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
setState(() {
|
||||
_stats = s;
|
||||
_rateTickCount++;
|
||||
// Compute rates every ~1 s (4 × 250 ms).
|
||||
if (_rateTickCount >= 4) {
|
||||
_txRate = s.framesSent - _prevSent;
|
||||
_rxRate = s.framesReceived - _prevReceived;
|
||||
@@ -487,6 +548,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_levelSub?.cancel();
|
||||
_statsTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -625,7 +687,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 4) Level meter + live TX/RX stats.
|
||||
VoiceLevelMeter(active: levelActive),
|
||||
VoiceLevelMeter(active: levelActive, level: _streamLevel ?? stats?.inputLevel),
|
||||
const SizedBox(height: 6),
|
||||
_StatsRow(
|
||||
txRate: _txRate,
|
||||
|
||||
@@ -1,28 +1,78 @@
|
||||
import 'dart:math' show max;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shared compact level meter used by voice surfaces.
|
||||
class VoiceLevelMeter extends StatelessWidget {
|
||||
const VoiceLevelMeter({super.key, required this.active});
|
||||
///
|
||||
/// When [level] is null (no stats available yet), falls back to [active]
|
||||
/// for a binary indicator. When [level] is provided it is interpreted as
|
||||
/// dBFS and mapped to a 0–1 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;
|
||||
|
||||
/// 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
|
||||
Widget build(BuildContext 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(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
widthFactor: active ? 0.75 : 0.05,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: begin, end: fill),
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
builder: (context, animatedFill, child) {
|
||||
return FractionallySizedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
widthFactor: max(animatedFill, 0.02),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outlineVariant,
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Resources
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
<?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>
|
||||
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
A
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/chanora_bridge
|
||||
@@ -1,57 +1,20 @@
|
||||
PODS:
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- chanora_bridge (1.0.0)
|
||||
- connectivity_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- 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:
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/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`)
|
||||
- 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:
|
||||
audio_session:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
chanora_bridge:
|
||||
:path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos"
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
FlutterMacOS:
|
||||
: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:
|
||||
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
||||
chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103
|
||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
|
||||
PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; };
|
||||
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 */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -93,6 +94,7 @@
|
||||
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; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -108,6 +110,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
8C6000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
||||
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
@@ -170,6 +173,7 @@
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
@@ -256,6 +260,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
8C6000032DD0000000000001 /* SileroCoreML */,
|
||||
);
|
||||
productName = Runner;
|
||||
@@ -302,6 +307,7 @@
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
||||
);
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
@@ -865,6 +871,10 @@
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = ../../../silero-coreml;
|
||||
};
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -873,6 +883,10 @@
|
||||
package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
||||
productName = SileroCoreML;
|
||||
};
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "chanora_flutter.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -48,6 +48,7 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
|
||||
// -- Input Monitoring ---------------------------------------------------
|
||||
case "checkInputMonitoring":
|
||||
result(inputMonitoringStateString())
|
||||
startInputMonitoringPolling()
|
||||
|
||||
case "requestInputMonitoring":
|
||||
requestInputMonitoring(result: result)
|
||||
|
||||
@@ -457,10 +457,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -790,10 +790,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -493,6 +493,7 @@ void main() {
|
||||
framesSent: 1,
|
||||
framesReceived: 0,
|
||||
pttActive: true,
|
||||
inputLevel: -30.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -253,49 +253,87 @@ pub enum SessionEvent {
|
||||
},
|
||||
/// Audio route changed (speaker/earpiece/BT/wired headset).
|
||||
AudioRouteChanged {
|
||||
/// New audio output route.
|
||||
route: AudioRoute,
|
||||
},
|
||||
/// A client moved to a different channel.
|
||||
ClientMoved {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Destination channel.
|
||||
new_channel_id: u64,
|
||||
},
|
||||
/// A new client connected.
|
||||
ClientJoined {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Channel the client joined.
|
||||
channel_id: u64,
|
||||
/// Display nickname.
|
||||
name: String,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// Whether this is a server query (bot) client.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A client disconnected.
|
||||
ClientLeft {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Display nickname at time of disconnect.
|
||||
name: String,
|
||||
},
|
||||
/// Client properties changed.
|
||||
ClientUpdated {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// Whether this is a server query (bot) client.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A new channel appeared.
|
||||
ChannelAdded {
|
||||
/// Unique channel identifier.
|
||||
id: u64,
|
||||
/// Parent channel ID.
|
||||
parent: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
||||
/// linked-list ordering hint). Zero means first child.
|
||||
order: i64,
|
||||
/// Whether the channel requires a password.
|
||||
has_password: bool,
|
||||
/// Talk power required to speak, or `None` when unrestricted.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
/// A channel was deleted.
|
||||
ChannelRemoved {
|
||||
/// Channel identifier.
|
||||
id: u64,
|
||||
},
|
||||
/// Channel properties changed.
|
||||
ChannelUpdated {
|
||||
/// Unique channel identifier.
|
||||
id: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Whether the channel requires a password.
|
||||
has_password: bool,
|
||||
/// Talk power required to speak, or `None` when unrestricted.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
}
|
||||
@@ -1307,8 +1345,8 @@ impl ChanoraSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read audio engine statistics: (frames_sent, frames_received, transmit_active).
|
||||
pub async fn audio_stats(&self) -> Result<(u32, u32, bool), CoreError> {
|
||||
/// Read audio engine statistics: (frames_sent, frames_received, transmit_active, input_level_dbfs).
|
||||
pub async fn audio_stats(&self) -> Result<(u32, u32, bool, f32), CoreError> {
|
||||
let guard = self.inner.lock().await;
|
||||
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
|
||||
let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?;
|
||||
@@ -1316,6 +1354,7 @@ impl ChanoraSession {
|
||||
audio.frames_sent(),
|
||||
audio.frames_received(),
|
||||
audio.transmit_active(),
|
||||
audio.input_level(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2399,6 +2438,7 @@ async fn supervisor_loop(ctx: SupervisorContext) {
|
||||
/// the UI would render. Two snapshots with identical channel
|
||||
/// memberships, names, and orderings produce the same signature;
|
||||
/// any in-channel move, rename, or reorder produces a different one.
|
||||
#[cfg(test)]
|
||||
fn snapshot_signature(snap: &ServerSnapshot) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
@@ -30,7 +30,6 @@ tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2
|
||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||
rustfft = "6.2.0"
|
||||
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]
|
||||
# Desktop audio I/O for Windows capture/playback and Linux capture.
|
||||
@@ -74,6 +73,11 @@ ndk-context = "0.1"
|
||||
# - deduplicated macro impls
|
||||
# - PowerSavingOffloaded PerformanceMode variant
|
||||
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]
|
||||
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices
|
||||
|
||||
@@ -523,7 +523,7 @@ impl AudioInputCallback for InputCallback {
|
||||
|
||||
struct OutputCallback {
|
||||
handler: AudioHandler<SessionAudioId>,
|
||||
event_queue: Arc<AudioEventQueue>,
|
||||
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
||||
output_gain: Arc<AtomicU32>,
|
||||
output_muted: Arc<AtomicBool>,
|
||||
event_tx: BackendEventTx,
|
||||
@@ -542,14 +542,12 @@ impl AudioOutputCallback for OutputCallback {
|
||||
frames: &mut [(f32, f32)],
|
||||
) -> DataCallbackResult {
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||
let buf: &mut [f32] = unsafe {
|
||||
std::slice::from_raw_parts_mut(frames.as_mut_ptr() as *mut f32, frames.len() * 2)
|
||||
};
|
||||
let buf: &mut [f32] =
|
||||
bytemuck::cast_slice_mut::<(f32, f32), f32>(frames);
|
||||
for s in buf.iter_mut() {
|
||||
*s = 0.0;
|
||||
}
|
||||
let consumer = AudioEventQueue::consumer(&self.event_queue);
|
||||
for cmd in consumer.drain_controls() {
|
||||
for cmd in self.event_consumer.drain_controls() {
|
||||
match cmd {
|
||||
AudioCommand::SetVolume(id, vol) => {
|
||||
if let Some(q) = self.handler.get_mut_queues().get_mut(&id) {
|
||||
@@ -562,7 +560,7 @@ impl AudioOutputCallback for OutputCallback {
|
||||
}
|
||||
}
|
||||
|
||||
for pkt in consumer.drain_packets(50) {
|
||||
for pkt in self.event_consumer.drain_packets(50) {
|
||||
if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
}
|
||||
@@ -793,7 +791,7 @@ impl AndroidVoiceUnit {
|
||||
let event_queue = params.event_producer.queue();
|
||||
let output_cb = OutputCallback {
|
||||
handler: params.handler,
|
||||
event_queue: event_queue.clone(),
|
||||
event_consumer: AudioEventQueue::consumer(&event_queue),
|
||||
output_gain: params.output_gain.clone(),
|
||||
output_muted: params.output_muted.clone(),
|
||||
event_tx: event_tx.clone(),
|
||||
@@ -816,7 +814,7 @@ impl AndroidVoiceUnit {
|
||||
cfg,
|
||||
&event_tx,
|
||||
AudioHandler::new(),
|
||||
event_queue.clone(),
|
||||
AudioEventQueue::consumer(&event_queue),
|
||||
params.output_gain.clone(),
|
||||
params.output_muted.clone(),
|
||||
audio_processing_stats.clone(),
|
||||
@@ -1069,7 +1067,7 @@ impl AndroidVoiceUnit {
|
||||
cfg: &AndroidVoiceStreamConfig,
|
||||
event_tx: &BackendEventTx,
|
||||
handler: AudioHandler<SessionAudioId>,
|
||||
event_queue: Arc<AudioEventQueue>,
|
||||
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
||||
output_gain: Arc<AtomicU32>,
|
||||
output_muted: Arc<AtomicBool>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
@@ -1077,7 +1075,7 @@ impl AndroidVoiceUnit {
|
||||
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
|
||||
let cb = OutputCallback {
|
||||
handler,
|
||||
event_queue,
|
||||
event_consumer,
|
||||
output_gain,
|
||||
output_muted,
|
||||
event_tx: event_tx.clone(),
|
||||
|
||||
@@ -22,6 +22,8 @@ pub enum AudioCommand {
|
||||
/// Set a client's output volume.
|
||||
SetVolume(SessionAudioId, f32),
|
||||
/// Remove a client's decode queue.
|
||||
// TODO: Wire to client disconnect path; handled in callback but no
|
||||
// producer currently pushes this command.
|
||||
RemoveClient(SessionAudioId),
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +404,19 @@ impl Default for 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.
|
||||
pub fn update_capture(
|
||||
&self,
|
||||
|
||||
@@ -838,6 +838,7 @@ impl AudioEngine {
|
||||
transmit_flag_for_capture,
|
||||
frames_sent.clone(),
|
||||
cfg.mic_gain,
|
||||
audio_processing_stats.clone(),
|
||||
);
|
||||
let (input_stream, capture_active) = match capture_result {
|
||||
Ok(s) => (Some(s), true),
|
||||
@@ -1597,6 +1598,11 @@ impl AudioEngine {
|
||||
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.
|
||||
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
|
||||
self.audio_processing_config.lock().unwrap().clone()
|
||||
@@ -1664,15 +1670,21 @@ impl AudioEngine {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
|
||||
loop {
|
||||
for _ in 0..64 {
|
||||
match self.audio_event_producer.push_control(cmd) {
|
||||
Ok(()) => break,
|
||||
Ok(()) => return,
|
||||
Err(returned) => {
|
||||
cmd = returned;
|
||||
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"))]
|
||||
{
|
||||
@@ -1711,6 +1723,7 @@ fn try_open_capture(
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
mic_gain: f32,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
) -> Result<cpal::Stream, AudioError> {
|
||||
let in_cfg = in_dev
|
||||
.default_input_config()
|
||||
@@ -1746,6 +1759,7 @@ fn try_open_capture(
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
frames_sent,
|
||||
audio_processing_stats,
|
||||
)));
|
||||
|
||||
let stream = match in_format {
|
||||
@@ -1797,6 +1811,7 @@ struct CaptureState {
|
||||
/// capacity so the drain-into-frame path skips the allocator
|
||||
/// after warmup. Same precedent as `mono_scratch` above.
|
||||
frame_scratch: Vec<f32>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
@@ -1809,6 +1824,7 @@ impl CaptureState {
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
) -> Self {
|
||||
Self {
|
||||
encoder,
|
||||
@@ -1822,12 +1838,9 @@ impl CaptureState {
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
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),
|
||||
// Exact upper bound: drain pulls FRAME_SAMPLES at a time.
|
||||
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
|
||||
audio_processing_stats,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1835,17 +1848,8 @@ impl CaptureState {
|
||||
/// 48 kHz mono frames; encode and send when `transmit_active`
|
||||
/// is true (PTT engaged).
|
||||
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
||||
if !self.transmit_active.load(Ordering::Relaxed) {
|
||||
// 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.
|
||||
// 1. Down-mix to mono (pre-gain). Always performed so the level
|
||||
// meter reflects real mic input even when PTT is released.
|
||||
let in_channels = self.in_channels;
|
||||
let mic_gain = self.mic_gain;
|
||||
self.mono_scratch.clear();
|
||||
@@ -1853,8 +1857,23 @@ impl CaptureState {
|
||||
self.mono_scratch.reserve(frame_count);
|
||||
for frame in buf.chunks(in_channels) {
|
||||
let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum();
|
||||
self.mono_scratch
|
||||
.push((sum / frame.len() as f32) * mic_gain);
|
||||
self.mono_scratch.push(sum / frame.len() as f32);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2482,6 +2501,7 @@ pub mod bench_seam {
|
||||
tx,
|
||||
transmit_active.clone(),
|
||||
frames_sent,
|
||||
Arc::new(crate::SharedAudioProcessingStats::default()),
|
||||
);
|
||||
Self {
|
||||
state,
|
||||
|
||||
@@ -22,9 +22,6 @@ use std::sync::atomic::{AtomicBool, AtomicU32};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tsclientlib::audio::AudioHandler;
|
||||
#[cfg(target_os = "android")]
|
||||
use tsclientlib::audio::AudioHandler;
|
||||
|
||||
use crate::engine::SessionAudioId;
|
||||
|
||||
@@ -1038,6 +1038,8 @@ pub struct BridgeAudioStats {
|
||||
pub frames_received: u32,
|
||||
/// Current push-to-talk state.
|
||||
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.
|
||||
@@ -1701,49 +1703,87 @@ pub enum BridgeEvent {
|
||||
},
|
||||
/// Audio route changed (speaker/earpiece/BT/wired).
|
||||
AudioRouteChanged {
|
||||
/// New audio output route.
|
||||
route: BridgeAudioRoute,
|
||||
},
|
||||
/// A client moved to a different channel.
|
||||
ClientMoved {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Destination channel.
|
||||
new_channel_id: u64,
|
||||
},
|
||||
/// A new client connected.
|
||||
ClientJoined {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Channel the client joined.
|
||||
channel_id: u64,
|
||||
/// Display nickname.
|
||||
name: String,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// True for server query (bot) clients.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A client disconnected.
|
||||
ClientLeft {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Display nickname at time of disconnect.
|
||||
name: String,
|
||||
},
|
||||
/// Client properties changed.
|
||||
ClientUpdated {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// True for server query (bot) clients.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A new channel appeared.
|
||||
ChannelAdded {
|
||||
/// Unique channel identifier.
|
||||
id: u64,
|
||||
/// Parent channel ID.
|
||||
parent: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
||||
/// linked-list ordering hint). Zero means first child.
|
||||
order: i64,
|
||||
/// Whether the channel requires a password.
|
||||
has_password: bool,
|
||||
/// Talk power required to speak; `None` means no restriction.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
/// A channel was deleted.
|
||||
ChannelRemoved {
|
||||
/// Channel identifier.
|
||||
id: u64,
|
||||
},
|
||||
/// Channel properties changed.
|
||||
ChannelUpdated {
|
||||
/// Unique channel identifier.
|
||||
id: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Whether the channel requires a password.
|
||||
has_password: bool,
|
||||
/// Talk power required to speak; `None` means no restriction.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
}
|
||||
@@ -2058,7 +2098,7 @@ pub fn events_stream(sink: StreamSink<BridgeEvent>) -> Result<(), BridgeError> {
|
||||
|
||||
/// Read audio statistics. Errors if no connection or audio not started.
|
||||
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
||||
let (s, r, p) = runtime()
|
||||
let (s, r, p, lvl) = runtime()
|
||||
.spawn(async { session().audio_stats().await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("audio_stats", e))??;
|
||||
@@ -2066,9 +2106,42 @@ pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
||||
frames_sent: s,
|
||||
frames_received: r,
|
||||
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.
|
||||
pub async fn set_audio_processing_config(
|
||||
config: BridgeAudioProcessingConfig,
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 281698435;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -20394775;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -738,6 +738,42 @@ 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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -1790,6 +1826,14 @@ 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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1962,10 +2006,12 @@ impl SseDecode for crate::api::BridgeAudioStats {
|
||||
let mut var_framesSent = <u32>::sse_decode(deserializer);
|
||||
let mut var_framesReceived = <u32>::sse_decode(deserializer);
|
||||
let mut var_pttActive = <bool>::sse_decode(deserializer);
|
||||
let mut var_inputLevel = <f32>::sse_decode(deserializer);
|
||||
return crate::api::BridgeAudioStats {
|
||||
frames_sent: var_framesSent,
|
||||
frames_received: var_framesReceived,
|
||||
ptt_active: var_pttActive,
|
||||
input_level: var_inputLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2755,33 +2801,34 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
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),
|
||||
20 => wire__crate__api__init_storage_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__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),
|
||||
25 => 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__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),
|
||||
31 => 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_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_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
36 => {
|
||||
21 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
|
||||
32 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
||||
33 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
|
||||
34 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
35 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
|
||||
36 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
37 => {
|
||||
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
38 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
|
||||
39 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
40 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
41 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
42 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
43 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
44 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
45 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
|
||||
46 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
47 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
48 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
49 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
39 => 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),
|
||||
41 => 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),
|
||||
43 => 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),
|
||||
45 => 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),
|
||||
47 => 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),
|
||||
49 => 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),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2850,10 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
data_len,
|
||||
),
|
||||
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
|
||||
37 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
25 => 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),
|
||||
31 => 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),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -2984,6 +3031,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioStats {
|
||||
self.frames_sent.into_into_dart().into_dart(),
|
||||
self.frames_received.into_into_dart().into_dart(),
|
||||
self.ptt_active.into_into_dart().into_dart(),
|
||||
self.input_level.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -3652,6 +3700,13 @@ 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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -3781,6 +3836,7 @@ impl SseEncode for crate::api::BridgeAudioStats {
|
||||
<u32>::sse_encode(self.frames_sent, serializer);
|
||||
<u32>::sse_encode(self.frames_received, serializer);
|
||||
<bool>::sse_encode(self.ptt_active, serializer);
|
||||
<f32>::sse_encode(self.input_level, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,13 @@ 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)]
|
||||
enum SendTimeoutError<T> {
|
||||
Timeout(T),
|
||||
@@ -278,10 +285,12 @@ impl ProtocolClient {
|
||||
cfg.clone(),
|
||||
rx,
|
||||
voice_out_rx,
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
EventChannels {
|
||||
voice_in: voice_in_tx,
|
||||
chat: chat_tx,
|
||||
activity: activity_tx,
|
||||
delta: delta_tx,
|
||||
},
|
||||
ready_tx,
|
||||
lost_tx,
|
||||
));
|
||||
@@ -471,6 +480,9 @@ 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>> {
|
||||
self.delta_rx.lock().ok().and_then(|mut g| g.take())
|
||||
}
|
||||
@@ -499,10 +511,7 @@ async fn connection_task(
|
||||
cfg: ConnectConfig,
|
||||
mut rx: mpsc::Receiver<Request>,
|
||||
mut voice_out_rx: mpsc::Receiver<OutPacket>,
|
||||
voice_in_tx: mpsc::Sender<InboundVoice>,
|
||||
chat_tx: mpsc::Sender<ChatMessage>,
|
||||
activity_tx: mpsc::Sender<ServerActivity>,
|
||||
delta_tx: mpsc::Sender<ProtocolDelta>,
|
||||
channels: EventChannels,
|
||||
ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
|
||||
lost_tx: oneshot::Sender<DisconnectReason>,
|
||||
) {
|
||||
@@ -683,14 +692,14 @@ async fn connection_task(
|
||||
Ok(Some(Ok(item))) => {
|
||||
match item {
|
||||
StreamItem::Audio(buf) => {
|
||||
handle_audio_stream_item(&voice_in_tx, &mut voice_activity, buf).await;
|
||||
handle_audio_stream_item(&channels.voice_in, &mut voice_activity, buf).await;
|
||||
}
|
||||
other => handle_non_audio_stream_item(
|
||||
&con,
|
||||
other,
|
||||
&chat_tx,
|
||||
&activity_tx,
|
||||
&delta_tx,
|
||||
&channels.chat,
|
||||
&channels.activity,
|
||||
&channels.delta,
|
||||
&mut pending_moves,
|
||||
),
|
||||
}
|
||||
@@ -727,10 +736,8 @@ async fn connection_task(
|
||||
})
|
||||
.collect();
|
||||
for handle in expired {
|
||||
if let Some((_target_channel, reply, _)) = pending_moves.remove(&handle) {
|
||||
if let Some(reply) = reply {
|
||||
let _ = reply.send(Ok(()));
|
||||
}
|
||||
if let Some((_target_channel, Some(reply), _)) = pending_moves.remove(&handle) {
|
||||
let _ = reply.send(Ok(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,10 +798,7 @@ async fn connection_task(
|
||||
let r = fetch_client_profile(
|
||||
&mut con,
|
||||
client_id,
|
||||
&voice_in_tx,
|
||||
&chat_tx,
|
||||
&activity_tx,
|
||||
&delta_tx,
|
||||
&channels,
|
||||
&mut pending_moves,
|
||||
&mut voice_activity,
|
||||
)
|
||||
@@ -869,7 +873,7 @@ fn handle_non_audio_stream_item(
|
||||
} = &ev
|
||||
{
|
||||
let own_client = con.get_state().ok().map(|state| state.own_client);
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientMoved {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -1115,10 +1119,7 @@ fn send_text_to_mode(
|
||||
async fn fetch_client_profile(
|
||||
con: &mut Connection,
|
||||
client_id: u64,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<ClientProfile, ProtocolError> {
|
||||
@@ -1155,10 +1156,7 @@ async fn fetch_client_profile(
|
||||
let _ = request_messages(
|
||||
con,
|
||||
build_command("servergrouplist", &[], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1168,10 +1166,7 @@ async fn fetch_client_profile(
|
||||
let _ = request_messages(
|
||||
con,
|
||||
build_command("channelgrouplist", &[], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1185,10 +1180,7 @@ async fn fetch_client_profile(
|
||||
&[("clid", client_id.to_string())],
|
||||
&[],
|
||||
),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1206,10 +1198,7 @@ async fn fetch_client_profile(
|
||||
if let Err(e) = request_messages(
|
||||
con,
|
||||
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1228,10 +1217,7 @@ async fn fetch_client_profile(
|
||||
request_client_db_info(
|
||||
con,
|
||||
database_id,
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1389,10 +1375,7 @@ fn client_profile_refresh_plan(
|
||||
async fn request_messages(
|
||||
con: &mut Connection,
|
||||
command: OutCommand,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<Vec<InMessage>, ProtocolError> {
|
||||
@@ -1428,14 +1411,14 @@ async fn request_messages(
|
||||
return Ok(messages);
|
||||
}
|
||||
StreamItem::Audio(buf) => {
|
||||
handle_audio_stream_item(voice_in_tx, voice_activity, buf).await;
|
||||
handle_audio_stream_item(&channels.voice_in, voice_activity, buf).await;
|
||||
}
|
||||
other => handle_non_audio_stream_item(
|
||||
con,
|
||||
other,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
&channels.chat,
|
||||
&channels.activity,
|
||||
&channels.delta,
|
||||
pending_moves,
|
||||
),
|
||||
}
|
||||
@@ -1445,20 +1428,14 @@ async fn request_messages(
|
||||
async fn request_client_db_info(
|
||||
con: &mut Connection,
|
||||
dbid: tsclientlib::ClientDbId,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<InClientDbInfoPart, ProtocolError> {
|
||||
let messages = request_messages(
|
||||
con,
|
||||
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1781,9 +1758,7 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) ->
|
||||
extra,
|
||||
..
|
||||
} => {
|
||||
if extra.reason.is_none() {
|
||||
return None;
|
||||
}
|
||||
extra.reason?;
|
||||
let client = activity_client(con, *client_id)?;
|
||||
let channel = activity_channel_name(con, client.channel)?;
|
||||
Some(format!(
|
||||
@@ -2161,7 +2136,7 @@ fn forward_delta(
|
||||
id: PropertyId::Client(client_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientJoined {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -2178,15 +2153,13 @@ fn forward_delta(
|
||||
}
|
||||
Event::PropertyRemoved {
|
||||
id: PropertyId::Client(_),
|
||||
old,
|
||||
old: PropertyValue::Client(client),
|
||||
..
|
||||
} => {
|
||||
if let PropertyValue::Client(client) = old {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
||||
client_id: client.id.0 as u64,
|
||||
name: client.name.clone(),
|
||||
});
|
||||
}
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
||||
client_id: client.id.0 as u64,
|
||||
name: client.name.clone(),
|
||||
});
|
||||
}
|
||||
Event::PropertyChanged {
|
||||
id: PropertyId::ClientChannel(_),
|
||||
@@ -2196,7 +2169,7 @@ fn forward_delta(
|
||||
id: PropertyId::Client(client_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -2213,7 +2186,7 @@ fn forward_delta(
|
||||
id: PropertyId::Channel(channel_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(channel) = state.channels.get(channel_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded {
|
||||
id: channel_id.0,
|
||||
@@ -2228,20 +2201,18 @@ fn forward_delta(
|
||||
}
|
||||
Event::PropertyRemoved {
|
||||
id: PropertyId::Channel(_),
|
||||
old,
|
||||
old: PropertyValue::Channel(channel),
|
||||
..
|
||||
} => {
|
||||
if let PropertyValue::Channel(channel) = old {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
||||
id: channel.id.0,
|
||||
});
|
||||
}
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
||||
id: channel.id.0,
|
||||
});
|
||||
}
|
||||
Event::PropertyChanged {
|
||||
id: PropertyId::Channel(channel_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(channel) = state.channels.get(channel_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated {
|
||||
id: channel_id.0,
|
||||
|
||||
@@ -168,52 +168,93 @@ pub struct ServerSnapshot {
|
||||
}
|
||||
|
||||
impl ChannelId {
|
||||
/// Root/top-level channel identifier.
|
||||
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)]
|
||||
pub enum ProtocolDelta {
|
||||
/// A client moved to a different channel.
|
||||
ClientMoved {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Destination channel identifier.
|
||||
new_channel_id: u64,
|
||||
},
|
||||
/// A new client appeared on the server.
|
||||
ClientJoined {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Channel the client joined.
|
||||
channel_id: u64,
|
||||
/// Display nickname.
|
||||
name: String,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// Whether the client is a server query client.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A client disconnected.
|
||||
ClientLeft {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Display nickname.
|
||||
name: String,
|
||||
},
|
||||
/// A client's properties changed.
|
||||
ClientUpdated {
|
||||
/// Unique client identifier.
|
||||
client_id: u64,
|
||||
/// Microphone muted state.
|
||||
input_muted: bool,
|
||||
/// Speaker muted state.
|
||||
output_muted: bool,
|
||||
/// Whether the client is a server query client.
|
||||
is_server_query: bool,
|
||||
/// Client's talk power value.
|
||||
talk_power: i32,
|
||||
/// Whether the server granted temporary talk power.
|
||||
talk_power_granted: bool,
|
||||
},
|
||||
/// A new channel appeared.
|
||||
ChannelAdded {
|
||||
/// Channel identifier.
|
||||
id: u64,
|
||||
/// Parent channel identifier.
|
||||
parent: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
||||
/// linked-list ordering hint). Zero means first child.
|
||||
order: i64,
|
||||
/// Whether the channel has a password.
|
||||
has_password: bool,
|
||||
/// Needed talk power, or `None` when unrestricted.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
/// A channel was deleted.
|
||||
ChannelRemoved {
|
||||
/// Channel identifier.
|
||||
id: u64,
|
||||
},
|
||||
/// Channel properties changed.
|
||||
ChannelUpdated {
|
||||
/// Channel identifier.
|
||||
id: u64,
|
||||
/// Channel name.
|
||||
name: String,
|
||||
/// Whether the channel has a password.
|
||||
has_password: bool,
|
||||
/// Needed talk power, or `None` when unrestricted.
|
||||
needed_talk_power: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -628,6 +628,12 @@ fn read_file_dek(path: &Path) -> Result<[u8; 32], StorageError> {
|
||||
/// headless / sandboxed environments force the file-fallback path
|
||||
/// without poking a real OS keyring (which would either prompt the
|
||||
/// 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 {
|
||||
matches!(
|
||||
std::env::var("CHANORA_DISABLE_KEYRING").as_deref(),
|
||||
|
||||
@@ -16,7 +16,7 @@ those terms.
|
||||
|
||||
| License | Crate count |
|
||||
|---------|-------------|
|
||||
| `Apache License 2.0` | 331 |
|
||||
| `Apache License 2.0` | 333 |
|
||||
| `MIT License` | 74 |
|
||||
| `Unicode License v3` | 19 |
|
||||
| `BSD 3-Clause "New" or "Revised" License` | 14 |
|
||||
@@ -117,6 +117,7 @@ those terms.
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
@@ -144,6 +145,7 @@ those terms.
|
||||
| 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-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> |
|
||||
| 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> |
|
||||
@@ -4993,6 +4995,213 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
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 "[]"
|
||||
replaced with your own identifying information. (Don'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 "printed page" 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 "License");
|
||||
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 "AS IS" 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.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" 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,
|
||||
"control" 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.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" 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.
|
||||
|
||||
"Work" 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).
|
||||
|
||||
"Derivative Works" 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.
|
||||
|
||||
"Contribution" 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, "submitted"
|
||||
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 "Not a Contribution."
|
||||
|
||||
"Contributor" 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 "NOTICE" 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 "AS IS" 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.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
|
||||
Reference in New Issue
Block a user