Revert "Smooth realtime display frame pacing"

This reverts commit a8e9729b3a.
This commit is contained in:
Mikei386
2026-08-05 20:54:33 +02:00
parent 96d323c32d
commit ab19798a2d
5 changed files with 20 additions and 59 deletions
+1 -1
View File
@@ -37,5 +37,5 @@ metrics.addEventListener('message', (event) => {
await new Promise((resolve) => setTimeout(resolve, 1100)); await new Promise((resolve) => setTimeout(resolve, 1100));
sockets.forEach((socket) => socket.close()); sockets.forEach((socket) => socket.close());
assert.equal(invalidPayload, false, 'metrics must be ordered and contain no large visual payloads'); 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)`); console.log(`runtime websocket test passed (${count} metrics in 1.1 s; max frame age ${maxFrameAgeMs} ms; all three streams opened)`);
+3 -8
View File
@@ -40,12 +40,8 @@ use crate::{
model::{InputSource, MeterFrame, PhoenixRtaConfig}, model::{InputSource, MeterFrame, PhoenixRtaConfig},
}; };
// Keep transport sampling safely above the 60 Hz display cadence. ALSA only #[cfg(target_os = "linux")]
// exposes complete capture periods, so a 60 Hz target with 128-frame periods const METRICS_TARGET_UPDATES_PER_SECOND: u32 = 60;
// 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)] #[derive(Clone, Copy, Debug, Default)]
struct TransportPeaks { struct TransportPeaks {
@@ -419,8 +415,7 @@ pub fn spawn_audio_capture_worker(deps: AudioWorkerDeps) {
pub fn spawn_audio_capture_worker(deps: AudioWorkerDeps) { pub fn spawn_audio_capture_worker(deps: AudioWorkerDeps) {
tokio::spawn(async move { tokio::spawn(async move {
warn!("Phoenix ALSA capture is only available on Linux; emitting placeholder frames on this host"); 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_millis(16));
let mut ticker = tokio::time::interval(Duration::from_nanos(tick_ns));
deps.actual_sample_rate deps.actual_sample_rate
.store(deps.config.sample_rate as u64, Ordering::SeqCst); .store(deps.config.sample_rate as u64, Ordering::SeqCst);
loop { loop {
-25
View File
@@ -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] #[test]
fn selection_never_upsamples_and_keeps_endpoints() { fn selection_never_upsamples_and_keeps_endpoints() {
assert_eq!(selected_sample_indices(3, 1024), vec![0, 1, 2]); assert_eq!(selected_sample_indices(3, 1024), vec![0, 1, 2]);
+11 -14
View File
@@ -26,6 +26,7 @@ let lifecycleHandlersBound = false;
let recoverTimer = null; let recoverTimer = null;
let lastHardRecoverAt = 0; let lastHardRecoverAt = 0;
let pendingPhoenixFrame = null; let pendingPhoenixFrame = null;
let phoenixPacketRaf = 0;
let phoenixPacketBusy = false; let phoenixPacketBusy = false;
const RMS_RING = { L: new Float32Array(512), R: new Float32Array(512), i: 0, n: 0 }; 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); if (phoenixVisualsRaf) cancelAnimationFrame(phoenixVisualsRaf);
phoenixVisualsRaf = 0; phoenixVisualsRaf = 0;
pendingPhoenixFrame = null; pendingPhoenixFrame = null;
if (phoenixPacketRaf) cancelAnimationFrame(phoenixPacketRaf);
phoenixPacketRaf = 0;
phoenixPacketBusy = false; phoenixPacketBusy = false;
} }
@@ -429,6 +432,7 @@ async function applyIncomingAudioPacket(env, packet, CONFIG, sampleTs = performa
const previousSeq = Number(env.audio.phoenixMetricsSeq || 0); const previousSeq = Number(env.audio.phoenixMetricsSeq || 0);
if (Number.isFinite(seq) && seq > 0 && previousSeq > 0 && seq <= previousSeq) return; if (Number.isFinite(seq) && seq > 0 && previousSeq > 0 && seq <= previousSeq) return;
if (Number.isFinite(seq) && seq > 0) env.audio.phoenixMetricsSeq = seq; if (Number.isFinite(seq) && seq > 0) env.audio.phoenixMetricsSeq = seq;
env.requestRender?.('audio');
env.audio.lastSampleTs = sampleTs; env.audio.lastSampleTs = sampleTs;
env.audio.alive = true; env.audio.alive = true;
if (Number.isFinite(Number(d.sampleRate)) && Number(d.sampleRate) > 0) { 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); 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) { function buildPhoenixMeterPacket(frame) {
@@ -627,26 +627,23 @@ function mergePendingPhoenixFrame(previous, next) {
} }
function schedulePhoenixPacketPump(env, CONFIG) { function schedulePhoenixPacketPump(env, CONFIG) {
if (phoenixPacketBusy || !pendingPhoenixFrame) return; if (phoenixPacketRaf || phoenixPacketBusy || !pendingPhoenixFrame) return;
phoenixPacketBusy = true; phoenixPacketRaf = requestAnimationFrame(async () => {
void (async () => { phoenixPacketRaf = 0;
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; const frame = pendingPhoenixFrame;
pendingPhoenixFrame = null; pendingPhoenixFrame = null;
if (!frame) return;
phoenixPacketBusy = true;
try {
const packet = buildPhoenixMeterPacket(frame); const packet = buildPhoenixMeterPacket(frame);
await applyIncomingAudioPacket(env, packet, CONFIG, performance.now()); await applyIncomingAudioPacket(env, packet, CONFIG, performance.now());
}
} catch (err) { } catch (err) {
console.warn('Phoenix packet error:', err); console.warn('Phoenix packet error:', err);
} finally { } finally {
phoenixPacketBusy = false; phoenixPacketBusy = false;
if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG); if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG);
} }
})(); });
} }
function enqueuePhoenixFrame(env, CONFIG, frame) { function enqueuePhoenixFrame(env, CONFIG, frame) {
+2 -8
View File
@@ -2698,20 +2698,14 @@ async function loop(now){
const currentAudioSeq = Math.max(currentMetricsSeq, currentVisualsSeq, currentXySeq); const currentAudioSeq = Math.max(currentMetricsSeq, currentVisualsSeq, currentXySeq);
const audioDirty = currentAudioSeq > lastRenderedAudioSeq; const audioDirty = currentAudioSeq > lastRenderedAudioSeq;
const shouldRenderFrame = const shouldRenderFrame =
audioOk ||
renderDirty || renderDirty ||
audioDirty || audioDirty ||
!audioOk || !audioOk ||
style === 'options-panel' || style === 'options-panel' ||
style === 'recorder' || style === 'recorder' ||
renderStyle === 'clock'; renderStyle === 'clock';
// With live audio, requestAnimationFrame itself is the display clock. An if (elapsed >= target){
// additional exact 60 Hz comparison occasionally rejected a real 60 Hz lastFrameTime = now - (elapsed % targetMs);
// 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);
try{ try{
updateRect(); updateRect();
meterHitRects.length = 0; meterHitRects.length = 0;