Fix RTA peak hold and FFT band energy
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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