Initial Phoenix analyzer
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { createPeakHoldState, stepPeakHold } from '../core/utils.js';
|
||||
|
||||
// meters/lufs.js — vereinfachtes LUFS-Display (Integrated, Momentary, Short-term) + LRA & TP
|
||||
// Hinweis: Der Audio-Worklet liefert aktuell keine echten LUFS-Filterausgänge.
|
||||
// Wir nähern LUFS über dBFS (RMS) an und integrieren per EMA:
|
||||
// - Momentary ≈ 400 ms
|
||||
// - Short-term ≈ 3 s
|
||||
// - Integrated = langsame EMA (ohne Gate)
|
||||
// TruePeak nehmen wir aus packet.tpL/tpR (dBFS), LRA aus Short-term vs. Momentary.
|
||||
|
||||
import { drawCachedStaticLayer } from './static_layer.js';
|
||||
import { LABEL_COLOR, OK_COLOR } from '../core/theme.js';
|
||||
|
||||
export const id = 'lufs';
|
||||
|
||||
export function initShared(CONFIG) {
|
||||
const now = performance.now();
|
||||
return {
|
||||
integ: -23.0,
|
||||
momentary: -23.0,
|
||||
shortTerm: -23.0,
|
||||
lra: 0.0,
|
||||
truePeak: -60.0,
|
||||
tpInstant: -60.0,
|
||||
_tpHoldState: createPeakHoldState(-60, now, CONFIG?.TP_HOLD_MS ?? CONFIG?.VU_HOLD_MS ?? 1000),
|
||||
_lastTs: now,
|
||||
};
|
||||
}
|
||||
|
||||
export function update(packet, shared) {
|
||||
// Prefer true LUFS from packet (computed in audio.js via K-weighted analysers)
|
||||
if (Number.isFinite(packet.lufsM)) shared.momentary = packet.lufsM;
|
||||
if (Number.isFinite(packet.lufsS)) shared.shortTerm = packet.lufsS;
|
||||
if (Number.isFinite(packet.lufsI)) shared.integ = packet.lufsI;
|
||||
if (Number.isFinite(packet.lra)) shared.lra = packet.lra;
|
||||
|
||||
// True Peak hold from packet
|
||||
if (Number.isFinite(packet.tpL) || Number.isFinite(packet.tpR)) {
|
||||
const tpL = Number.isFinite(packet.tpL) ? packet.tpL : -60;
|
||||
const tpR = Number.isFinite(packet.tpR) ? packet.tpR : -60;
|
||||
shared.tpInstant = Math.max(tpL, tpR);
|
||||
}
|
||||
}
|
||||
|
||||
export function draw(g, rect, CONFIG, shared) {
|
||||
// Skala: LUFS (negativ). Wir verwenden Bereich [-50 .. -5]
|
||||
const LUFS_TOP = -5, LUFS_BOTTOM = -50;
|
||||
const mapY = (lufs) => {
|
||||
const v = Math.max(LUFS_BOTTOM, Math.min(LUFS_TOP, lufs));
|
||||
const t = (v - LUFS_BOTTOM) / (LUFS_TOP - LUFS_BOTTOM);
|
||||
return rect.y + (1 - t) * rect.h;
|
||||
};
|
||||
|
||||
// 3 Spalten (Momentary, Short-term, Integrated)
|
||||
const innerPad = 8, gap = 10;
|
||||
const colW = Math.floor((rect.w - innerPad * 2 - gap * 2) / 3);
|
||||
const xM = rect.x + innerPad;
|
||||
const xS = xM + colW + gap;
|
||||
const xI = xS + colW + gap;
|
||||
|
||||
// Bars zeichnen
|
||||
drawLufsBar(g, { x: xM, y: rect.y, w: colW, h: rect.h }, shared.momentary, 'M', CONFIG, mapY);
|
||||
drawLufsBar(g, { x: xS, y: rect.y, w: colW, h: rect.h }, shared.shortTerm, 'S', CONFIG, mapY);
|
||||
drawLufsBar(g, { x: xI, y: rect.y, w: colW, h: rect.h }, shared.integ, 'I', CONFIG, mapY);
|
||||
|
||||
// True Peak hold smoothing
|
||||
const now = performance.now();
|
||||
const holdMs = Number.isFinite(CONFIG.TP_HOLD_MS) ? CONFIG.TP_HOLD_MS : (CONFIG.VU_HOLD_MS ?? 1000);
|
||||
const decay = Number.isFinite(CONFIG.TP_DECAY_DB_PER_S) ? CONFIG.TP_DECAY_DB_PER_S : 20;
|
||||
if (!shared._tpHoldState) {
|
||||
shared._tpHoldState = createPeakHoldState(shared.truePeak ?? -60, now, holdMs);
|
||||
}
|
||||
const tpInstant = Number.isFinite(shared.tpInstant) ? shared.tpInstant : -60;
|
||||
const tpValue = stepPeakHold(tpInstant, shared._tpHoldState, now, {
|
||||
holdMs,
|
||||
decayDbPerS: decay,
|
||||
floor: -60,
|
||||
riseThreshold: 0.1,
|
||||
});
|
||||
shared.truePeak = tpValue;
|
||||
|
||||
// Zusatz-Infos unterhalb
|
||||
// g.save();
|
||||
// g.fillStyle = '#8fd3d4';
|
||||
// g.textAlign = 'left';
|
||||
// g.fillText(`LRA: ${shared.lra.toFixed(1)} LU`, rect.x, rect.y + rect.h + 20);
|
||||
// g.fillText(`TP: ${shared.truePeak.toFixed(1)} dBFS`, rect.x, rect.y + rect.h + 38);
|
||||
// g.restore();
|
||||
|
||||
drawLufsStaticOverlay(g, shared, rect, CONFIG, { xM, xS, xI, colW }, mapY);
|
||||
}
|
||||
|
||||
function drawLufsStaticOverlay(g, shared, rect, CONFIG, geom, mapY) {
|
||||
const { xM, xS, xI, colW } = geom;
|
||||
const topPad = 10;
|
||||
const layerX = rect.x;
|
||||
const layerY = rect.y - topPad;
|
||||
const layerW = rect.w;
|
||||
const layerH = rect.h + topPad;
|
||||
const scaleCol = CONFIG?.LUFS_SCALE_LABEL_COLOR || LABEL_COLOR;
|
||||
const key = [
|
||||
'scale-font-header-v1-top-pad',
|
||||
Math.round(rect.w),
|
||||
Math.round(rect.h),
|
||||
topPad,
|
||||
scaleCol,
|
||||
].join('|');
|
||||
drawCachedStaticLayer(g, shared, 'lufs-static', key, layerX, layerY, layerW, layerH, (cg) => {
|
||||
cg.save();
|
||||
cg.translate(-layerX, -layerY);
|
||||
const drawRefs = (xBase) => {
|
||||
const col = { x: xBase, y: rect.y, w: colW, h: rect.h };
|
||||
for (const v of [-23, -18, -10, -5]) {
|
||||
const y = mapY(v);
|
||||
const isMajor = (v === -23 || v === -18 || v === -10);
|
||||
cg.save();
|
||||
cg.strokeStyle = scaleCol;
|
||||
cg.globalAlpha = isMajor ? 1 : 0.28;
|
||||
cg.setLineDash(isMajor ? [] : [2,2]);
|
||||
cg.beginPath(); cg.moveTo(col.x, y); cg.lineTo(col.x + col.w, y); cg.stroke();
|
||||
if (isMajor) {
|
||||
cg.globalAlpha = 1;
|
||||
cg.fillStyle = scaleCol; cg.textAlign = 'center';
|
||||
cg.fillText(`${v}`, col.x + col.w / 2, y - 4);
|
||||
}
|
||||
cg.restore();
|
||||
}
|
||||
};
|
||||
drawRefs(xM); drawRefs(xS); drawRefs(xI);
|
||||
cg.restore();
|
||||
});
|
||||
}
|
||||
|
||||
function drawLufsBar(g, rect, value, label, CONFIG, mapY) {
|
||||
const yVal = mapY(value);
|
||||
const yFloor = mapY(-50);
|
||||
const color = getLufsBarColor(label, CONFIG);
|
||||
|
||||
g.save();
|
||||
g.fillStyle = color;
|
||||
g.fillRect(rect.x + 1, yVal, Math.max(2, rect.w - 2), Math.max(0, yFloor - yVal));
|
||||
|
||||
g.fillStyle = '#ffffff';
|
||||
g.textAlign = 'center';
|
||||
g.fillText(label, rect.x + rect.w / 2, rect.y - 10);
|
||||
g.fillText(value.toFixed(1), rect.x + rect.w / 2, rect.y + rect.h + 18);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
function getLufsBarColor(label, CONFIG = {}) {
|
||||
const fallback = CONFIG.LUFS_COLOR_GREEN || OK_COLOR;
|
||||
if (label === 'I') return CONFIG.LUFS_COLOR_I || CONFIG.LUFS_COLOR_I_GREEN || fallback;
|
||||
if (label === 'M') return CONFIG.LUFS_COLOR_M || CONFIG.LUFS_COLOR_M_GREEN || fallback;
|
||||
if (label === 'S') return CONFIG.LUFS_COLOR_S || CONFIG.LUFS_COLOR_S_GREEN || fallback;
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user