Fix RTA peak hold and FFT band energy
This commit is contained in:
+58
-25
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user