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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user