Fix RTA peak hold and FFT band energy

This commit is contained in:
Mikei386
2026-07-22 08:42:32 +02:00
parent caec1edfcf
commit 3a0bdbb5ad
13 changed files with 394 additions and 87 deletions
+58 -25
View File
@@ -180,7 +180,7 @@ export async function render(env, state) {
// Native IIR values already contain the selected power-domain
// integration. A second browser attack/hold stage would falsify it.
const display = displayBase;
applyPeakHold(state, integrated, CONFIG, range);
syncNativePeakHold(state, displayRtaPacket, CONFIG, range, state.mapping.length, 'iir');
state.displayLevels = display;
state.currentRange = range;
if (typeof window !== 'undefined') window.__RTA_STATE__ = state;
@@ -200,16 +200,20 @@ export async function render(env, state) {
);
}
} else if (useNativeFftEngine) {
const nativeLevels = mapNativeFftLevels(displayRtaPacket, CONFIG, range, state.mapping.length);
const ballisticsData = mapNativeFftLevels(displayRtaPacket, CONFIG, range, state.mapping.length);
const nativeLevels = ballisticsData?.primary;
if (!nativeLevels || !nativeLevels.length) {
drawWaiting(g, plotX, plotY);
} else {
const integrated = applyIntegration(state, nativeLevels, CONFIG, range);
// Native FFT packets already contain the selected power-domain
// integration. Integrating them again in the browser adds lag and
// changes the measured value.
const integrated = nativeLevels;
const displayBase = applyDisplayHold(state, integrated, CONFIG, range);
const display = (CONFIG.REALTIME_RENDER_STYLE || 'bars') === 'bars'
? applyRealtimeBarBallistics(state, displayBase, CONFIG, range)
: displayBase;
applyPeakHold(state, integrated, CONFIG, range);
syncNativePeakHold(state, displayRtaPacket, CONFIG, range, state.mapping.length, 'fft');
state.displayLevels = display;
state.currentRange = range;
if (typeof window !== 'undefined') window.__RTA_STATE__ = state;
@@ -224,8 +228,8 @@ export async function render(env, state) {
CONFIG,
range,
freqBounds,
null,
CONFIG.RTA_BALLISTICS_MODE || 'average'
ballisticsData?.overlay || null,
ballisticsData?.mode || CONFIG.RTA_BALLISTICS_MODE || 'average'
);
}
} else if (!analyser || !buf) {
@@ -286,7 +290,7 @@ function buildConfigSignature(CONFIG, nyq, binCount) {
CONFIG.RTA_INTEGRATION,
CONFIG.RTA_PEAK_HOLD_MODE,
CONFIG.RTA_PEAK_HOLD_SEC,
CONFIG.RTA_PEAK_DECAY_DB_PER_S,
CONFIG.RTA_PEAK_RESET_TOKEN,
CONFIG.RTA_DISPLAY_HOLD_SEC,
CONFIG.REALTIME_RENDER_STYLE,
CONFIG.REALTIME_BAR_HOLD_MS,
@@ -415,9 +419,7 @@ function applyPeakHold(state, levels, CONFIG, range) {
const holdSec = Number.isFinite(mapped)
? Math.max(0, mapped)
: Math.max(0, Number(CONFIG.RTA_PEAK_HOLD_SEC) || 0);
const decayRate = Math.max(0, Number(CONFIG.RTA_PEAK_DECAY_DB_PER_S) || 0);
const now = performance.now();
const dt = Math.max(0, (now - (state.holdSampleTs || now)) / 1000);
state.holdSampleTs = now;
if (!state.peakHold || state.peakHold.length !== len) {
@@ -440,18 +442,37 @@ function applyPeakHold(state, levels, CONFIG, range) {
continue;
}
if (mapped === Infinity || mode === 'manual') continue;
let allowDecay = mode === 'off';
if (mode === 'auto') {
allowDecay = (now - (state.lastPeakTime[i] || 0)) >= holdSec * 1000;
}
if (allowDecay && decayRate > 0) {
buffer[i] = Math.max(current, buffer[i] - decayRate * dt);
if (mode === 'auto' && (now - (state.lastPeakTime[i] || 0)) >= holdSec * 1000) {
buffer[i] = current;
state.lastPeakTime[i] = now;
}
buffer[i] = clamp(buffer[i], range.bottom, range.top);
}
return buffer;
}
function syncNativePeakHold(state, packet, CONFIG, range, expectedLen, engine) {
const src = isVectorLike(packet?.bands_peak) && packet.bands_peak.length === expectedLen
? packet.bands_peak
: null;
if (!src) return applyPeakHold(state, state.displayLevels, CONFIG, range);
const gain = engine === 'iir'
? (Number(CONFIG.RTA_DISPLAY_GAIN_IIR_DB ?? 0) || 0)
: (Number(CONFIG.RTA_DISPLAY_GAIN_FFT_DB ?? 0) || 0);
if (!state.peakHold || state.peakHold.length !== expectedLen) {
state.peakHold = new Float32Array(expectedLen);
}
for (let i = 0; i < expectedLen; i++) {
const value = Number(src[i]);
state.peakHold[i] = clamp(
(Number.isFinite(value) ? value : range.bottom) + gain,
range.bottom,
range.top,
);
}
return state.peakHold;
}
function applyDisplayHold(state, levels, CONFIG, range) {
const holdSec = Math.max(0, Number(CONFIG.RTA_DISPLAY_HOLD_SEC) || 0);
const len = levels.length;
@@ -912,16 +933,28 @@ function mapNativeFftLevels(packet, CONFIG, range, expectedLen) {
const bottom = (range && Number.isFinite(range.bottom)) ? range.bottom : FLOOR_DB;
const top = (range && Number.isFinite(range.top)) ? range.top : 9;
const gain = Number(CONFIG.RTA_DISPLAY_GAIN_FFT_DB ?? 0) || 0;
const src = isVectorLike(packet.bands_avg || packet.bands)
? (packet.bands_avg || packet.bands)
: null;
if (!src || (expectedLen && src.length !== expectedLen)) return null;
const out = new Float32Array(src.length);
for (let i = 0; i < src.length; i++) {
const v = Number(src[i]);
out[i] = clamp((Number.isFinite(v) ? v : bottom) + gain, bottom, top);
}
return out;
const ensureLen = (arr) => isVectorLike(arr) && (!expectedLen || arr.length === expectedLen) ? arr : null;
const avgRaw = ensureLen(packet.bands_avg || packet.bands);
const peakRaw = ensureLen(packet.bands_peak);
const requested = ['average', 'peak', 'both'].includes(CONFIG.RTA_BALLISTICS_MODE)
? CONFIG.RTA_BALLISTICS_MODE
: 'average';
const convert = (src) => {
if (!src) return null;
const out = new Float32Array(src.length);
for (let i = 0; i < src.length; i++) {
const value = Number(src[i]);
out[i] = clamp((Number.isFinite(value) ? value : bottom) + gain, bottom, top);
}
return out;
};
const average = convert(avgRaw);
const peak = convert(peakRaw);
if (requested === 'peak' && peak) return { primary: peak, overlay: null, mode: 'peak' };
if (requested === 'both' && average && peak) return { primary: average, overlay: peak, mode: 'both' };
if (average) return { primary: average, overlay: null, mode: 'average' };
if (peak) return { primary: peak, overlay: null, mode: 'peak' };
return null;
}
function isVectorLike(v) {