Optimize realtime display processing
This commit is contained in:
+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