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
+5
View File
@@ -916,6 +916,11 @@ export function buildRtaRuntimeConfig(CONFIG = {}) {
tauSlow: CONFIG.RTA_IIR_TAU_SLOW || 1.0,
integration: CONFIG.RTA_INTEGRATION || 'fast',
layout,
rtaPeakHoldMode: ['off', 'manual'].includes(CONFIG.RTA_PEAK_HOLD_MODE)
? CONFIG.RTA_PEAK_HOLD_MODE
: 'auto',
rtaPeakHoldSeconds: Number.isFinite(CONFIG.RTA_PEAK_HOLD_SEC) ? CONFIG.RTA_PEAK_HOLD_SEC : 2.5,
rtaPeakResetToken: Math.max(0, Math.floor(Number(CONFIG.RTA_PEAK_RESET_TOKEN) || 0)),
inputOffsetDbL: Number.isFinite(CONFIG.INPUT_OFFSET_DB_L) ? CONFIG.INPUT_OFFSET_DB_L : -5,
inputOffsetDbR: Number.isFinite(CONFIG.INPUT_OFFSET_DB_R) ? CONFIG.INPUT_OFFSET_DB_R : -5,
ppmDinAttackMs: 10,
+5 -1
View File
@@ -64,7 +64,7 @@ const CONFIG = {
RTA_BALLISTICS_MODE: 'average', // 'average' | 'peak' | 'both'
RTA_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual'
RTA_PEAK_HOLD_SEC: 2.5,
RTA_PEAK_DECAY_DB_PER_S: 20,
RTA_PEAK_RESET_TOKEN: 0,
RTA_DISPLAY_HOLD_SEC: 0,
RTA_IIR_ORDER: 6,
RTA_IIR_TAU_FAST: 0.125,
@@ -810,6 +810,10 @@ function loadConfig(opts = {}) {
const hold = Number(CONFIG.RTA_PEAK_HOLD_SEC);
CONFIG.RTA_PEAK_HOLD_SEC = hold >= 3.25 ? 4.0 : 2.5;
}
CONFIG.RTA_PEAK_HOLD_MODE = ['off', 'auto', 'manual'].includes(CONFIG.RTA_PEAK_HOLD_MODE)
? CONFIG.RTA_PEAK_HOLD_MODE
: 'auto';
CONFIG.RTA_PEAK_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.RTA_PEAK_RESET_TOKEN) || 0));
if (!Number.isFinite(CONFIG.DBFS_TOP)) CONFIG.DBFS_TOP = CONFIG_DEFAULTS.DBFS_TOP;
if (!Number.isFinite(CONFIG.DBFS_BOTTOM)) CONFIG.DBFS_BOTTOM = CONFIG_DEFAULTS.DBFS_BOTTOM;
// Clamp to sensible bounds while keeping the user-selected display range.
+1 -5
View File
@@ -138,22 +138,18 @@ export function buildBandBinMapping(bands, nyq, binCount) {
const bins = [];
const start = Math.max(0, Math.floor((band.fLo / nyq) * binCount));
const end = Math.min(binCount - 1, Math.ceil((band.fHi / nyq) * binCount));
let weightSum = 0;
for (let i = start; i <= end; i++) {
const binStart = i * binWidth;
const binEnd = binStart + binWidth;
const overlap = Math.max(0, Math.min(binEnd, band.fHi) - Math.max(binStart, band.fLo));
if (overlap > 0) {
bins.push({ index: i, weight: overlap });
weightSum += overlap;
bins.push({ index: i, weight: Math.min(1, overlap / binWidth) });
}
}
if (!bins.length) {
const idx = Math.max(0, Math.min(binCount - 1, Math.round((band.center / nyq) * binCount)));
bins.push({ index: idx, weight: 1 });
weightSum = 1;
}
bins.forEach((b) => { b.weight /= weightSum || 1; });
result.push({
center: band.center,
fLo: band.fLo,
+1 -5
View File
@@ -389,16 +389,12 @@
<option value="auto" selected>Auto</option>
<option value="manual">Manuell</option>
</select>
<small>Peak-Hold Verhalten</small>
<small>Auto gibt den Peak nach 2,5 oder 4 Sekunden direkt auf den aktuellen Messwert frei; Manuell hält bis zum Reset.</small>
</div>
<div class="opt"><label>Hold Time (s)</label>
<input id="opt_rtaHoldTime" type="number" min="0" max="30" step="0.5" style="width:110px">
<small>Dauer der Hold-Anzeige</small>
</div>
<div class="opt"><label>Hold Decay (dB/s)</label>
<input id="opt_rtaDecay" type="number" min="1" max="60" step="1" style="width:110px">
<small>Abfall des Hold-Peaks</small>
</div>
<div class="opt"><label>Display Hold (s)</label>
<input id="opt_rtaDisplayHold" type="number" min="0" max="5" step="0.1" style="width:110px">
<small>Zusatzhalt für Anzeige</small>
+3
View File
@@ -2065,6 +2065,9 @@ if (peakHistScrollSel) {
}
if (resetPeakBtn) {
resetPeakBtn.addEventListener('click', () => {
CONFIG.RTA_PEAK_RESET_TOKEN = Math.max(0, Math.floor(Number(CONFIG.RTA_PEAK_RESET_TOKEN) || 0)) + 1;
saveConfig();
try { env.audio.updateRtaConfig?.(); } catch (_) {}
try {
if (typeof window.resetRealTimeAnalyzerPeakHold === 'function') {
window.resetRealTimeAnalyzerPeakHold();
-2
View File
@@ -293,7 +293,6 @@ function syncUI() {
['opt_rtaBallistics', CONFIG.RTA_BALLISTICS_MODE || 'average'],
['opt_rtaHoldMode', CONFIG.RTA_PEAK_HOLD_MODE || 'auto'],
['opt_rtaHoldTime', CONFIG.RTA_PEAK_HOLD_SEC ?? 2.5],
['opt_rtaDecay', CONFIG.RTA_PEAK_DECAY_DB_PER_S ?? 20],
['opt_rtaDisplayHold', CONFIG.RTA_DISPLAY_HOLD_SEC ?? 0],
['opt_spectroGamma', CONFIG.SPECTRO_GAMMA ?? 0.9, 'val_spectroGamma', (v)=>Number(v).toFixed(2)],
['opt_spectroScroll', CONFIG.SPECTRO_SCROLL_MODE ?? 1],
@@ -1175,7 +1174,6 @@ function wireHandlers(env) {
: requested;
return CONFIG.RTA_PEAK_HOLD_SEC;
});
h('opt_rtaDecay', v => { CONFIG.RTA_PEAK_DECAY_DB_PER_S = clamp(+v, 1, 60); });
h('opt_rtaDisplayHold', v => { CONFIG.RTA_DISPLAY_HOLD_SEC = clamp(+v, 0, 5); });
h('opt_spectroGamma', v => {
const num = clamp(parseFloat(v), 0.3, 1.2);
+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) {