Optimize realtime display processing
This commit is contained in:
+9
-1
@@ -102,6 +102,8 @@ pub struct AudioWorkerDeps {
|
|||||||
pub actual_sample_rate: Arc<AtomicU64>,
|
pub actual_sample_rate: Arc<AtomicU64>,
|
||||||
pub metrics_tx: tokio::sync::broadcast::Sender<Arc<MeterFrame>>,
|
pub metrics_tx: tokio::sync::broadcast::Sender<Arc<MeterFrame>>,
|
||||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
|
pub spectro_subscribers: Arc<AtomicU64>,
|
||||||
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
pub restart_token: Arc<AtomicU64>,
|
pub restart_token: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +439,7 @@ fn capture_until_restart(deps: AudioWorkerDeps, generation: u64) -> anyhow::Resu
|
|||||||
&deps.seq,
|
&deps.seq,
|
||||||
&mut ppm,
|
&mut ppm,
|
||||||
&rta_config,
|
&rta_config,
|
||||||
|
deps.spectro_subscribers.load(Ordering::Relaxed) > 0,
|
||||||
);
|
);
|
||||||
let _ = deps.metrics_tx.send(Arc::new(frame));
|
let _ = deps.metrics_tx.send(Arc::new(frame));
|
||||||
}
|
}
|
||||||
@@ -1078,6 +1081,7 @@ fn build_meter_frame(
|
|||||||
seq: &Arc<AtomicU64>,
|
seq: &Arc<AtomicU64>,
|
||||||
ppm_state: &mut PpmState,
|
ppm_state: &mut PpmState,
|
||||||
rta_config: &PhoenixRtaConfig,
|
rta_config: &PhoenixRtaConfig,
|
||||||
|
spectro_requested: bool,
|
||||||
) -> MeterFrame {
|
) -> MeterFrame {
|
||||||
let mut rms_power_l = 0.0f32;
|
let mut rms_power_l = 0.0f32;
|
||||||
let mut rms_power_r = 0.0f32;
|
let mut rms_power_r = 0.0f32;
|
||||||
@@ -1172,10 +1176,14 @@ fn build_meter_frame(
|
|||||||
}
|
}
|
||||||
let mut spectro_frame = None;
|
let mut spectro_frame = None;
|
||||||
if let Some(state) = ppm_state.spectro_state.as_mut() {
|
if let Some(state) = ppm_state.spectro_state.as_mut() {
|
||||||
if finalize_spectro_state(state, sample_rate) {
|
if spectro_requested && finalize_spectro_state(state, sample_rate) {
|
||||||
let frame = build_spectro_frame(state, sample_rate);
|
let frame = build_spectro_frame(state, sample_rate);
|
||||||
ppm_state.last_spectro = Some(frame.clone());
|
ppm_state.last_spectro = Some(frame.clone());
|
||||||
spectro_frame = Some(frame);
|
spectro_frame = Some(frame);
|
||||||
|
} else if !spectro_requested && state.ring_fill >= state.fft_size {
|
||||||
|
// Keep the current audio window warm, but do not accumulate a
|
||||||
|
// transform backlog while no client displays the spectrogram.
|
||||||
|
state.samples_since = state.fft_step_samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -959,6 +959,12 @@ async fn visuals_ws_inner(mut socket: axum::extract::ws::WebSocket, state: AppSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn spectro_ws_inner(mut socket: axum::extract::ws::WebSocket, state: AppState) {
|
async fn spectro_ws_inner(mut socket: axum::extract::ws::WebSocket, state: AppState) {
|
||||||
|
state.spectro_subscriber_connected();
|
||||||
|
spectro_ws_session(&mut socket, &state).await;
|
||||||
|
state.spectro_subscriber_disconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spectro_ws_session(socket: &mut axum::extract::ws::WebSocket, state: &AppState) {
|
||||||
let mut rx = state.subscribe_metrics();
|
let mut rx = state.subscribe_metrics();
|
||||||
loop {
|
loop {
|
||||||
let mut latest = loop {
|
let mut latest = loop {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct AppState {
|
|||||||
seq: Arc<AtomicU64>,
|
seq: Arc<AtomicU64>,
|
||||||
actual_sample_rate: Arc<AtomicU64>,
|
actual_sample_rate: Arc<AtomicU64>,
|
||||||
metrics_tx: broadcast::Sender<Arc<MeterFrame>>,
|
metrics_tx: broadcast::Sender<Arc<MeterFrame>>,
|
||||||
|
spectro_subscribers: Arc<AtomicU64>,
|
||||||
restart_token: Arc<AtomicU64>,
|
restart_token: Arc<AtomicU64>,
|
||||||
rta_config: Arc<RwLock<PhoenixRtaConfig>>,
|
rta_config: Arc<RwLock<PhoenixRtaConfig>>,
|
||||||
global_config: Arc<RwLock<PhoenixGlobalConfig>>,
|
global_config: Arc<RwLock<PhoenixGlobalConfig>>,
|
||||||
@@ -105,6 +106,7 @@ impl AppState {
|
|||||||
seq: Arc::new(AtomicU64::new(0)),
|
seq: Arc::new(AtomicU64::new(0)),
|
||||||
actual_sample_rate: Arc::new(AtomicU64::new(configured_sample_rate as u64)),
|
actual_sample_rate: Arc::new(AtomicU64::new(configured_sample_rate as u64)),
|
||||||
metrics_tx,
|
metrics_tx,
|
||||||
|
spectro_subscribers: Arc::new(AtomicU64::new(0)),
|
||||||
restart_token: Arc::new(AtomicU64::new(0)),
|
restart_token: Arc::new(AtomicU64::new(0)),
|
||||||
rta_config: Arc::new(RwLock::new(initial_rta_config)),
|
rta_config: Arc::new(RwLock::new(initial_rta_config)),
|
||||||
global_config: Arc::new(RwLock::new(initial_global_config)),
|
global_config: Arc::new(RwLock::new(initial_global_config)),
|
||||||
@@ -117,6 +119,18 @@ impl AppState {
|
|||||||
self.metrics_tx.subscribe()
|
self.metrics_tx.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn spectro_subscriber_connected(&self) {
|
||||||
|
self.spectro_subscribers.fetch_add(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spectro_subscriber_disconnected(&self) {
|
||||||
|
let _ =
|
||||||
|
self.spectro_subscribers
|
||||||
|
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
|
||||||
|
Some(count.saturating_sub(1))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn input(&self) -> InputSource {
|
pub async fn input(&self) -> InputSource {
|
||||||
*self.current_input.read().await
|
*self.current_input.read().await
|
||||||
}
|
}
|
||||||
@@ -268,6 +282,7 @@ impl AppState {
|
|||||||
seq: self.seq.clone(),
|
seq: self.seq.clone(),
|
||||||
actual_sample_rate: self.actual_sample_rate.clone(),
|
actual_sample_rate: self.actual_sample_rate.clone(),
|
||||||
metrics_tx: self.metrics_tx.clone(),
|
metrics_tx: self.metrics_tx.clone(),
|
||||||
|
spectro_subscribers: self.spectro_subscribers.clone(),
|
||||||
restart_token: self.restart_token.clone(),
|
restart_token: self.restart_token.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -9,6 +9,7 @@ import { getRtwCenters } from './rtw_centers.js';
|
|||||||
let phoenixSocket = null;
|
let phoenixSocket = null;
|
||||||
let phoenixSpectroSocket = null;
|
let phoenixSpectroSocket = null;
|
||||||
let phoenixSpectroRetryTimer = null;
|
let phoenixSpectroRetryTimer = null;
|
||||||
|
let phoenixSpectroDemanded = false;
|
||||||
let pendingSpectroBuffer = null;
|
let pendingSpectroBuffer = null;
|
||||||
let phoenixSpectroRaf = 0;
|
let phoenixSpectroRaf = 0;
|
||||||
let phoenixVisualsSocket = null;
|
let phoenixVisualsSocket = null;
|
||||||
@@ -99,6 +100,7 @@ function closePhoenixSocket() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
phoenixSpectroSocket = null;
|
phoenixSpectroSocket = null;
|
||||||
|
phoenixSpectroDemanded = false;
|
||||||
pendingSpectroBuffer = null;
|
pendingSpectroBuffer = null;
|
||||||
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||||
phoenixSpectroRaf = 0;
|
phoenixSpectroRaf = 0;
|
||||||
@@ -253,6 +255,7 @@ function copyPhoenixSpectroBins(audioState, spectro, frameDelta = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPhoenixSpectroSocket(baseUrl, env) {
|
function openPhoenixSpectroSocket(baseUrl, env) {
|
||||||
|
if (!phoenixSpectroDemanded) return;
|
||||||
if (phoenixSpectroSocket && (
|
if (phoenixSpectroSocket && (
|
||||||
phoenixSpectroSocket.readyState === WebSocket.OPEN
|
phoenixSpectroSocket.readyState === WebSocket.OPEN
|
||||||
|| phoenixSpectroSocket.readyState === WebSocket.CONNECTING
|
|| phoenixSpectroSocket.readyState === WebSocket.CONNECTING
|
||||||
@@ -269,7 +272,7 @@ function openPhoenixSpectroSocket(baseUrl, env) {
|
|||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
if (phoenixSpectroSocket === socket) phoenixSpectroSocket = null;
|
if (phoenixSpectroSocket === socket) phoenixSpectroSocket = null;
|
||||||
if (!phoenixSocket || phoenixSocket.readyState !== WebSocket.OPEN) return;
|
if (!phoenixSpectroDemanded || !phoenixSocket || phoenixSocket.readyState !== WebSocket.OPEN) return;
|
||||||
if (phoenixSpectroRetryTimer) clearTimeout(phoenixSpectroRetryTimer);
|
if (phoenixSpectroRetryTimer) clearTimeout(phoenixSpectroRetryTimer);
|
||||||
phoenixSpectroRetryTimer = setTimeout(() => {
|
phoenixSpectroRetryTimer = setTimeout(() => {
|
||||||
phoenixSpectroRetryTimer = null;
|
phoenixSpectroRetryTimer = null;
|
||||||
@@ -280,6 +283,31 @@ function openPhoenixSpectroSocket(baseUrl, env) {
|
|||||||
socket.onclose = retry;
|
socket.onclose = retry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPhoenixSpectroDemand(baseUrl, env, demanded) {
|
||||||
|
const next = !!demanded;
|
||||||
|
phoenixSpectroDemanded = next;
|
||||||
|
if (next) {
|
||||||
|
if (phoenixSocket?.readyState === WebSocket.OPEN) openPhoenixSpectroSocket(baseUrl, env);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phoenixSpectroRetryTimer) clearTimeout(phoenixSpectroRetryTimer);
|
||||||
|
phoenixSpectroRetryTimer = null;
|
||||||
|
if (phoenixSpectroSocket) {
|
||||||
|
const socket = phoenixSpectroSocket;
|
||||||
|
phoenixSpectroSocket = null;
|
||||||
|
try {
|
||||||
|
socket.onopen = null;
|
||||||
|
socket.onmessage = null;
|
||||||
|
socket.onerror = null;
|
||||||
|
socket.onclose = null;
|
||||||
|
socket.close();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
pendingSpectroBuffer = null;
|
||||||
|
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||||
|
phoenixSpectroRaf = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleSpectroBufferPump(env) {
|
function scheduleSpectroBufferPump(env) {
|
||||||
if (phoenixSpectroRaf || !pendingSpectroBuffer) return;
|
if (phoenixSpectroRaf || !pendingSpectroBuffer) return;
|
||||||
phoenixSpectroRaf = requestAnimationFrame(() => {
|
phoenixSpectroRaf = requestAnimationFrame(() => {
|
||||||
@@ -1043,6 +1071,7 @@ async function initPhoenixAudio(env) {
|
|||||||
? cfg
|
? cfg
|
||||||
: ((typeof env?.getProcessingProfile === 'function') ? env.getProcessingProfile() : null);
|
: ((typeof env?.getProcessingProfile === 'function') ? env.getProcessingProfile() : null);
|
||||||
const profile = rawProfile || {};
|
const profile = rawProfile || {};
|
||||||
|
setPhoenixSpectroDemand(baseUrl, env, profile.needSpectro);
|
||||||
if (!profile.needXy) {
|
if (!profile.needXy) {
|
||||||
env.audio.xyL = null;
|
env.audio.xyL = null;
|
||||||
env.audio.xyR = null;
|
env.audio.xyR = null;
|
||||||
@@ -1057,6 +1086,9 @@ async function initPhoenixAudio(env) {
|
|||||||
resetWaveformStateFallback(env.audio.waveformFallback);
|
resetWaveformStateFallback(env.audio.waveformFallback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
env.audio.updateProcessingConfig(
|
||||||
|
(typeof env?.getProcessingProfile === 'function') ? env.getProcessingProfile() : null,
|
||||||
|
);
|
||||||
|
|
||||||
const pushPhoenixGlobalConfig = async () => {
|
const pushPhoenixGlobalConfig = async () => {
|
||||||
const response = await requestPhoenixGlobalConfigUpdate(baseUrl, buildPhoenixGlobalConfigPayload());
|
const response = await requestPhoenixGlobalConfigUpdate(baseUrl, buildPhoenixGlobalConfigPayload());
|
||||||
@@ -1105,7 +1137,7 @@ async function initPhoenixAudio(env) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
openPhoenixSpectroSocket(baseUrl, env);
|
if (phoenixSpectroDemanded) openPhoenixSpectroSocket(baseUrl, env);
|
||||||
openPhoenixVisualsSocket(baseUrl, env);
|
openPhoenixVisualsSocket(baseUrl, env);
|
||||||
finish(true);
|
finish(true);
|
||||||
};
|
};
|
||||||
|
|||||||
+32
-5
@@ -716,6 +716,7 @@ function buildProcessingProfile(viewId = style) {
|
|||||||
const profile = {
|
const profile = {
|
||||||
needXy: false,
|
needXy: false,
|
||||||
needRta: false,
|
needRta: false,
|
||||||
|
needSpectro: false,
|
||||||
needVu: false,
|
needVu: false,
|
||||||
needPpmDin: false,
|
needPpmDin: false,
|
||||||
needPpmEbu: false,
|
needPpmEbu: false,
|
||||||
@@ -728,6 +729,8 @@ function buildProcessingProfile(viewId = style) {
|
|||||||
for (const plotId of plotIds) {
|
for (const plotId of plotIds) {
|
||||||
if (plotId === 'realtime') {
|
if (plotId === 'realtime') {
|
||||||
profile.needRta = true;
|
profile.needRta = true;
|
||||||
|
} else if (plotId === 'spectrogram') {
|
||||||
|
profile.needSpectro = true;
|
||||||
} else if (plotId === 'goniometer-rtw') {
|
} else if (plotId === 'goniometer-rtw') {
|
||||||
profile.needXy = true;
|
profile.needXy = true;
|
||||||
profile.needRms = true;
|
profile.needRms = true;
|
||||||
@@ -792,6 +795,7 @@ function setSlotForView(viewId, idx, value){
|
|||||||
try { saveConfig?.(); } catch (_) {}
|
try { saveConfig?.(); } catch (_) {}
|
||||||
try { env?.audio?.updatePhoenixGlobalConfig?.(); } catch (_) {}
|
try { env?.audio?.updatePhoenixGlobalConfig?.(); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
requestRender('processing-profile-change');
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAllSlotsToDefaults() {
|
function resetAllSlotsToDefaults() {
|
||||||
@@ -804,6 +808,7 @@ function resetAllSlotsToDefaults() {
|
|||||||
saveSlotState(slotsState);
|
saveSlotState(slotsState);
|
||||||
saveSlotCustomizedState({});
|
saveSlotCustomizedState({});
|
||||||
refreshSlotEditors();
|
refreshSlotEditors();
|
||||||
|
requestRender('processing-profile-change');
|
||||||
}
|
}
|
||||||
function initSlotSelect(sel, idx){
|
function initSlotSelect(sel, idx){
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
@@ -1007,11 +1012,13 @@ initSplitPlotSelect(splitLeftSel, (val) => {
|
|||||||
const clean = sanitizeSplitPlotId(val) ?? 'none';
|
const clean = sanitizeSplitPlotId(val) ?? 'none';
|
||||||
CONFIG.SPLIT_VIEW_LEFT = clean;
|
CONFIG.SPLIT_VIEW_LEFT = clean;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
requestRender('processing-profile-change');
|
||||||
});
|
});
|
||||||
initSplitPlotSelect(splitRightSel, (val) => {
|
initSplitPlotSelect(splitRightSel, (val) => {
|
||||||
const clean = sanitizeSplitPlotId(val) ?? 'none';
|
const clean = sanitizeSplitPlotId(val) ?? 'none';
|
||||||
CONFIG.SPLIT_VIEW_RIGHT = clean;
|
CONFIG.SPLIT_VIEW_RIGHT = clean;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
requestRender('processing-profile-change');
|
||||||
});
|
});
|
||||||
|
|
||||||
function clampMeterCount3(v) {
|
function clampMeterCount3(v) {
|
||||||
@@ -1053,6 +1060,7 @@ function writeMeterToConfig(prefix, idx, val) {
|
|||||||
const key = getMeterKey(prefix, idx);
|
const key = getMeterKey(prefix, idx);
|
||||||
CONFIG[key] = sanitizeSlotId(val) ?? 'vu';
|
CONFIG[key] = sanitizeSlotId(val) ?? 'vu';
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
requestRender('processing-profile-change');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSplitMeterPopupOpen(open) {
|
function setSplitMeterPopupOpen(open) {
|
||||||
@@ -1098,10 +1106,10 @@ function syncQuadPlotPopup() {
|
|||||||
if (quadPlotSelBR) quadPlotSelBR.value = sanitizeSplitPlotId(CONFIG.QUAD_VIEW_BR) ?? 'none';
|
if (quadPlotSelBR) quadPlotSelBR.value = sanitizeSplitPlotId(CONFIG.QUAD_VIEW_BR) ?? 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
initSplitPlotSelect(quadPlotSelTL, (val) => { CONFIG.QUAD_VIEW_TL = sanitizeSplitPlotId(val) ?? 'phase-wheel'; saveConfig(); });
|
initSplitPlotSelect(quadPlotSelTL, (val) => { CONFIG.QUAD_VIEW_TL = sanitizeSplitPlotId(val) ?? 'phase-wheel'; saveConfig(); requestRender('processing-profile-change'); });
|
||||||
initSplitPlotSelect(quadPlotSelTR, (val) => { CONFIG.QUAD_VIEW_TR = sanitizeSplitPlotId(val) ?? 'realtime'; saveConfig(); });
|
initSplitPlotSelect(quadPlotSelTR, (val) => { CONFIG.QUAD_VIEW_TR = sanitizeSplitPlotId(val) ?? 'realtime'; saveConfig(); requestRender('processing-profile-change'); });
|
||||||
initSplitPlotSelect(quadPlotSelBL, (val) => { CONFIG.QUAD_VIEW_BL = sanitizeSplitPlotId(val) ?? 'goniometer-rtw'; saveConfig(); });
|
initSplitPlotSelect(quadPlotSelBL, (val) => { CONFIG.QUAD_VIEW_BL = sanitizeSplitPlotId(val) ?? 'goniometer-rtw'; saveConfig(); requestRender('processing-profile-change'); });
|
||||||
initSplitPlotSelect(quadPlotSelBR, (val) => { CONFIG.QUAD_VIEW_BR = sanitizeSplitPlotId(val) ?? 'none'; saveConfig(); });
|
initSplitPlotSelect(quadPlotSelBR, (val) => { CONFIG.QUAD_VIEW_BR = sanitizeSplitPlotId(val) ?? 'none'; saveConfig(); requestRender('processing-profile-change'); });
|
||||||
|
|
||||||
if (splitMeterBtn) {
|
if (splitMeterBtn) {
|
||||||
splitMeterBtn.addEventListener('click', () => {
|
splitMeterBtn.addEventListener('click', () => {
|
||||||
@@ -1857,10 +1865,12 @@ function handleSplitTapUp(x, y) {
|
|||||||
if (env?.splitPopup?.open) {
|
if (env?.splitPopup?.open) {
|
||||||
env.splitPopup.open = false;
|
env.splitPopup.open = false;
|
||||||
env.splitPopup.viewId = null;
|
env.splitPopup.viewId = null;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
env.splitPopup.open = true;
|
env.splitPopup.open = true;
|
||||||
env.splitPopup.viewId = target;
|
env.splitPopup.viewId = target;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
env.switchView?.(target);
|
env.switchView?.(target);
|
||||||
@@ -1877,6 +1887,7 @@ function handleQuadTapUp(x, y) {
|
|||||||
// Popup ist offen: Tap innerhalb/außerhalb schließt
|
// Popup ist offen: Tap innerhalb/außerhalb schließt
|
||||||
env.quadPopup.open = false;
|
env.quadPopup.open = false;
|
||||||
env.quadPopup.viewId = null;
|
env.quadPopup.viewId = null;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isInAnyMeterHitRect(x, y)) return;
|
if (isInAnyMeterHitRect(x, y)) return;
|
||||||
@@ -1889,6 +1900,7 @@ function handleQuadTapUp(x, y) {
|
|||||||
if (env?.quadPopup) {
|
if (env?.quadPopup) {
|
||||||
env.quadPopup.open = true;
|
env.quadPopup.open = true;
|
||||||
env.quadPopup.viewId = target;
|
env.quadPopup.viewId = target;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1912,6 +1924,7 @@ C.addEventListener('pointerdown', (e) => {
|
|||||||
if (!env?.splitPopup?.open) return;
|
if (!env?.splitPopup?.open) return;
|
||||||
env.splitPopup.open = false;
|
env.splitPopup.open = false;
|
||||||
env.splitPopup.viewId = null;
|
env.splitPopup.viewId = null;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
try { e.preventDefault?.(); } catch (_) {}
|
try { e.preventDefault?.(); } catch (_) {}
|
||||||
try { e.stopPropagation?.(); } catch (_) {}
|
try { e.stopPropagation?.(); } catch (_) {}
|
||||||
}, { capture: true, passive: false });
|
}, { capture: true, passive: false });
|
||||||
@@ -1968,6 +1981,7 @@ C.addEventListener('pointerdown', (e) => {
|
|||||||
if (pointInRect(x, y, box)) return;
|
if (pointInRect(x, y, box)) return;
|
||||||
env.quadPopup.open = false;
|
env.quadPopup.open = false;
|
||||||
env.quadPopup.viewId = null;
|
env.quadPopup.viewId = null;
|
||||||
|
requestRender('processing-profile-change');
|
||||||
try { e.preventDefault?.(); } catch (_) {}
|
try { e.preventDefault?.(); } catch (_) {}
|
||||||
try { e.stopPropagation?.(); } catch (_) {}
|
try { e.stopPropagation?.(); } catch (_) {}
|
||||||
}, { capture: true, passive: false });
|
}, { capture: true, passive: false });
|
||||||
@@ -2612,9 +2626,14 @@ let audioRecoverInFlight = false;
|
|||||||
let lastAudioRecoverAt = 0;
|
let lastAudioRecoverAt = 0;
|
||||||
let renderDirty = true;
|
let renderDirty = true;
|
||||||
let lastRenderedAudioSeq = 0;
|
let lastRenderedAudioSeq = 0;
|
||||||
|
let processingProfileDirty = true;
|
||||||
|
let appliedProcessingProfileView = '';
|
||||||
|
|
||||||
|
const DATA_ONLY_RENDER_REASONS = new Set(['audio', 'spectro', 'visuals']);
|
||||||
|
|
||||||
function requestRender(reason = 'ui') {
|
function requestRender(reason = 'ui') {
|
||||||
renderDirty = true;
|
renderDirty = true;
|
||||||
|
if (!DATA_ONLY_RENDER_REASONS.has(reason)) processingProfileDirty = true;
|
||||||
env.__lastRenderReason = reason;
|
env.__lastRenderReason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2647,7 +2666,15 @@ async function loop(now){
|
|||||||
const elapsed = now - lastFrameTime;
|
const elapsed = now - lastFrameTime;
|
||||||
const audioOk = env.audio.alive && !audioLost(env);
|
const audioOk = env.audio.alive && !audioLost(env);
|
||||||
const renderStyle = getRenderableStyle();
|
const renderStyle = getRenderableStyle();
|
||||||
try { env.audio?.updateProcessingConfig?.(buildProcessingProfile(renderStyle)); } catch (_) {}
|
if (processingProfileDirty || appliedProcessingProfileView !== renderStyle) {
|
||||||
|
try {
|
||||||
|
if (typeof env.audio?.updateProcessingConfig === 'function') {
|
||||||
|
env.audio.updateProcessingConfig(buildProcessingProfile(renderStyle));
|
||||||
|
processingProfileDirty = false;
|
||||||
|
appliedProcessingProfileView = renderStyle;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
const targetMs = audioOk ? (1000 / desiredFpsForStyle(renderStyle)) : (1000 / 20);
|
const targetMs = audioOk ? (1000 / desiredFpsForStyle(renderStyle)) : (1000 / 20);
|
||||||
const target = targetMs;
|
const target = targetMs;
|
||||||
const currentAudioSeq = Number.isFinite(env.audio?.xySeq) ? env.audio.xySeq : 0;
|
const currentAudioSeq = Number.isFinite(env.audio?.xySeq) ? env.audio.xySeq : 0;
|
||||||
|
|||||||
+69
-18
@@ -67,6 +67,12 @@ export function init() {
|
|||||||
ampBuffer: new Float32Array(0),
|
ampBuffer: new Float32Array(0),
|
||||||
filteredL: new Float32Array(0),
|
filteredL: new Float32Array(0),
|
||||||
filteredR: new Float32Array(0),
|
filteredR: new Float32Array(0),
|
||||||
|
phaseAngleBuffer: new Float32Array(0),
|
||||||
|
phaseAmplitudeBuffer: new Float32Array(0),
|
||||||
|
phaseAnalysisSeq: -1,
|
||||||
|
phaseAnalysisLength: 0,
|
||||||
|
phaseAnalysisSampleRate: 0,
|
||||||
|
phaseAnalysisCount: 0,
|
||||||
currentPhase: null,
|
currentPhase: null,
|
||||||
currentRadius: 0,
|
currentRadius: 0,
|
||||||
smoothPhase: null,
|
smoothPhase: null,
|
||||||
@@ -100,8 +106,8 @@ export async function render(env, state) {
|
|||||||
|
|
||||||
drawStaticLayer(g, state, rect, layout, CONFIG, slots.length);
|
drawStaticLayer(g, state, rect, layout, CONFIG, slots.length);
|
||||||
const xyData = extractXYData(audio);
|
const xyData = extractXYData(audio);
|
||||||
const gainCtrl = resolvePhaseGain(state, xyData, CONFIG);
|
|
||||||
if (xyData.ready) {
|
if (xyData.ready) {
|
||||||
|
const gainCtrl = resolvePhaseGain(state, xyData, CONFIG);
|
||||||
const trace = buildWheelTrace(state, xyData, layout.wheel, gainCtrl.gain, CONFIG, audio);
|
const trace = buildWheelTrace(state, xyData, layout.wheel, gainCtrl.gain, CONFIG, audio);
|
||||||
renderWheel(g, trace, layout.wheel, state, CONFIG, now);
|
renderWheel(g, trace, layout.wheel, state, CONFIG, now);
|
||||||
} else {
|
} else {
|
||||||
@@ -330,6 +336,7 @@ function extractXYData(audio) {
|
|||||||
xyR,
|
xyR,
|
||||||
length: ready ? Math.min(xyL.length, xyR.length) : 0,
|
length: ready ? Math.min(xyL.length, xyR.length) : 0,
|
||||||
sampleRate: audio?.sampleRate || 48000,
|
sampleRate: audio?.sampleRate || 48000,
|
||||||
|
seq: Number(audio?.xySeq) || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,14 +345,12 @@ function buildWheelTrace(state, xyData, wheel, gain = 1, CONFIG, audio) {
|
|||||||
decayPhasePointer(state);
|
decayPhasePointer(state);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const filtered = preparePhaseFilteredBuffers(state, xyData);
|
const analysis = preparePhaseAnalysis(state, xyData);
|
||||||
const amplitudeMode = getPhaseAmplitudeMode(CONFIG);
|
const amplitudeMode = getPhaseAmplitudeMode(CONFIG);
|
||||||
const ringDbValues = getRingDbValues(CONFIG);
|
const ringDbValues = getRingDbValues(CONFIG);
|
||||||
const ppmRadiusNorm = amplitudeMode === 'ppm-din'
|
const ppmRadiusNorm = amplitudeMode === 'ppm-din'
|
||||||
? computePpmDinRadiusNorm(audio, CONFIG, ringDbValues)
|
? computePpmDinRadiusNorm(audio, CONFIG, ringDbValues)
|
||||||
: 0;
|
: 0;
|
||||||
const targetPoints = Math.min(TARGET_POINTS, xyData.length);
|
|
||||||
const step = Math.max(1, Math.floor(xyData.length / targetPoints));
|
|
||||||
const radius = wheel.radius;
|
const radius = wheel.radius;
|
||||||
let ampIdx = 0;
|
let ampIdx = 0;
|
||||||
let sumSin = 0;
|
let sumSin = 0;
|
||||||
@@ -354,20 +359,9 @@ function buildWheelTrace(state, xyData, wheel, gain = 1, CONFIG, audio) {
|
|||||||
let levelAcc = 0;
|
let levelAcc = 0;
|
||||||
const gainLinear = Number.isFinite(gain) ? gain : 1;
|
const gainLinear = Number.isFinite(gain) ? gain : 1;
|
||||||
|
|
||||||
for (let i = 0; i < xyData.length; i += step) {
|
for (let i = 0; i < analysis.count; i++) {
|
||||||
const lRe = clamp1(filtered.L[i]);
|
const angle = analysis.angles[i];
|
||||||
const rRe = clamp1(filtered.R[i]);
|
const amp = analysis.amplitudes[i];
|
||||||
const lIm = hilbertAt(filtered.L, i);
|
|
||||||
const rIm = hilbertAt(filtered.R, i);
|
|
||||||
const phaseL = Math.atan2(lIm, lRe);
|
|
||||||
const phaseR = Math.atan2(rIm, rRe);
|
|
||||||
let phaseDiff = phaseL - phaseR;
|
|
||||||
if (!Number.isFinite(phaseDiff)) continue;
|
|
||||||
phaseDiff = wrapAngle(phaseDiff);
|
|
||||||
const angle = phaseDiff - Math.PI / 2;
|
|
||||||
const magL = Math.min(1, Math.hypot(lRe, lIm));
|
|
||||||
const magR = Math.min(1, Math.hypot(rRe, rIm));
|
|
||||||
const amp = Math.min(1, 0.5 * (magL + magR));
|
|
||||||
const ampScaled = Math.min(1, amp * gainLinear);
|
const ampScaled = Math.min(1, amp * gainLinear);
|
||||||
const radiusNorm = amplitudeMode === 'ppm-din'
|
const radiusNorm = amplitudeMode === 'ppm-din'
|
||||||
? ppmRadiusNorm
|
? ppmRadiusNorm
|
||||||
@@ -809,6 +803,63 @@ function preparePhaseFilteredBuffers(state, xyData) {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensurePhaseAnalysisBuffers(state, length) {
|
||||||
|
if (!state.phaseAngleBuffer || state.phaseAngleBuffer.length < length) {
|
||||||
|
state.phaseAngleBuffer = new Float32Array(length);
|
||||||
|
}
|
||||||
|
if (!state.phaseAmplitudeBuffer || state.phaseAmplitudeBuffer.length < length) {
|
||||||
|
state.phaseAmplitudeBuffer = new Float32Array(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePhaseAnalysis(state, xyData) {
|
||||||
|
const sampleRate = Math.max(1, Math.round(xyData.sampleRate) || 48000);
|
||||||
|
const seq = Number(xyData.seq) || 0;
|
||||||
|
const canReuse = seq > 0
|
||||||
|
&& state.phaseAnalysisSeq === seq
|
||||||
|
&& state.phaseAnalysisLength === xyData.length
|
||||||
|
&& state.phaseAnalysisSampleRate === sampleRate;
|
||||||
|
if (canReuse) {
|
||||||
|
return {
|
||||||
|
angles: state.phaseAngleBuffer,
|
||||||
|
amplitudes: state.phaseAmplitudeBuffer,
|
||||||
|
count: state.phaseAnalysisCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = preparePhaseFilteredBuffers(state, xyData);
|
||||||
|
const targetPoints = Math.min(TARGET_POINTS, xyData.length);
|
||||||
|
const step = Math.max(1, Math.floor(xyData.length / targetPoints));
|
||||||
|
const sampleCount = Math.ceil(xyData.length / step);
|
||||||
|
ensurePhaseAnalysisBuffers(state, sampleCount);
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < xyData.length; i += step) {
|
||||||
|
const lRe = clamp1(filtered.L[i]);
|
||||||
|
const rRe = clamp1(filtered.R[i]);
|
||||||
|
const lIm = hilbertAt(filtered.L, i);
|
||||||
|
const rIm = hilbertAt(filtered.R, i);
|
||||||
|
const phaseL = Math.atan2(lIm, lRe);
|
||||||
|
const phaseR = Math.atan2(rIm, rRe);
|
||||||
|
let phaseDiff = phaseL - phaseR;
|
||||||
|
if (!Number.isFinite(phaseDiff)) continue;
|
||||||
|
phaseDiff = wrapAngle(phaseDiff);
|
||||||
|
const magL = Math.min(1, Math.hypot(lRe, lIm));
|
||||||
|
const magR = Math.min(1, Math.hypot(rRe, rIm));
|
||||||
|
state.phaseAngleBuffer[count] = phaseDiff - Math.PI / 2;
|
||||||
|
state.phaseAmplitudeBuffer[count] = Math.min(1, 0.5 * (magL + magR));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
state.phaseAnalysisSeq = seq;
|
||||||
|
state.phaseAnalysisLength = xyData.length;
|
||||||
|
state.phaseAnalysisSampleRate = sampleRate;
|
||||||
|
state.phaseAnalysisCount = count;
|
||||||
|
return {
|
||||||
|
angles: state.phaseAngleBuffer,
|
||||||
|
amplitudes: state.phaseAmplitudeBuffer,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function decayPhasePointer(state) {
|
function decayPhasePointer(state) {
|
||||||
const prevPhase = Number.isFinite(state.currentPhase) ? state.currentPhase : 0;
|
const prevPhase = Number.isFinite(state.currentPhase) ? state.currentPhase : 0;
|
||||||
const prevRadius = Number.isFinite(state.currentRadius) ? state.currentRadius : 0;
|
const prevRadius = Number.isFinite(state.currentRadius) ? state.currentRadius : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user