Initial Phoenix analyzer
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
// meters/rms.js — True RMS (dBu/dBFS) L/R mit dualem Modus, Kalibrierung & Warnschwelle
|
||||
// Erwartet update(packet) mit packet.rmsL / packet.rmsR (in dBFS).
|
||||
// Offset via CONFIG.RMS_OFFSET_DB (wirkt vor der dBu-Umrechnung).
|
||||
//
|
||||
// Defaults jetzt DIGITAL:
|
||||
// - CONFIG.RMS_MODE bleibt auf 'dbfs', damit das Meter direkt dBFS ausgibt.
|
||||
// - CONFIG.RMS_REF_DBFS_FOR_REF_DBU: Referenz in dBFS RMS (default: -18 für 0 dBu)
|
||||
// - CONFIG.RMS_REF_DBU: Referenz in dBu RMS (default: 0)
|
||||
// -> Umrechnung: dBu = (dBFS_RMS - REF_DBFS) + REF_DBU, falls jemals benötigt.
|
||||
// - Titel passt sich an: "RMS (dBu)" oder "RMS (dBFS RMS)" je nach Modus.
|
||||
// - Skalenobergrenze: dBu: +24 (typisch Pro-Audio Headroom) / dBFS: +5
|
||||
// - Standard-Warnschwelle: dBu: +20 / dBFS: 0 (überschreibbar via CONFIG.RMS_RED_START)
|
||||
|
||||
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;
|
||||
return {
|
||||
values: { L: initVal, R: initVal }, // intern immer dBFS RMS aus der Messkette
|
||||
offset,
|
||||
lastValidL: initVal,
|
||||
lastValidR: initVal,
|
||||
_smooth: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function update(packet, shared) {
|
||||
const offset = shared.offset || 0;
|
||||
const floor = -60 + offset;
|
||||
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;
|
||||
shared.values.L = vL;
|
||||
shared.lastValidL = vL;
|
||||
} else {
|
||||
shared.values.L = shared.lastValidL;
|
||||
}
|
||||
|
||||
if (Number.isFinite(packet?.rmsR)) {
|
||||
const vR = packet.rmsR + offset;
|
||||
shared.values.R = vR;
|
||||
shared.lastValidR = vR;
|
||||
} else {
|
||||
shared.values.R = shared.lastValidR;
|
||||
}
|
||||
}
|
||||
|
||||
export function draw(g, rect, CONFIG = {}, shared) {
|
||||
const MODE = (CONFIG.RMS_MODE ?? 'dbfs').toLowerCase(); // Default jetzt 'dbfs'
|
||||
|
||||
// Referenz: -18 dBFS RMS ≙ +4 dBu (übliches Studio-Line-Up). Anpassbar.
|
||||
const REF_DBFS = Number.isFinite(CONFIG.RMS_REF_DBFS_FOR_REF_DBU) ? CONFIG.RMS_REF_DBFS_FOR_REF_DBU : -18;
|
||||
const REF_DBU = Number.isFinite(CONFIG.RMS_REF_DBU) ? CONFIG.RMS_REF_DBU : +4;
|
||||
|
||||
// Anzeige-Umschaltung & Skala
|
||||
const isDBU = (MODE === 'dbu');
|
||||
const LOG_MIN = -60;
|
||||
const LOG_TOP = isDBU ? +24 : 0;
|
||||
|
||||
const RED_START =
|
||||
Number.isFinite(CONFIG.RMS_RED_START)
|
||||
? CONFIG.RMS_RED_START
|
||||
: (isDBU ? +20 : 0);
|
||||
|
||||
// Umrechnung auf Anzeigeeinheit
|
||||
const toDisplay = (dbfsRms) => {
|
||||
if (!Number.isFinite(dbfsRms)) return -Infinity;
|
||||
return isDBU ? (dbfsRms - REF_DBFS + REF_DBU) : dbfsRms;
|
||||
};
|
||||
|
||||
// Skalen-Mapping: unterhalb −20 stärker komprimiert, darüber nahezu linear.
|
||||
function scaleNorm(db) {
|
||||
if (!Number.isFinite(db)) return 0;
|
||||
if (db <= LOG_MIN) return 0;
|
||||
|
||||
if (isDBU) {
|
||||
// dBu: −60…−20 (45%), −20…0 (+25%), 0…+20 (+25%), +20…+24 (+5%)
|
||||
if (db <= -20) { const t = (db + 60) / 40; return Math.pow(t, 1.6) * 0.45; } // → 0…0.45
|
||||
if (db <= 0) { const t = (db + 20) / 20; return 0.45 + t * 0.25; } // → 0.70
|
||||
if (db <= +20) { const t = (db ) / 20; return 0.70 + t * 0.25; } // → 0.95
|
||||
if (db <= +24) { const t = (db - 20) / 4; return 0.95 + t * 0.05; } // → 1.00
|
||||
return 1;
|
||||
} else {
|
||||
// dBFS: linear 0 … −60
|
||||
if (db >= LOG_TOP) return 1;
|
||||
return (db - LOG_MIN) / (LOG_TOP - LOG_MIN);
|
||||
}
|
||||
}
|
||||
const mapY = (db) => rect.y + (1 - Math.max(0, Math.min(1, scaleNorm(db)))) * rect.h;
|
||||
|
||||
// Layout
|
||||
const innerPad = 8, scaleW = 26, gap = 8;
|
||||
const avail = rect.w - innerPad * 2 - scaleW - gap * 2;
|
||||
let barW = Math.max(8, Math.floor(avail / 2));
|
||||
barW = Math.floor(barW * (CONFIG.METER_BAR_THIN || 0.55));
|
||||
|
||||
const used = 2 * barW + scaleW + 2 * gap;
|
||||
const extra = Math.max(0, (rect.w - innerPad * 2) - used);
|
||||
const leftX = rect.x + innerPad + extra / 2;
|
||||
const scaleX = leftX + barW + gap;
|
||||
const rightX = scaleX + scaleW + gap;
|
||||
const centerX = scaleX + scaleW / 2;
|
||||
const centerLeft = leftX + barW / 2;
|
||||
const centerRight = rightX + barW / 2;
|
||||
|
||||
// Header-Hintergrund säubern
|
||||
g.save();
|
||||
g.fillStyle = HEADER_BG;
|
||||
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 vL = clamp(rawDispL, LOG_MIN, LOG_TOP);
|
||||
const vR = clamp(rawDispR, LOG_MIN, LOG_TOP);
|
||||
g.save();
|
||||
const headerWarn = CONFIG.RMS_HEADER_SHOW_VALUE && (rawDispL > RED_START || rawDispR > RED_START);
|
||||
const prevFont = g.font;
|
||||
g.font = 'bold 12px ui-monospace, monospace';
|
||||
g.fillStyle = headerWarn ? WARN_COLOR : (CONFIG.HEADER_TEXT_COLOR || MID_COLOR);
|
||||
g.textAlign = 'center';
|
||||
if (CONFIG.RMS_HEADER_SHOW_VALUE) {
|
||||
const yText = rect.y - 12;
|
||||
const fmt = (v) => {
|
||||
const sign = v >= 0 ? '+' : '-';
|
||||
return `${sign}${Math.abs(v).toFixed(1)}`;
|
||||
};
|
||||
g.textAlign = 'center';
|
||||
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.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);
|
||||
} else {
|
||||
const title = isDBU ? 'RMS (dBu)' : 'RMS (dBFS RMS)';
|
||||
g.fillText(title, centerX, rect.y - 12);
|
||||
}
|
||||
g.font = prevFont;
|
||||
g.restore();
|
||||
|
||||
const yFloor = mapY(LOG_MIN);
|
||||
const redStart = clamp(RED_START, LOG_MIN, LOG_TOP);
|
||||
const yRed = mapY(redStart);
|
||||
const innerW = Math.max(2, barW - 2);
|
||||
|
||||
const colNorm = CONFIG.RMS_COLOR_NORMAL || OK_COLOR;
|
||||
const colWarn = CONFIG.RMS_COLOR_WARN || WARN_COLOR;
|
||||
|
||||
const drawBar = (x0, valDisp) => {
|
||||
const yVal = mapY(valDisp);
|
||||
|
||||
if (valDisp > RED_START) {
|
||||
if (CONFIG.RMS_RED_BAR_ONLY) {
|
||||
// normaler Teil (bis Warnschwelle)
|
||||
const yNormTop = Math.min(yRed, yFloor);
|
||||
g.fillStyle = colNorm; g.fillRect(x0 + 1, yNormTop, innerW, Math.max(0, yFloor - yNormTop));
|
||||
// warnender Teil
|
||||
const yWarnTop = Math.min(yVal, yRed);
|
||||
g.fillStyle = colWarn; g.fillRect(x0 + 1, yWarnTop, innerW, Math.max(0, yRed - yWarnTop));
|
||||
} 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'; g.fillRect(x0 + 1, yVal, innerW, 2); g.globalAlpha = 1;
|
||||
};
|
||||
|
||||
drawBar(leftX, vL);
|
||||
drawBar(rightX, vR);
|
||||
|
||||
const majors = isDBU ? [24, 20, 10, 0, -10, -20, -30, -40, -50, -60]
|
||||
: [0, -6, -12, -18, -24, -30, -40, -60];
|
||||
const minorValues = isDBU ? null : buildDbfsMinorTicks();
|
||||
const alignmentHighlight = (!isDBU) ? getRmsAlignmentHighlight(CONFIG) : null;
|
||||
drawRmsStaticOverlay(g, shared, rect, CONFIG, {
|
||||
leftX,
|
||||
rightX,
|
||||
barW,
|
||||
innerW,
|
||||
scaleX,
|
||||
scaleW,
|
||||
centerX,
|
||||
yRed,
|
||||
mapY,
|
||||
LOG_TOP,
|
||||
LOG_MIN,
|
||||
isDBU,
|
||||
majors,
|
||||
minorValues,
|
||||
alignmentHighlight,
|
||||
});
|
||||
}
|
||||
|
||||
function drawRmsStaticOverlay(g, shared, rect, CONFIG, geom) {
|
||||
const {
|
||||
leftX,
|
||||
rightX,
|
||||
barW,
|
||||
innerW,
|
||||
scaleX,
|
||||
scaleW,
|
||||
centerX,
|
||||
yRed,
|
||||
mapY,
|
||||
LOG_TOP,
|
||||
LOG_MIN,
|
||||
isDBU,
|
||||
majors,
|
||||
minorValues,
|
||||
alignmentHighlight,
|
||||
} = geom;
|
||||
const topPad = 10;
|
||||
const layerX = rect.x;
|
||||
const layerY = rect.y - topPad;
|
||||
const layerW = rect.w;
|
||||
const layerH = rect.h + 24 + topPad;
|
||||
const key = [
|
||||
'scale-font-header-v1-top-pad',
|
||||
Math.round(rect.w),
|
||||
Math.round(rect.h),
|
||||
topPad,
|
||||
CONFIG.METER_BAR_THIN || 0.55,
|
||||
CONFIG.RMS_MODE || 'dbfs',
|
||||
CONFIG.AL_MARKERS_ENABLED === false ? 0 : 1,
|
||||
CONFIG.RMS_RED_START ?? '',
|
||||
METER_HEADER_FONT,
|
||||
].join('|');
|
||||
drawCachedStaticLayer(g, shared, 'rms-static', key, layerX, layerY, layerW, layerH, (cg) => {
|
||||
cg.save();
|
||||
cg.translate(-layerX, -layerY);
|
||||
drawOverlayTicksLR(cg, leftX, rightX, innerW, mapY, LOG_TOP, LOG_MIN, majors, minorValues, alignmentHighlight);
|
||||
const yTop = mapY(LOG_TOP);
|
||||
drawRedStripeLR(cg, leftX, barW, rightX, centerX, mapY, yRed, yTop);
|
||||
const scaleRect = { x: scaleX, y: rect.y, w: scaleW, h: rect.h };
|
||||
drawScale(cg, scaleRect, centerX, mapY, {
|
||||
isDBU,
|
||||
majors,
|
||||
minors: minorValues,
|
||||
hairStep: isDBU ? null : 1,
|
||||
topValue: LOG_TOP,
|
||||
bottomValue: LOG_MIN,
|
||||
highlightTick: null,
|
||||
});
|
||||
const unitLabel = 'dBFS (RMS)';
|
||||
cg.fillStyle = LABEL_COLOR;
|
||||
cg.textAlign = 'center';
|
||||
const baseY = rect.y + rect.h + 16;
|
||||
cg.fillText('L', leftX + barW / 2, baseY);
|
||||
cg.fillText('R', rightX + barW / 2, baseY);
|
||||
const prevFontLabels = cg.font;
|
||||
cg.fillStyle = '#ffffff';
|
||||
cg.font = 'bold 8.4px ui-monospace, monospace';
|
||||
cg.fillText(unitLabel, centerX, baseY);
|
||||
cg.font = prevFontLabels;
|
||||
cg.restore();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
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);
|
||||
const insetL = Math.max(1, Math.floor(gapL * 0.35));
|
||||
const xL = Math.round(innerRight + insetL) + 0.5;
|
||||
|
||||
const innerLeft = rightX;
|
||||
const gapR = Math.abs(centerX - innerLeft);
|
||||
const insetR = Math.max(1, Math.floor(gapR * 0.35));
|
||||
const xR = Math.round(innerLeft - insetR) + 0.5;
|
||||
|
||||
const y1 = Math.min(yWarn, yTop), y2 = Math.max(yWarn, yTop);
|
||||
g.save(); g.strokeStyle = WARN_COLOR; g.lineWidth = 1;
|
||||
g.beginPath(); g.moveTo(xL, y1); g.lineTo(xL, y2); g.stroke();
|
||||
g.beginPath(); g.moveTo(xR, y1); g.lineTo(xR, y2); g.stroke();
|
||||
g.restore();
|
||||
}
|
||||
|
||||
function drawOverlayTicksLR(g, leftX, rightX, widthPx, mapY, TOP, BOTTOM, majors, minorValues = null, highlightTick = null) {
|
||||
g.save();
|
||||
|
||||
const MAJOR_INSET = 2;
|
||||
const MINOR_FRAC = 0.45;
|
||||
|
||||
const cxL = leftX + 1 + widthPx / 2;
|
||||
const cxR = rightX + 1 + widthPx / 2;
|
||||
|
||||
const approx = (a, b) => Math.abs(a - b) < 0.01;
|
||||
|
||||
const drawMajor = (value) => {
|
||||
const majorW = Math.max(1, widthPx - 4 * MAJOR_INSET);
|
||||
const yPix = Math.round(mapY(value)) + 0.5;
|
||||
const isHighlight = highlightTick && approx(value, highlightTick.value);
|
||||
const color = isHighlight ? highlightTick.color : 'rgb(0,0,255)';
|
||||
const lineWidth = isHighlight ? 1.8 : 1;
|
||||
|
||||
const x1L = Math.round(cxL - majorW / 2) + 0.5;
|
||||
const x2L = Math.round(cxL + majorW / 2) + 0.5;
|
||||
g.strokeStyle = color; g.lineWidth = lineWidth;
|
||||
g.beginPath(); g.moveTo(x1L, yPix); g.lineTo(x2L, yPix); g.stroke();
|
||||
|
||||
const x1R = Math.round(cxR - majorW / 2) + 0.5;
|
||||
const x2R = Math.round(cxR + majorW / 2) + 0.5;
|
||||
g.beginPath(); g.moveTo(x1R, yPix); g.lineTo(x2R, yPix); g.stroke();
|
||||
};
|
||||
|
||||
const drawMinor = (value) => {
|
||||
const minorW = Math.max(1, Math.floor(widthPx * MINOR_FRAC));
|
||||
const yPix = Math.round(mapY(value)) + 0.5;
|
||||
const isHighlight = highlightTick && approx(value, highlightTick.value);
|
||||
const color = isHighlight ? highlightTick.color : 'rgba(0,0,255,0.55)';
|
||||
const lineWidth = isHighlight ? 1.4 : 1;
|
||||
|
||||
const x1L = Math.round(cxL - minorW / 2) + 0.5;
|
||||
const x2L = Math.round(cxL + minorW / 2) + 0.5;
|
||||
g.strokeStyle = color; g.lineWidth = lineWidth;
|
||||
g.beginPath(); g.moveTo(x1L, yPix); g.lineTo(x2L, yPix); g.stroke();
|
||||
|
||||
const x1R = Math.round(cxR - minorW / 2) + 0.5;
|
||||
const x2R = Math.round(cxR + minorW / 2) + 0.5;
|
||||
g.beginPath(); g.moveTo(x1R, yPix); g.lineTo(x2R, yPix); g.stroke();
|
||||
};
|
||||
|
||||
const top = TOP, bot = BOTTOM;
|
||||
const hi = Math.max(top, bot), lo = Math.min(top, bot);
|
||||
const inRange = (v) => v <= hi && v >= lo;
|
||||
|
||||
if (Array.isArray(majors) && majors.length > 0) {
|
||||
const sorted = majors.slice().sort((a, b) => b - a); // desc
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const vMaj = sorted[i];
|
||||
if (inRange(vMaj)) {
|
||||
drawMajor(vMaj);
|
||||
}
|
||||
if (minorValues == null && i < sorted.length - 1) {
|
||||
const vNext = sorted[i + 1];
|
||||
const mid = (vMaj + vNext) / 2;
|
||||
if (inRange(mid)) {
|
||||
drawMinor(mid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(minorValues) && minorValues.length > 0) {
|
||||
const sortedMin = minorValues.slice().sort((a, b) => b - a);
|
||||
for (const v of sortedMin) {
|
||||
if (!inRange(v)) continue;
|
||||
drawMinor(v);
|
||||
}
|
||||
}
|
||||
g.restore();
|
||||
}
|
||||
|
||||
// Mittenskala & Raster
|
||||
function drawScale(g, colRect, centerX, mapY, {
|
||||
isDBU,
|
||||
majors = [],
|
||||
minors = [],
|
||||
hairStep = null,
|
||||
topValue = 0,
|
||||
bottomValue = -60,
|
||||
highlightTick = null,
|
||||
}) {
|
||||
if (isDBU) {
|
||||
g.save();
|
||||
const prevFont = g.font;
|
||||
g.font = METER_HEADER_FONT;
|
||||
g.fillStyle = LABEL_COLOR;
|
||||
g.textAlign = 'center';
|
||||
g.textBaseline = 'alphabetic';
|
||||
|
||||
const labels = [24, 20, 10, 4, 0, -10, -20, -30, -40, -50, -60];
|
||||
for (const v of labels) {
|
||||
const y = mapY(v);
|
||||
if (y < colRect.y || y > colRect.y + colRect.h) continue;
|
||||
const lab = v > 0 ? ('+' + v) : (v === 0 ? '0' : String(v));
|
||||
g.fillText(lab, centerX, y + 5);
|
||||
}
|
||||
g.font = prevFont;
|
||||
g.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
const hi = Math.max(topValue, bottomValue);
|
||||
const lo = Math.min(topValue, bottomValue);
|
||||
const inRange = (v) => v <= hi && v >= lo;
|
||||
const approx = (a, b) => Math.abs(a - b) < 0.01;
|
||||
const isHighlightValue = (value) => !!highlightTick && approx(value, highlightTick.value);
|
||||
|
||||
if (hairStep && hairStep > 0) {
|
||||
g.save();
|
||||
g.beginPath();
|
||||
g.rect(colRect.x, colRect.y, colRect.w, colRect.h);
|
||||
g.clip();
|
||||
for (let v = topValue; v >= bottomValue; v -= hairStep) {
|
||||
if (!inRange(v)) continue;
|
||||
const y = Math.round(mapY(v)) + 0.5;
|
||||
g.strokeStyle = isHighlightValue(v) ? (highlightTick?.color || '#ffffff') : 'rgba(255,255,255,0.08)';
|
||||
g.lineWidth = isHighlightValue(v) ? 1.2 : 0.6;
|
||||
g.beginPath();
|
||||
g.moveTo(colRect.x, y);
|
||||
g.lineTo(colRect.x + colRect.w, y);
|
||||
g.stroke();
|
||||
}
|
||||
g.restore();
|
||||
}
|
||||
|
||||
g.save();
|
||||
const prevFont = g.font;
|
||||
g.font = METER_HEADER_FONT;
|
||||
g.textAlign = 'center';
|
||||
g.textBaseline = 'alphabetic';
|
||||
for (const v of majors || []) {
|
||||
if (!inRange(v)) continue;
|
||||
const y = mapY(v);
|
||||
g.fillStyle = isHighlightValue(v) ? (highlightTick?.color || LABEL_COLOR) : LABEL_COLOR;
|
||||
g.fillText(String(v), centerX, y + 5);
|
||||
}
|
||||
g.font = prevFont;
|
||||
g.restore();
|
||||
}
|
||||
|
||||
function buildDbfsMinorTicks() {
|
||||
return [-3, -9, -15, -21, -27, -33, -36, -42, -48, -54];
|
||||
}
|
||||
|
||||
function getRmsAlignmentHighlight(CONFIG) {
|
||||
if (!CONFIG || CONFIG.AL_MARKERS_ENABLED === false) return null;
|
||||
const isArd = CONFIG.PPM_DIN_MODE === 'al_minus9';
|
||||
return {
|
||||
value: isArd ? -15 : -18,
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user