feat(voice): real-time mic input level metering at 30 Hz (#25)

* feat(voice): add real-time mic input level metering at 30 Hz

Expose input RMS from the audio engine through the bridge as a
dedicated Rust→Dart Stream<double>, replacing the binary on/off
indicator with a proportional dBFS level meter.

Rust side:
- chanora_audio: add set_input_dbfs/input_dbfs accessors to
  SharedAudioProcessingStats; restructure CaptureState::ingest()
  to compute dBFS from mono buffer before the PTT guard so the
  meter shows mic activity even when not transmitting.
- chanora_core: widen audio_stats() return to include f32 input
  level.
- chanora_bridge: add input_level: f32 to BridgeAudioStats and
  new input_level_stream(sink: StreamSink<f32>) that pushes at
  ~30 Hz via tokio interval task.
- Update frb_generated.rs serialization for the new field.

Flutter side:
- VoiceLevelMeter: accept optional double level (dBFS), map
  -60..0 dBFS to 0..1 fill fraction, animate with
  TweenAnimationBuilder for smooth transitions.
- voice_compact.dart: subscribe to inputLevelStream in the voice
  details sheet for 30 Hz meter updates, keeping 250 ms poll for
  TX/RX counters.
- voice_bar.dart: accept optional inputLevel from the stream.
- main.dart: subscribe to inputLevelStream, pass to VoiceBar.

* chore: sync Flutter build config and dependency updates

- Add Flutter migrator flags to gradle.properties (builtInKotlin, newDsl)
- Add FlutterGeneratedPluginSwiftPackage to iOS/macOS Xcode projects
- Update meta 1.17→1.18, test_api 0.7.10→0.7.11
- Rebuild chanora_bridge framework for macOS
- Update Podfile.lock for iOS and macOS

* fix(voice): correct meter animation, pre-gain dBFS, stream lifecycle, and protocol warnings

B1: Convert VoiceLevelMeter to StatefulWidget tracking previous fill
     as Tween begin so the meter animates smoothly instead of resetting
     to zero on every frame.

B2: Compute dBFS from pre-gain mono samples in CaptureState::ingest()
     so the level meter reflects raw mic input, matching mobile paths.

B4: End input_level_stream after 10 consecutive session errors instead
     of emitting -120 dBFS forever when the session is gone.

Also fixes all 13 clippy warnings in chanora_protocol: collapsed
nested if-let patterns, replaced .ok() + Some matching with Ok, used
? operator, and introduced EventChannels struct to reduce the four
helper functions below the 7-argument threshold.

* fix(voice): use MissedTickBehavior::Skip for level meter stream and align dBFS doc

Set MissedTickBehavior::Skip on the input_level_stream tokio interval
so slow audio_stats() calls skip missed ticks instead of bursting,
preventing CPU spikes on the UI meter thread.

Align VoiceLevelMeter class doc: the mapping floors at -60 dBFS
(via dbfsToFraction), not the full -120 range.
This commit is contained in:
Edison Jwa
2026-06-05 20:57:16 +09:00
committed by GitHub
parent 2c7b68e21e
commit 82441f3d97
25 changed files with 519 additions and 258 deletions
@@ -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
-37
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
+7
View File
@@ -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
@@ -395,6 +397,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.
@@ -957,6 +962,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;
@@ -2378,6 +2384,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_currentVoiceChannelId,
),
audioStats: _audioStats,
inputLevel: _inputLevel,
pttLevel: _pttLevel,
pttBackendId: _pttBackendId,
pttBoundInputClass: _pttBoundInputClass,
+16 -2
View File
@@ -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;
@@ -455,6 +455,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
int _rateTickCount = 0;
late final AudioProcessingConfigState _audioProcessing;
double? _streamLevel;
StreamSubscription<double>? _levelSub;
@override
void initState() {
@@ -463,8 +465,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 +478,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 +492,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
@override
void dispose() {
_levelSub?.cancel();
_statsTimer?.cancel();
super.dispose();
}
@@ -625,7 +631,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 01 fill fraction via [dbfsToFraction] (floors
/// at -60 dBFS).
class VoiceLevelMeter extends StatefulWidget {
const VoiceLevelMeter({super.key, this.active = false, this.level});
/// Binary fallback when no dBFS value is available.
final bool active;
/// 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),
),
),
@@ -9,6 +9,6 @@
<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>
<key>MinimumOSVersion</key><string>13.0</string>
</dict>
</plist>
-37
View File
@@ -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 = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "chanora_flutter.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
+4 -4
View File
@@ -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,
),
),
);
+3 -2
View File
@@ -1345,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)?;
@@ -1354,6 +1354,7 @@ impl ChanoraSession {
audio.frames_sent(),
audio.frames_received(),
audio.transmit_active(),
audio.input_level(),
))
}
@@ -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,
+31 -17
View File
@@ -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()
@@ -1717,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()
@@ -1752,6 +1759,7 @@ fn try_open_capture(
voice_out_tx,
transmit_active,
frames_sent,
audio_processing_stats,
)));
let stream = match in_format {
@@ -1803,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")))]
@@ -1815,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,
@@ -1828,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,
}
}
@@ -1841,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();
@@ -1859,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
@@ -2488,6 +2501,7 @@ pub mod bench_seam {
tx,
transmit_active.clone(),
frames_sent,
Arc::new(crate::SharedAudioProcessingStats::default()),
);
Self {
state,
+36 -1
View File
@@ -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.
@@ -2096,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))??;
@@ -2104,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,
+86 -30
View File
@@ -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);
}
}
+49 -81
View File
@@ -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,
));
@@ -502,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>,
) {
@@ -686,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,
),
}
@@ -730,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(()));
}
}
}
@@ -794,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,
)
@@ -872,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,
@@ -1118,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> {
@@ -1158,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,
)
@@ -1171,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,
)
@@ -1188,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,
)
@@ -1209,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,
)
@@ -1231,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,
)
@@ -1392,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> {
@@ -1431,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,
),
}
@@ -1448,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,
)
@@ -1784,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!(
@@ -2164,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,
@@ -2181,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(_),
@@ -2199,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,
@@ -2216,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,
@@ -2231,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,