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
@@ -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 */;