Correct audio metering and realtime displays

This commit is contained in:
Mikei386
2026-07-22 10:50:15 +02:00
parent 97352ecfce
commit 2e868bfa3a
26 changed files with 999 additions and 903 deletions
+12 -67
View File
@@ -15,32 +15,25 @@ import { drawCachedStaticLayer } from './static_layer.js';
import { METER_HEADER_FONT } from './scale_helpers.js';
import { HEADER_BG, LABEL_COLOR, OK_COLOR, WARN_COLOR } from '../core/theme.js';
const RMS_FRAME_MS_NOMINAL = 16; // ~60 Hz UI-Refresh
const RMS_FRAME_MS_MAX = 40; // Obergrenze für dt in der Glättung
const RMS_FRAME_MS_SKIP = 250; // Heuristischer Schutz: riesige Gaps komplett skippen
export const id = 'rms';
export function initShared(CONFIG = {}) {
const offset = CONFIG.RMS_OFFSET_DB || 0;
const initVal = -60 + offset;
const initVal = -60;
return {
values: { L: initVal, R: initVal }, // intern immer dBFS RMS aus der Messkette
offset,
values: { L: initVal, R: initVal }, // unveränderte dBFS-RMS-Werte aus dem Backend
offset: 0,
lastValidL: initVal,
lastValidR: initVal,
_smooth: null,
};
}
export function update(packet, shared) {
const offset = shared.offset || 0;
const floor = -60 + offset;
const floor = -60;
if (!Number.isFinite(shared.lastValidL)) shared.lastValidL = floor;
if (!Number.isFinite(shared.lastValidR)) shared.lastValidR = floor;
if (Number.isFinite(packet?.rmsL)) {
const vL = packet.rmsL + offset;
const vL = packet.rmsL;
shared.values.L = vL;
shared.lastValidL = vL;
} else {
@@ -48,7 +41,7 @@ export function update(packet, shared) {
}
if (Number.isFinite(packet?.rmsR)) {
const vR = packet.rmsR + offset;
const vR = packet.rmsR;
shared.values.R = vR;
shared.lastValidR = vR;
} else {
@@ -65,6 +58,7 @@ export function draw(g, rect, CONFIG = {}, shared) {
// Anzeige-Umschaltung & Skala
const isDBU = (MODE === 'dbu');
const offset = Number.isFinite(CONFIG.RMS_OFFSET_DB) ? CONFIG.RMS_OFFSET_DB : 0;
const LOG_MIN = -60;
const LOG_TOP = isDBU ? +24 : 0;
@@ -120,35 +114,9 @@ export function draw(g, rect, CONFIG = {}, shared) {
g.fillRect(rect.x, rect.y - 24, rect.w, 24);
g.restore();
// IEC-ähnliche Zeitkonstanten (Impulse/Fast/Slow) für die Anzeige
const tauMs = resolveRmsTau(CONFIG);
if (tauMs > 0) {
const now = performance.now();
if (!shared._smooth) {
shared._smooth = {
L: shared.values.L,
R: shared.values.R,
lastTs: now - RMS_FRAME_MS_NOMINAL,
};
}
const lastTs = shared._smooth.lastTs ?? (now - RMS_FRAME_MS_NOMINAL);
const dtRawMs = Math.max(0, now - lastTs);
shared._smooth.lastTs = now;
if (dtRawMs <= RMS_FRAME_MS_SKIP) {
const dtUsedMs = Math.min(dtRawMs, RMS_FRAME_MS_MAX); // clamp dt to ignore occasional large gaps
const alpha = 1 - Math.exp(-dtUsedMs / tauMs);
shared._smooth.L += alpha * (shared.values.L - shared._smooth.L);
shared._smooth.R += alpha * (shared.values.R - shared._smooth.R);
}
} else {
shared._smooth = null;
}
// Werte in Anzeigeeinheit clampen
const rawDispL = toDisplay(shared._smooth?.L ?? shared.values.L);
const rawDispR = toDisplay(shared._smooth?.R ?? shared.values.R);
const smooth = smoothHeader(shared, rawDispL, rawDispR);
const rawDispL = toDisplay(shared.values.L + offset);
const rawDispR = toDisplay(shared.values.R + offset);
const vL = clamp(rawDispL, LOG_MIN, LOG_TOP);
const vR = clamp(rawDispR, LOG_MIN, LOG_TOP);
g.save();
@@ -167,11 +135,11 @@ export function draw(g, rect, CONFIG = {}, shared) {
const isRedL = rawDispL > RED_START;
const isRedR = rawDispR > RED_START;
g.fillStyle = isRedL ? WARN_COLOR : (CONFIG.HEADER_TEXT_COLOR || MID_COLOR);
g.fillText(fmt(smooth.L), centerLeft, yText);
g.fillText(fmt(rawDispL), centerLeft, yText);
g.fillStyle = CONFIG.HEADER_TEXT_COLOR || MID_COLOR;
g.fillText('|', centerX, yText);
g.fillStyle = isRedR ? WARN_COLOR : (CONFIG.HEADER_TEXT_COLOR || MID_COLOR);
g.fillText(fmt(smooth.R), centerRight, yText);
g.fillText(fmt(rawDispR), centerRight, yText);
} else {
const title = isDBU ? 'RMS (dBu)' : 'RMS (dBFS RMS)';
g.fillText(title, centerX, rect.y - 12);
@@ -284,7 +252,7 @@ function drawRmsStaticOverlay(g, shared, rect, CONFIG, geom) {
bottomValue: LOG_MIN,
highlightTick: null,
});
const unitLabel = 'dBFS (RMS)';
const unitLabel = isDBU ? 'dBu (RMS)' : 'dBFS (RMS)';
cg.fillStyle = LABEL_COLOR;
cg.textAlign = 'center';
const baseY = rect.y + rect.h + 16;
@@ -303,19 +271,6 @@ function drawRmsStaticOverlay(g, shared, rect, CONFIG, geom) {
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function resolveRmsTau(CONFIG = {}) {
const mode = String(CONFIG.RMS_TC_MODE || 'fast').toLowerCase();
const custom = Number(CONFIG.RMS_TC_MS);
if (Number.isFinite(custom) && custom > 0) return custom;
switch (mode) {
case 'impulse': return 35;
case 'slow': return 1000;
case 'none': return 0;
case 'fast':
default: return 125;
}
}
function drawRedStripeLR(g, leftX, barW, rightX, centerX, mapY, yWarn, yTop) {
const innerRight = leftX + barW;
const gapL = Math.abs(centerX - innerRight);
@@ -491,13 +446,3 @@ function getRmsAlignmentHighlight(CONFIG) {
color: WARN_COLOR,
};
}
function smoothHeader(shared, rawL, rawR, alpha = 0.2) {
if (!shared._header) {
shared._header = { L: rawL, R: rawR };
return shared._header;
}
shared._header.L += alpha * (rawL - shared._header.L);
shared._header.R += alpha * (rawR - shared._header.R);
return shared._header;
}
+34 -34
View File
@@ -1,25 +1,12 @@
// meters/tp.js — True Peak (dBTP) L/R mit nichtlinearer Skala, Warnschwelle und Glanzkante
// meters/tp.js — True Peak (dBTP) L/R mit Übersteuerungsreserve bis +6 dBTP
import { createPeakHoldState, stepPeakHold } from '../core/utils.js';
import { drawHairlineGrid, METER_HEADER_FONT } from './scale_helpers.js';
import { drawCachedStaticLayer } from './static_layer.js';
import { TRUE_PEAK_MINOR_TICKS, TRUE_PEAK_SCALE } from './true_peak_scale.js';
import { HEADER_BG, LABEL_COLOR, MID_COLOR, WARN_COLOR } from '../core/theme.js';
export const id = 'tp';
const TP_PERCENT_SCALE = [
{ db: -60, percent: '0,00 %', frac: 0.0000 },
{ db: -50, percent: '16,67 %', frac: 0.0373 },
{ db: -40, percent: '33,33 %', frac: 0.1429 },
{ db: -35, percent: '41,67 %', frac: 0.2112 },
{ db: -30, percent: '50,00 %', frac: 0.2857 },
{ db: -25, percent: '58,33 %', frac: 0.3851 },
{ db: -20, percent: '66,67 %', frac: 0.4783 },
{ db: -15, percent: '75,00 %', frac: 0.6087 },
{ db: -10, percent: '83,33 %', frac: 0.7391 },
{ db: -5, percent: '91,67 %', frac: 0.8696 },
{ db: 0, percent: '100,00 %', frac: 1.0000 },
];
export function initShared(CONFIG) {
const now = performance.now();
return {
@@ -42,22 +29,22 @@ export function update(packet, shared) {
shared.values.R = R + (shared.offset || 0);
}
const LOG_MIN = TP_PERCENT_SCALE[0].db;
const LOG_TOP = TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db;
const LOG_MIN = TRUE_PEAK_SCALE[0].db;
const LOG_TOP = TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db;
function interpolateFrac(db) {
if (db <= TP_PERCENT_SCALE[0].db) return TP_PERCENT_SCALE[0].frac;
if (db >= TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].db) return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
for (let i = 1; i < TP_PERCENT_SCALE.length; i++) {
const prev = TP_PERCENT_SCALE[i - 1];
const curr = TP_PERCENT_SCALE[i];
if (db <= TRUE_PEAK_SCALE[0].db) return TRUE_PEAK_SCALE[0].frac;
if (db >= TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].db) return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
for (let i = 1; i < TRUE_PEAK_SCALE.length; i++) {
const prev = TRUE_PEAK_SCALE[i - 1];
const curr = TRUE_PEAK_SCALE[i];
if (db <= curr.db) {
const span = curr.db - prev.db || 1;
const t = (db - prev.db) / span;
return prev.frac + t * (curr.frac - prev.frac);
}
}
return TP_PERCENT_SCALE[TP_PERCENT_SCALE.length - 1].frac;
return TRUE_PEAK_SCALE[TRUE_PEAK_SCALE.length - 1].frac;
}
export function draw(g, rect, CONFIG, shared) {
@@ -122,13 +109,30 @@ export function draw(g, rect, CONFIG, shared) {
g.font = prevFont;
g.restore();
const yFloor = mapY(LOG_MIN);
const configuredRedStart = Number(CONFIG.TP_RED_START);
const redStart = Math.max(LOG_MIN, Math.min(LOG_TOP, Number.isFinite(configuredRedStart) ? configuredRedStart : -1));
const yRed = mapY(redStart);
const innerW = Math.max(2, barW - 2);
const colNorm = CONFIG.TP_COLOR_NORMAL || MID_COLOR;
const colWarn = CONFIG.TP_COLOR_WARN || WARN_COLOR;
const redOnly = CONFIG.TP_RED_BAR_ONLY !== false;
const drawBar = (x0, val) => {
const yVal = mapY(val);
const colNorm = CONFIG.TP_COLOR_NORMAL;
g.fillStyle = colNorm;
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
if (val > redStart) {
if (redOnly) {
g.fillStyle = colNorm;
g.fillRect(x0 + 1, yRed, innerW, Math.max(0, yFloor - yRed));
g.fillStyle = colWarn;
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yRed - yVal));
} else {
g.fillStyle = colWarn;
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
}
} else {
g.fillStyle = colNorm;
g.fillRect(x0 + 1, yVal, innerW, Math.max(0, yFloor - yVal));
}
// Glanzkante
g.globalAlpha = .12;
g.fillStyle = '#fff';
@@ -211,9 +215,6 @@ function drawTpStaticOverlay(g, shared, rect, CONFIG, geom, mapY) {
});
}
// Balken-Overlay-Ticks auf Basis der Vintage-Prozent-Skala
const EXTRA_MINOR_TICKS = [-47, -19, -18, -17, -16, -14, -13, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1];
function drawOverlayTicksLR(g, leftX, rightX, widthPx, mapY, alignmentTick = null) {
if (!g) return;
@@ -254,14 +255,14 @@ function drawOverlayTicksLR(g, leftX, rightX, widthPx, mapY, alignmentTick = nul
g.beginPath(); g.moveTo(x1R, yPix); g.lineTo(x2R, yPix); g.stroke();
};
for (const point of TP_PERCENT_SCALE) {
for (const point of TRUE_PEAK_SCALE) {
const y = Math.round(mapY(point.db)) + 0.5;
const isHighlight = highlightValue !== null && approx(point.db, highlightValue);
if (isHighlight) highlightMatched = true;
drawMajor(y, isHighlight);
}
for (const db of EXTRA_MINOR_TICKS) {
for (const db of TRUE_PEAK_MINOR_TICKS) {
const y = Math.round(mapY(db)) + 0.5;
const isHighlight = highlightValue !== null && approx(db, highlightValue);
if (isHighlight) highlightMatched = true;
@@ -289,11 +290,10 @@ function drawScale(g, colRect, centerX, mapY, CONFIG) {
g.textAlign = 'center';
g.textBaseline = 'alphabetic';
for (const point of TP_PERCENT_SCALE) {
for (const point of TRUE_PEAK_SCALE) {
const y = mapY(point.db);
if (y < colRect.y || y > colRect.y + colRect.h) continue;
const value = Math.abs(point.db);
const dbLabel = Number.isInteger(value) ? String(value) : value.toFixed(1);
const dbLabel = point.db > 0 ? `+${point.db}` : String(point.db);
g.fillText(dbLabel, centerX, y + 5);
}
g.font = prevFont;
+22
View File
@@ -0,0 +1,22 @@
// Shared dBTP scale for every True-Peak presentation. The final 10% is kept
// above 0 dBTP so intersample overloads remain measurable up to +6 dBTP.
export const TRUE_PEAK_SCALE = Object.freeze([
{ db: -60, frac: 0.0000 },
{ db: -50, frac: 0.0336 },
{ db: -40, frac: 0.1286 },
{ db: -35, frac: 0.1901 },
{ db: -30, frac: 0.2571 },
{ db: -25, frac: 0.3466 },
{ db: -20, frac: 0.4305 },
{ db: -15, frac: 0.5478 },
{ db: -10, frac: 0.6652 },
{ db: -5, frac: 0.7826 },
{ db: 0, frac: 0.9000 },
{ db: +3, frac: 0.9500 },
{ db: +6, frac: 1.0000 },
]);
export const TRUE_PEAK_MINOR_TICKS = Object.freeze([
-47, -19, -18, -17, -16, -14, -13, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1,
+1, +2, +4, +5,
]);