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