From 66c912e2799cb622a6fa810675281d7b261a7862 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Fri, 29 May 2026 17:15:06 -0400 Subject: [PATCH] perf: coalesce per-peer audio levels to ~10/sec for the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mixer ran at the fixed 20ms playback cadence and pushed an AudioLevels event every tick — ~50/sec — each waking the iced runtime for a full re-render. Levels are now peak-held per peer across a 100ms window and emitted once per window (~10/sec), cutting UI-bound events 5x. Peak-hold (rather than last-sample) means a brief speech transient inside a window still registers, so the speaking indicator stays responsive. Co-Authored-By: Claude Opus 4.8 --- src/core/mod.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/core/mod.rs b/src/core/mod.rs index 2238ed6..13e7d74 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -325,20 +325,28 @@ async fn run_core_loop( let mut interval = tokio::time::interval(Duration::from_millis(20)); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // The 20ms mix cadence is fixed by playback, but pushing a + // level event every tick floods the UI runtime at ~50/sec. We + // peak-hold per-peer levels across this many ticks and emit + // once per window (~10/sec) — peak-hold so a brief transient + // inside the window still lights the speaking indicator. + const LEVEL_EMIT_TICKS: u32 = 5; + let mut level_peaks: HashMap = HashMap::new(); + let mut ticks_since_emit: u32 = 0; + loop { interval.tick().await; let current_volumes = peer_volumes_mixer.lock().await.clone(); - let mut active_levels = Vec::new(); let mut peer_frames = Vec::new(); { let mut guard = jitter_mixer.lock().await; for (&peer_id, buffer) in guard.iter_mut() { - // `None` means idle/buffering: contribute nothing - // and report a zero level so the UI shows idle. + // `None` means idle/buffering: contribute nothing, + // but keep a (zero) entry so the UI sees it idle. let Some(mut frame) = buffer.pop_frame() else { - active_levels.push((peer_id, 0.0)); + level_peaks.entry(peer_id).or_insert(0.0); continue; }; @@ -352,7 +360,8 @@ async fn run_core_loop( let sum_sq: f32 = frame.iter().map(|&x| (x as f32).powi(2)).sum(); let rms = (sum_sq / frame.len().max(1) as f32).sqrt(); let level = (rms / 32768.0).clamp(0.0, 1.0); - active_levels.push((peer_id, level)); + let peak = level_peaks.entry(peer_id).or_insert(0.0); + *peak = peak.max(level); peer_frames.push(frame); } @@ -379,7 +388,13 @@ async fn run_core_loop( break; } - let _ = ui_tx_mixer.send(UiEvent::AudioLevels(active_levels)).await; + // Emit coalesced peaks once per window, then reset. + ticks_since_emit += 1; + if ticks_since_emit >= LEVEL_EMIT_TICKS { + let levels: Vec<(EndpointId, f32)> = level_peaks.drain().collect(); + let _ = ui_tx_mixer.send(UiEvent::AudioLevels(levels)).await; + ticks_since_emit = 0; + } } });