Optimize realtime display processing

This commit is contained in:
Mikei386
2026-07-21 22:04:50 +02:00
parent a123539023
commit c067c73024
6 changed files with 165 additions and 26 deletions
+69 -18
View File
@@ -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;