Revert "Synchronize realtime data with display frames"
This reverts commit c6a1037611.
This commit is contained in:
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict';
|
|||||||
|
|
||||||
const html = fs.readFileSync(new URL('../www/index.html', import.meta.url), 'utf8');
|
const html = fs.readFileSync(new URL('../www/index.html', import.meta.url), 'utf8');
|
||||||
const main = fs.readFileSync(new URL('../www/main.js', import.meta.url), 'utf8');
|
const main = fs.readFileSync(new URL('../www/main.js', import.meta.url), 'utf8');
|
||||||
const audio = fs.readFileSync(new URL('../www/core/audio.js', import.meta.url), 'utf8');
|
|
||||||
const realtime = fs.readFileSync(new URL('../www/views/realtime.js', import.meta.url), 'utf8');
|
const realtime = fs.readFileSync(new URL('../www/views/realtime.js', import.meta.url), 'utf8');
|
||||||
|
|
||||||
const styleSelect = html.match(/<select\b[^>]*\bid="styleSel"[^>]*>/)?.[0] || '';
|
const styleSelect = html.match(/<select\b[^>]*\bid="styleSel"[^>]*>/)?.[0] || '';
|
||||||
@@ -12,24 +11,6 @@ assert(!/\bopt-w-\d+\b/.test(styleSelect), 'style selector must not have a fixed
|
|||||||
assert(/adjustStyleDropdownWidth\(\)/.test(main), 'dynamic style selector sizing is missing');
|
assert(/adjustStyleDropdownWidth\(\)/.test(main), 'dynamic style selector sizing is missing');
|
||||||
assert(/styleSel\.style\.width\s*=/.test(main), 'dynamic style selector width assignment is missing');
|
assert(/styleSel\.style\.width\s*=/.test(main), 'dynamic style selector width assignment is missing');
|
||||||
|
|
||||||
assert(/currentMetricsSeq\s*=\s*Number\.isFinite\(env\.audio\?\.phoenixMetricsSeq\)/.test(main),
|
|
||||||
'render loop must track the metrics sequence used by the RTA');
|
|
||||||
assert(/renderRequestGeneration\s*\+=\s*1/.test(main),
|
|
||||||
'render requests must carry a generation that survives asynchronous rendering');
|
|
||||||
assert(/renderRequestGeneration\s*===\s*renderGeneration\)\s*renderDirty\s*=\s*false/.test(main),
|
|
||||||
'an in-flight render must not clear a newer render request');
|
|
||||||
assert(/const frameDue\s*=\s*audioOk\s*\|\|\s*elapsed\s*>=\s*target/.test(main),
|
|
||||||
'live displays must use requestAnimationFrame directly instead of a second 60 Hz gate');
|
|
||||||
assert(/await flushPendingPhoenixData\(env, CONFIG\);[^]*?const currentMetricsSeq/.test(main),
|
|
||||||
'the render loop must publish buffered Phoenix data before reading frame sequences');
|
|
||||||
assert(/await updateActiveMeters\(env, d, CONFIG\);\s*[^]*?env\.requestRender\?\.\('audio'\)/.test(audio),
|
|
||||||
'audio rendering must be requested after active meters finish updating');
|
|
||||||
assert(/export async function flushPendingPhoenixData\(env, CONFIG\)/.test(audio),
|
|
||||||
'Phoenix data paths must expose one display-synchronized presentation pump');
|
|
||||||
assert(!/requestAnimationFrame/.test(audio),
|
|
||||||
'audio transport must not create requestAnimationFrame loops that compete with the canvas loop');
|
|
||||||
assert(!/while\s*\(pendingPhoenixFrame\)/.test(audio),
|
|
||||||
'metrics transport must coalesce bursts instead of draining every queued state');
|
|
||||||
assert(!/syncNativePeakHold/.test(realtime),
|
assert(!/syncNativePeakHold/.test(realtime),
|
||||||
'native RTA peak anchors must not be copied directly into the visible hold line');
|
'native RTA peak anchors must not be copied directly into the visible hold line');
|
||||||
assert((realtime.match(/applyPeakHold\(state, integrated, CONFIG, range\)/g) || []).length >= 3,
|
assert((realtime.match(/applyPeakHold\(state, integrated, CONFIG, range\)/g) || []).length >= 3,
|
||||||
|
|||||||
+67
-37
@@ -16,14 +16,17 @@ let phoenixSpectroSocket = null;
|
|||||||
let phoenixSpectroRetryTimer = null;
|
let phoenixSpectroRetryTimer = null;
|
||||||
let phoenixSpectroDemanded = false;
|
let phoenixSpectroDemanded = false;
|
||||||
let pendingSpectroBuffer = null;
|
let pendingSpectroBuffer = null;
|
||||||
|
let phoenixSpectroRaf = 0;
|
||||||
let phoenixVisualsSocket = null;
|
let phoenixVisualsSocket = null;
|
||||||
let phoenixVisualsRetryTimer = null;
|
let phoenixVisualsRetryTimer = null;
|
||||||
let pendingVisualsBuffer = null;
|
let pendingVisualsBuffer = null;
|
||||||
|
let phoenixVisualsRaf = 0;
|
||||||
let envRef = null;
|
let envRef = null;
|
||||||
let lifecycleHandlersBound = false;
|
let lifecycleHandlersBound = false;
|
||||||
let recoverTimer = null;
|
let recoverTimer = null;
|
||||||
let lastHardRecoverAt = 0;
|
let lastHardRecoverAt = 0;
|
||||||
let pendingPhoenixFrame = null;
|
let pendingPhoenixFrame = null;
|
||||||
|
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 };
|
||||||
const WAVEFORM_RING_SECONDS = 20;
|
const WAVEFORM_RING_SECONDS = 20;
|
||||||
@@ -103,6 +106,8 @@ function closePhoenixSocket() {
|
|||||||
phoenixSpectroSocket = null;
|
phoenixSpectroSocket = null;
|
||||||
phoenixSpectroDemanded = false;
|
phoenixSpectroDemanded = false;
|
||||||
pendingSpectroBuffer = null;
|
pendingSpectroBuffer = null;
|
||||||
|
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||||
|
phoenixSpectroRaf = 0;
|
||||||
if (phoenixVisualsRetryTimer) clearTimeout(phoenixVisualsRetryTimer);
|
if (phoenixVisualsRetryTimer) clearTimeout(phoenixVisualsRetryTimer);
|
||||||
phoenixVisualsRetryTimer = null;
|
phoenixVisualsRetryTimer = null;
|
||||||
if (phoenixVisualsSocket) {
|
if (phoenixVisualsSocket) {
|
||||||
@@ -116,7 +121,10 @@ function closePhoenixSocket() {
|
|||||||
}
|
}
|
||||||
phoenixVisualsSocket = null;
|
phoenixVisualsSocket = null;
|
||||||
pendingVisualsBuffer = null;
|
pendingVisualsBuffer = null;
|
||||||
|
if (phoenixVisualsRaf) cancelAnimationFrame(phoenixVisualsRaf);
|
||||||
|
phoenixVisualsRaf = 0;
|
||||||
pendingPhoenixFrame = null;
|
pendingPhoenixFrame = null;
|
||||||
|
phoenixPacketBusy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestPhoenixRtaConfig(baseUrl, config) {
|
async function requestPhoenixRtaConfig(baseUrl, config) {
|
||||||
@@ -261,6 +269,7 @@ function openPhoenixSpectroSocket(baseUrl, env) {
|
|||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
if (!(event.data instanceof ArrayBuffer)) return;
|
if (!(event.data instanceof ArrayBuffer)) return;
|
||||||
pendingSpectroBuffer = event.data;
|
pendingSpectroBuffer = event.data;
|
||||||
|
scheduleSpectroBufferPump(env);
|
||||||
};
|
};
|
||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
@@ -297,6 +306,27 @@ function setPhoenixSpectroDemand(baseUrl, env, demanded) {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
pendingSpectroBuffer = null;
|
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) {
|
function openPhoenixVisualsSocket(baseUrl, env) {
|
||||||
@@ -310,6 +340,7 @@ function openPhoenixVisualsSocket(baseUrl, env) {
|
|||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
if (!(event.data instanceof ArrayBuffer)) return;
|
if (!(event.data instanceof ArrayBuffer)) return;
|
||||||
pendingVisualsBuffer = event.data;
|
pendingVisualsBuffer = event.data;
|
||||||
|
scheduleVisualsBufferPump(env);
|
||||||
};
|
};
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
if (phoenixVisualsSocket === socket) phoenixVisualsSocket = null;
|
if (phoenixVisualsSocket === socket) phoenixVisualsSocket = null;
|
||||||
@@ -324,6 +355,22 @@ function openPhoenixVisualsSocket(baseUrl, env) {
|
|||||||
socket.onclose = retry;
|
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) {
|
function applyVisualsBuffer(env, buffer) {
|
||||||
const decoded = decodePhoenixVisualsBuffer(buffer);
|
const decoded = decodePhoenixVisualsBuffer(buffer);
|
||||||
if (!decoded) return;
|
if (!decoded) return;
|
||||||
@@ -579,49 +626,32 @@ function mergePendingPhoenixFrame(previous, next) {
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueuePhoenixFrame(frame) {
|
function schedulePhoenixPacketPump(env, CONFIG) {
|
||||||
pendingPhoenixFrame = mergePendingPhoenixFrame(pendingPhoenixFrame, frame);
|
if (phoenixPacketBusy || !pendingPhoenixFrame) return;
|
||||||
}
|
phoenixPacketBusy = true;
|
||||||
|
void (async () => {
|
||||||
// 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 {
|
try {
|
||||||
const packet = buildPhoenixMeterPacket(metricsFrame);
|
// 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());
|
await applyIncomingAudioPacket(env, packet, CONFIG, performance.now());
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Phoenix packet error:', err);
|
console.warn('Phoenix packet error:', err);
|
||||||
|
} finally {
|
||||||
|
phoenixPacketBusy = false;
|
||||||
|
if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visualsBuffer) {
|
function enqueuePhoenixFrame(env, CONFIG, frame) {
|
||||||
try {
|
pendingPhoenixFrame = mergePendingPhoenixFrame(pendingPhoenixFrame, frame);
|
||||||
applyVisualsBuffer(env, visualsBuffer);
|
schedulePhoenixPacketPump(env, CONFIG);
|
||||||
} 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() {
|
function bindLifecycleHandlers() {
|
||||||
@@ -1144,7 +1174,7 @@ async function initPhoenixAudio(env) {
|
|||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const frame = JSON.parse(event.data);
|
const frame = JSON.parse(event.data);
|
||||||
enqueuePhoenixFrame(frame);
|
enqueuePhoenixFrame(env, CONFIG, frame);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Phoenix metrics parse error:', err);
|
console.warn('Phoenix metrics parse error:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -23,7 +23,7 @@ try {
|
|||||||
import { CONFIG, applyRtaBpoSelection, loadConfig, saveConfig, loadLayoutPreset } from './core/config.js';
|
import { CONFIG, applyRtaBpoSelection, loadConfig, saveConfig, loadLayoutPreset } from './core/config.js';
|
||||||
import * as utils from './core/utils.js';
|
import * as utils from './core/utils.js';
|
||||||
import { meterFacade, registerMeter } from './core/registry.js';
|
import { meterFacade, registerMeter } from './core/registry.js';
|
||||||
import { initAudio, audioLost, reloadAudio, flushPendingPhoenixData } from './core/audio.js';
|
import { initAudio, audioLost, reloadAudio } from './core/audio.js';
|
||||||
import { createScreensaver } from './core/screensaver.js';
|
import { createScreensaver } from './core/screensaver.js';
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
@@ -2675,10 +2675,6 @@ async function maybeRecoverAudio(reason) {
|
|||||||
|
|
||||||
async function loop(now){
|
async function loop(now){
|
||||||
const elapsed = now - lastFrameTime;
|
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 audioOk = env.audio.alive && !audioLost(env);
|
||||||
const renderStyle = getRenderableStyle();
|
const renderStyle = getRenderableStyle();
|
||||||
if (processingProfileDirty || appliedProcessingProfileView !== renderStyle) {
|
if (processingProfileDirty || appliedProcessingProfileView !== renderStyle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user