From ab19798a2d0ba90a4404b95f1ff818c983206c09 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:54:33 +0200 Subject: [PATCH] Revert "Smooth realtime display frame pacing" This reverts commit a8e9729b3a0557401ad0c8c124ae9826a64c9617. --- scripts/test_websocket_runtime.mjs | 2 +- src/audio.rs | 11 +++-------- src/goniometer.rs | 25 ------------------------ www/core/audio.js | 31 ++++++++++++++---------------- www/main.js | 10 ++-------- 5 files changed, 20 insertions(+), 59 deletions(-) diff --git a/scripts/test_websocket_runtime.mjs b/scripts/test_websocket_runtime.mjs index 6a6107b..9dce44d 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 >= 110 && count <= 145, `expected about 120 metrics/s, received ${count}`); +assert.ok(count >= 50 && count <= 75, `expected about 60 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 9cc1a85..cc59036 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -40,12 +40,8 @@ use crate::{ model::{InputSource, MeterFrame, PhoenixRtaConfig}, }; -// 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; +#[cfg(target_os = "linux")] +const METRICS_TARGET_UPDATES_PER_SECOND: u32 = 60; #[derive(Clone, Copy, Debug, Default)] struct TransportPeaks { @@ -419,8 +415,7 @@ 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 tick_ns = 1_000_000_000u64 / u64::from(METRICS_TARGET_UPDATES_PER_SECOND); - let mut ticker = tokio::time::interval(Duration::from_nanos(tick_ns)); + let mut ticker = tokio::time::interval(Duration::from_millis(16)); 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 1d3543c..de27292 100644 --- a/src/goniometer.rs +++ b/src/goniometer.rs @@ -58,31 +58,6 @@ 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]); diff --git a/www/core/audio.js b/www/core/audio.js index 1bfb66a..8d1e0fc 100644 --- a/www/core/audio.js +++ b/www/core/audio.js @@ -26,6 +26,7 @@ let lifecycleHandlersBound = false; let recoverTimer = null; let lastHardRecoverAt = 0; let pendingPhoenixFrame = null; +let phoenixPacketRaf = 0; let phoenixPacketBusy = false; const RMS_RING = { L: new Float32Array(512), R: new Float32Array(512), i: 0, n: 0 }; @@ -124,6 +125,8 @@ function closePhoenixSocket() { if (phoenixVisualsRaf) cancelAnimationFrame(phoenixVisualsRaf); phoenixVisualsRaf = 0; pendingPhoenixFrame = null; + if (phoenixPacketRaf) cancelAnimationFrame(phoenixPacketRaf); + phoenixPacketRaf = 0; phoenixPacketBusy = false; } @@ -429,6 +432,7 @@ async function applyIncomingAudioPacket(env, packet, CONFIG, sampleTs = performa const previousSeq = Number(env.audio.phoenixMetricsSeq || 0); if (Number.isFinite(seq) && seq > 0 && previousSeq > 0 && seq <= previousSeq) return; if (Number.isFinite(seq) && seq > 0) env.audio.phoenixMetricsSeq = seq; + env.requestRender?.('audio'); env.audio.lastSampleTs = sampleTs; env.audio.alive = true; if (Number.isFinite(Number(d.sampleRate)) && Number(d.sampleRate) > 0) { @@ -483,10 +487,6 @@ async function applyIncomingAudioPacket(env, packet, CONFIG, sampleTs = performa } await updateActiveMeters(env, d, CONFIG); - // Publish the frame only after every active meter has consumed the packet. - // Otherwise the canvas loop can draw between the data update and the meter - // update, producing an old frame followed by a visible catch-up step. - env.requestRender?.('audio'); } function buildPhoenixMeterPacket(frame) { @@ -627,26 +627,23 @@ function mergePendingPhoenixFrame(previous, next) { } function schedulePhoenixPacketPump(env, CONFIG) { - if (phoenixPacketBusy || !pendingPhoenixFrame) return; - phoenixPacketBusy = true; - void (async () => { + if (phoenixPacketRaf || phoenixPacketBusy || !pendingPhoenixFrame) return; + phoenixPacketRaf = requestAnimationFrame(async () => { + phoenixPacketRaf = 0; + const frame = pendingPhoenixFrame; + pendingPhoenixFrame = null; + if (!frame) return; + phoenixPacketBusy = true; try { - // Consume the newest packet immediately. Incoming packets are still - // coalesced while an asynchronous meter/config update is in progress, - // but they no longer wait for a second requestAnimationFrame loop. - while (pendingPhoenixFrame) { - const frame = pendingPhoenixFrame; - pendingPhoenixFrame = null; - const packet = buildPhoenixMeterPacket(frame); - await applyIncomingAudioPacket(env, packet, CONFIG, performance.now()); - } + const packet = buildPhoenixMeterPacket(frame); + await applyIncomingAudioPacket(env, packet, CONFIG, performance.now()); } catch (err) { console.warn('Phoenix packet error:', err); } finally { phoenixPacketBusy = false; if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG); } - })(); + }); } function enqueuePhoenixFrame(env, CONFIG, frame) { diff --git a/www/main.js b/www/main.js index 8a2d5d7..35fffa5 100644 --- a/www/main.js +++ b/www/main.js @@ -2698,20 +2698,14 @@ async function loop(now){ const currentAudioSeq = Math.max(currentMetricsSeq, currentVisualsSeq, currentXySeq); const audioDirty = currentAudioSeq > lastRenderedAudioSeq; const shouldRenderFrame = - audioOk || renderDirty || audioDirty || !audioOk || style === 'options-panel' || style === 'recorder' || renderStyle === 'clock'; - // With live audio, requestAnimationFrame itself is the display clock. An - // additional exact 60 Hz comparison occasionally rejected a real 60 Hz - // frame because of fractional timestamp jitter, creating 33 ms pauses in - // otherwise smooth peak decay. Only throttle the disconnected state. - const frameDue = audioOk || elapsed >= target; - if (frameDue){ - lastFrameTime = audioOk ? now : now - (elapsed % targetMs); + if (elapsed >= target){ + lastFrameTime = now - (elapsed % targetMs); try{ updateRect(); meterHitRects.length = 0;