From dc09a637372bc18c537fc66916c90202fa15bdb0 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:43:03 +0200 Subject: [PATCH] Increase realtime display transport rate --- scripts/test_websocket_runtime.mjs | 2 +- src/audio.rs | 11 ++++++++--- src/goniometer.rs | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/scripts/test_websocket_runtime.mjs b/scripts/test_websocket_runtime.mjs index 9dce44d..6a6107b 100644 --- a/scripts/test_websocket_runtime.mjs +++ b/scripts/test_websocket_runtime.mjs @@ -37,5 +37,5 @@ metrics.addEventListener('message', (event) => { await new Promise((resolve) => setTimeout(resolve, 1100)); sockets.forEach((socket) => socket.close()); assert.equal(invalidPayload, false, 'metrics must be ordered and contain no large visual payloads'); -assert.ok(count >= 50 && count <= 75, `expected about 60 metrics/s, received ${count}`); +assert.ok(count >= 110 && count <= 145, `expected about 120 metrics/s, received ${count}`); console.log(`runtime websocket test passed (${count} metrics in 1.1 s; max frame age ${maxFrameAgeMs} ms; all three streams opened)`); diff --git a/src/audio.rs b/src/audio.rs index cc59036..9cc1a85 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -40,8 +40,12 @@ use crate::{ model::{InputSource, MeterFrame, PhoenixRtaConfig}, }; -#[cfg(target_os = "linux")] -const METRICS_TARGET_UPDATES_PER_SECOND: u32 = 60; +// Keep transport sampling safely above the 60 Hz display cadence. ALSA only +// exposes complete capture periods, so a 60 Hz target with 128-frame periods +// alternates between roughly 16.0 and 18.7 ms at 48 kHz. A 120 Hz snapshot +// clock guarantees at least one fresh native state per display frame without +// changing any detector or ballistic calculation. +const METRICS_TARGET_UPDATES_PER_SECOND: u32 = 120; #[derive(Clone, Copy, Debug, Default)] struct TransportPeaks { @@ -415,7 +419,8 @@ pub fn spawn_audio_capture_worker(deps: AudioWorkerDeps) { pub fn spawn_audio_capture_worker(deps: AudioWorkerDeps) { tokio::spawn(async move { warn!("Phoenix ALSA capture is only available on Linux; emitting placeholder frames on this host"); - let mut ticker = tokio::time::interval(Duration::from_millis(16)); + let tick_ns = 1_000_000_000u64 / u64::from(METRICS_TARGET_UPDATES_PER_SECOND); + let mut ticker = tokio::time::interval(Duration::from_nanos(tick_ns)); deps.actual_sample_rate .store(deps.config.sample_rate as u64, Ordering::SeqCst); loop { diff --git a/src/goniometer.rs b/src/goniometer.rs index de27292..1d3543c 100644 --- a/src/goniometer.rs +++ b/src/goniometer.rs @@ -58,6 +58,31 @@ mod tests { } } + #[test] + fn double_rate_transport_never_leaves_a_sixty_hz_display_interval_empty() { + let sample_rate = 48_000u32; + let period = 128usize; + let mut clock = GoniometerClock::default(); + let mut processed = 0usize; + let mut previous_emission = None; + let mut largest_gap = 0usize; + + while processed < sample_rate as usize * 10 { + processed += period; + if clock.advance(period, sample_rate, 120) { + if let Some(previous) = previous_emission { + largest_gap = largest_gap.max(processed - previous); + } + previous_emission = Some(processed); + } + } + + assert!( + largest_gap <= sample_rate as usize / 60, + "largest transport gap was {largest_gap} samples" + ); + } + #[test] fn selection_never_upsamples_and_keeps_endpoints() { assert_eq!(selected_sample_indices(3, 1024), vec![0, 1, 2]);