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