feat(ui): adaptive 3-panel layout, chat panel switching, audio metering fix

- Add responsive breakpoints (compact <600, medium 600-1023, expanded >=1024)
- Add ViewportInfo InheritedWidget for layout-aware descendants
- Add inline ChatPanel (380dp right column) for expanded desktop layout
- Add channel right-click context menu with Chat option for in-place switching
- Add per-target draft persistence via restoredDraft/onDraftChanged callbacks
- Fix header chat button to switch to current voice channel when panel open
- Fix close = dismiss (preserves last target and draft for reopen)
- Add unread dot indicator on channel tiles when chat is closed
- Fix audio regression: decimate dBFS computation to every 3rd callback (~31 Hz)
  to avoid buffer underruns on macOS CoreAudio real-time thread
- Add tools/build-macos.sh release build script (7-step process)
- Add chat panel switching implementation plan and 3-panel design spec

Tests: 183 passed, 2 skipped. Flutter analyze clean.
This commit is contained in:
Edison Jwa
2026-06-07 23:12:07 +09:00
parent e7f7c55b30
commit 5e8b7915db
14 changed files with 2187 additions and 283 deletions
+19 -4
View File
@@ -1812,6 +1812,16 @@ struct CaptureState {
/// after warmup. Same precedent as `mono_scratch` above.
frame_scratch: Vec<f32>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
/// Decimation counter for the level meter. The cpal callback fires
/// ~93 times/sec (256 frames at 48 kHz), but the bridge consumer
/// (`input_level_stream`) only reads at ~30 Hz. Computing `sqrt()`
/// + `log10()` every callback wastes real-time budget and causes
/// buffer underruns on macOS CoreAudio. We accumulate the running
/// sum-of-squares every callback (trivially cheap: O(n) multiply-
/// add) and only compute the final dBFS every 3rd callback (~31 Hz),
/// matching the consumer rate. See commit history for the metering
/// regression that motivated this.
level_decimation_counter: u32,
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
@@ -1841,6 +1851,7 @@ impl CaptureState {
mono_scratch: Vec::with_capacity(4096),
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
audio_processing_stats,
level_decimation_counter: 0,
}
}
@@ -1860,10 +1871,14 @@ impl CaptureState {
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));
// Level meter: accumulate sum-of-squares on every callback
// (trivially cheap), but only pay for sqrt() + log10() every
// 3rd callback (~31 Hz, matching the bridge consumer rate).
self.level_decimation_counter = self.level_decimation_counter.wrapping_add(1);
if self.level_decimation_counter % 3 == 0 {
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 {