fix(ui): address PR #30 review findings
- ViewportInfo.updateShouldNotify: compare layoutClass only (not width/height), avoiding unnecessary rebuilds on every resize frame within the same layout class. - ChatPanel: use BorderDirectional(start:) for RTL support. - ChatPanel: localize 'Close chat' tooltip via AppL10n.chatCloseAction. - Inline panel snackbar: localize via AppL10n.chatPanelCollapsedHint. New en/zh ARB entries added. - _saveCurrentDraft(): removed — it was a self-assignment no-op. Draft persistence relies on ChatDetailView's didUpdateWidget (fires onDraftChanged on target switch) and dispose (fires on panel tear-down), both of which already populate _chatDrafts correctly without an explicit save call. - _handleInlineChatViewport layout snackbar: use AppL10n. - Audio level-meter: switch from callback-count (% 3) to time-based gating (std::time::Duration::from_millis(33)), robust to cpal buffer-size or sample-rate changes. Remove level_decimation_counter. - chat_panel_test.dart: add AppL10n.localizationsDelegates so the test resolves l10n keys. Tests: 183 passed, 2 skipped. Dart analyze clean. cargo test -p chanora_audio --lib: 125 passed.
This commit is contained in:
@@ -1818,18 +1818,27 @@ 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,
|
||||
/// Time-based decimation for the level meter. The cpal callback
|
||||
/// cadence depends on platform (256 frames at 48 kHz ≈ 187 Hz,
|
||||
/// 512 frames ≈ 94 Hz, 1024 frames ≈ 47 Hz) and can change at
|
||||
/// runtime on device or sample-rate switch. The bridge consumer
|
||||
/// (`input_level_stream`) only reads at ~30 Hz, so computing
|
||||
/// `sqrt()` + `log10()` on every callback wastes real-time budget
|
||||
/// and caused buffer underruns on macOS CoreAudio with small
|
||||
/// buffer sizes. We only emit a new dBFS sample after at least
|
||||
/// [LEVEL_METER_INTERVAL] has elapsed since the previous emit,
|
||||
/// which is platform-cadence-independent.
|
||||
last_level_emit: std::time::Instant,
|
||||
}
|
||||
|
||||
/// Minimum interval between input-level dBFS samples sent to the
|
||||
/// bridge. Matches the consumer rate (`input_level_stream` at ~30 Hz).
|
||||
/// Time-based gating is robust to cpal buffer-size and sample-rate
|
||||
/// changes that a fixed callback-count would not be.
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
const LEVEL_METER_INTERVAL: std::time::Duration =
|
||||
std::time::Duration::from_millis(33);
|
||||
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
impl CaptureState {
|
||||
fn new(
|
||||
@@ -1857,7 +1866,10 @@ impl CaptureState {
|
||||
mono_scratch: Vec::with_capacity(4096),
|
||||
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
|
||||
audio_processing_stats,
|
||||
level_decimation_counter: 0,
|
||||
// Start in the past so the first ingest emits immediately.
|
||||
last_level_emit: std::time::Instant::now()
|
||||
.checked_sub(LEVEL_METER_INTERVAL)
|
||||
.unwrap_or_else(std::time::Instant::now),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1877,11 +1889,12 @@ impl CaptureState {
|
||||
self.mono_scratch.push(sum / frame.len() as f32);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Level meter: pay sqrt() + log10() only when at least
|
||||
// LEVEL_METER_INTERVAL has elapsed, regardless of the
|
||||
// platform's cpal callback cadence.
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(self.last_level_emit) >= LEVEL_METER_INTERVAL {
|
||||
self.last_level_emit = now;
|
||||
self.audio_processing_stats
|
||||
.set_input_dbfs(crate::frame::dbfs(&self.mono_scratch));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user