Synchronize realtime data with display frames
This commit is contained in:
+42
-72
@@ -16,17 +16,14 @@ let phoenixSpectroSocket = null;
|
||||
let phoenixSpectroRetryTimer = null;
|
||||
let phoenixSpectroDemanded = false;
|
||||
let pendingSpectroBuffer = null;
|
||||
let phoenixSpectroRaf = 0;
|
||||
let phoenixVisualsSocket = null;
|
||||
let phoenixVisualsRetryTimer = null;
|
||||
let pendingVisualsBuffer = null;
|
||||
let phoenixVisualsRaf = 0;
|
||||
let envRef = null;
|
||||
let lifecycleHandlersBound = false;
|
||||
let recoverTimer = null;
|
||||
let lastHardRecoverAt = 0;
|
||||
let pendingPhoenixFrame = null;
|
||||
let phoenixPacketBusy = false;
|
||||
|
||||
const RMS_RING = { L: new Float32Array(512), R: new Float32Array(512), i: 0, n: 0 };
|
||||
const WAVEFORM_RING_SECONDS = 20;
|
||||
@@ -106,8 +103,6 @@ function closePhoenixSocket() {
|
||||
phoenixSpectroSocket = null;
|
||||
phoenixSpectroDemanded = false;
|
||||
pendingSpectroBuffer = null;
|
||||
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||
phoenixSpectroRaf = 0;
|
||||
if (phoenixVisualsRetryTimer) clearTimeout(phoenixVisualsRetryTimer);
|
||||
phoenixVisualsRetryTimer = null;
|
||||
if (phoenixVisualsSocket) {
|
||||
@@ -121,10 +116,7 @@ function closePhoenixSocket() {
|
||||
}
|
||||
phoenixVisualsSocket = null;
|
||||
pendingVisualsBuffer = null;
|
||||
if (phoenixVisualsRaf) cancelAnimationFrame(phoenixVisualsRaf);
|
||||
phoenixVisualsRaf = 0;
|
||||
pendingPhoenixFrame = null;
|
||||
phoenixPacketBusy = false;
|
||||
}
|
||||
|
||||
async function requestPhoenixRtaConfig(baseUrl, config) {
|
||||
@@ -269,7 +261,6 @@ function openPhoenixSpectroSocket(baseUrl, env) {
|
||||
socket.onmessage = (event) => {
|
||||
if (!(event.data instanceof ArrayBuffer)) return;
|
||||
pendingSpectroBuffer = event.data;
|
||||
scheduleSpectroBufferPump(env);
|
||||
};
|
||||
|
||||
const retry = () => {
|
||||
@@ -306,27 +297,6 @@ function setPhoenixSpectroDemand(baseUrl, env, demanded) {
|
||||
} catch (_) {}
|
||||
}
|
||||
pendingSpectroBuffer = null;
|
||||
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||
phoenixSpectroRaf = 0;
|
||||
}
|
||||
|
||||
function scheduleSpectroBufferPump(env) {
|
||||
if (phoenixSpectroRaf || !pendingSpectroBuffer) return;
|
||||
phoenixSpectroRaf = requestAnimationFrame(() => {
|
||||
phoenixSpectroRaf = 0;
|
||||
const buffer = pendingSpectroBuffer;
|
||||
pendingSpectroBuffer = null;
|
||||
try {
|
||||
const decoded = decodePhoenixSpectroBuffer(buffer);
|
||||
if (!decoded) return;
|
||||
if (!copyPhoenixSpectroBins(env.audio, decoded)) return;
|
||||
env.requestRender?.('spectro');
|
||||
} catch (err) {
|
||||
console.warn('Phoenix spectrogram packet error:', err);
|
||||
} finally {
|
||||
if (pendingSpectroBuffer) scheduleSpectroBufferPump(env);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openPhoenixVisualsSocket(baseUrl, env) {
|
||||
@@ -340,7 +310,6 @@ function openPhoenixVisualsSocket(baseUrl, env) {
|
||||
socket.onmessage = (event) => {
|
||||
if (!(event.data instanceof ArrayBuffer)) return;
|
||||
pendingVisualsBuffer = event.data;
|
||||
scheduleVisualsBufferPump(env);
|
||||
};
|
||||
const retry = () => {
|
||||
if (phoenixVisualsSocket === socket) phoenixVisualsSocket = null;
|
||||
@@ -355,22 +324,6 @@ function openPhoenixVisualsSocket(baseUrl, env) {
|
||||
socket.onclose = retry;
|
||||
}
|
||||
|
||||
function scheduleVisualsBufferPump(env) {
|
||||
if (phoenixVisualsRaf || !pendingVisualsBuffer) return;
|
||||
phoenixVisualsRaf = requestAnimationFrame(() => {
|
||||
phoenixVisualsRaf = 0;
|
||||
const buffer = pendingVisualsBuffer;
|
||||
pendingVisualsBuffer = null;
|
||||
try {
|
||||
applyVisualsBuffer(env, buffer);
|
||||
} catch (err) {
|
||||
console.warn('Phoenix visual packet error:', err);
|
||||
} finally {
|
||||
if (pendingVisualsBuffer) scheduleVisualsBufferPump(env);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyVisualsBuffer(env, buffer) {
|
||||
const decoded = decodePhoenixVisualsBuffer(buffer);
|
||||
if (!decoded) return;
|
||||
@@ -626,32 +579,49 @@ function mergePendingPhoenixFrame(previous, next) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
function schedulePhoenixPacketPump(env, CONFIG) {
|
||||
if (phoenixPacketBusy || !pendingPhoenixFrame) return;
|
||||
phoenixPacketBusy = true;
|
||||
void (async () => {
|
||||
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());
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Phoenix packet error:', err);
|
||||
} finally {
|
||||
phoenixPacketBusy = false;
|
||||
if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG);
|
||||
}
|
||||
})();
|
||||
function enqueuePhoenixFrame(frame) {
|
||||
pendingPhoenixFrame = mergePendingPhoenixFrame(pendingPhoenixFrame, frame);
|
||||
}
|
||||
|
||||
function enqueuePhoenixFrame(env, CONFIG, frame) {
|
||||
pendingPhoenixFrame = mergePendingPhoenixFrame(pendingPhoenixFrame, frame);
|
||||
schedulePhoenixPacketPump(env, CONFIG);
|
||||
// WebSocket delivery and the display's VSync are independent clocks. Keep the
|
||||
// callbacks allocation-light and publish one coherent newest-state snapshot at
|
||||
// the beginning of the single canvas render loop. This prevents metrics,
|
||||
// visuals and spectrogram updates from being applied in bursts between frames.
|
||||
export async function flushPendingPhoenixData(env, CONFIG) {
|
||||
const metricsFrame = pendingPhoenixFrame;
|
||||
const visualsBuffer = pendingVisualsBuffer;
|
||||
const spectroBuffer = pendingSpectroBuffer;
|
||||
pendingPhoenixFrame = null;
|
||||
pendingVisualsBuffer = null;
|
||||
pendingSpectroBuffer = null;
|
||||
|
||||
if (metricsFrame) {
|
||||
try {
|
||||
const packet = buildPhoenixMeterPacket(metricsFrame);
|
||||
await applyIncomingAudioPacket(env, packet, CONFIG, performance.now());
|
||||
} catch (err) {
|
||||
console.warn('Phoenix packet error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (visualsBuffer) {
|
||||
try {
|
||||
applyVisualsBuffer(env, visualsBuffer);
|
||||
} catch (err) {
|
||||
console.warn('Phoenix visual packet error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (spectroBuffer) {
|
||||
try {
|
||||
const decoded = decodePhoenixSpectroBuffer(spectroBuffer);
|
||||
if (decoded && copyPhoenixSpectroBins(env.audio, decoded)) {
|
||||
env.requestRender?.('spectro');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Phoenix spectrogram packet error:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindLifecycleHandlers() {
|
||||
@@ -1174,7 +1144,7 @@ async function initPhoenixAudio(env) {
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const frame = JSON.parse(event.data);
|
||||
enqueuePhoenixFrame(env, CONFIG, frame);
|
||||
enqueuePhoenixFrame(frame);
|
||||
} catch (err) {
|
||||
console.warn('Phoenix metrics parse error:', err);
|
||||
}
|
||||
|
||||
+5
-1
@@ -23,7 +23,7 @@ try {
|
||||
import { CONFIG, applyRtaBpoSelection, loadConfig, saveConfig, loadLayoutPreset } from './core/config.js';
|
||||
import * as utils from './core/utils.js';
|
||||
import { meterFacade, registerMeter } from './core/registry.js';
|
||||
import { initAudio, audioLost, reloadAudio } from './core/audio.js';
|
||||
import { initAudio, audioLost, reloadAudio, flushPendingPhoenixData } from './core/audio.js';
|
||||
import { createScreensaver } from './core/screensaver.js';
|
||||
|
||||
// Views
|
||||
@@ -2675,6 +2675,10 @@ async function maybeRecoverAudio(reason) {
|
||||
|
||||
async function loop(now){
|
||||
const elapsed = now - lastFrameTime;
|
||||
// Apply all newest transport states once, immediately before this display
|
||||
// frame reads them. WebSocket callbacks only buffer; this is the sole
|
||||
// presentation pump for metrics, visuals and spectrogram data.
|
||||
await flushPendingPhoenixData(env, CONFIG);
|
||||
const audioOk = env.audio.alive && !audioLost(env);
|
||||
const renderStyle = getRenderableStyle();
|
||||
if (processingProfileDirty || appliedProcessingProfileView !== renderStyle) {
|
||||
|
||||
Reference in New Issue
Block a user