Recover analyzer DSP and realtime pipeline corrections
This commit is contained in:
+277
-32
@@ -2,14 +2,26 @@
|
||||
// Wird von main.js mit `initAudio(env)` gestartet. Nutzt env.config (CONFIG) und env.meters.
|
||||
|
||||
import { applyPhoenixGlobalConfig, buildPhoenixGlobalConfigPayload, saveConfig } from './config.js';
|
||||
import { decodePhoenixSpectroBuffer, decodePhoenixVisualsBuffer } from './binary_protocol.js';
|
||||
import { getRtwCenters } from './rtw_centers.js';
|
||||
|
||||
// interner Zustand (pro App-Instanz)
|
||||
let phoenixSocket = null;
|
||||
let phoenixSpectroSocket = null;
|
||||
let phoenixSpectroRetryTimer = null;
|
||||
let pendingSpectroBuffer = null;
|
||||
let phoenixSpectroRaf = 0;
|
||||
let phoenixVisualsSocket = null;
|
||||
let phoenixVisualsRetryTimer = null;
|
||||
let pendingVisualsBuffer = null;
|
||||
let phoenixVisualsRaf = 0;
|
||||
let envRef = null;
|
||||
let lifecycleHandlersBound = false;
|
||||
let recoverTimer = null;
|
||||
let lastHardRecoverAt = 0;
|
||||
let pendingPhoenixFrame = null;
|
||||
let phoenixPacketRaf = 0;
|
||||
let phoenixPacketBusy = false;
|
||||
|
||||
const RMS_RING = { L: new Float32Array(512), R: new Float32Array(512), i: 0, n: 0 };
|
||||
const WAVEFORM_RING_SECONDS = 20;
|
||||
@@ -50,30 +62,65 @@ function normalizePhoenixBaseUrl(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPhoenixWsUrl(baseUrl) {
|
||||
function buildPhoenixWsUrl(baseUrl, pathname = '/api/v1/metrics/ws') {
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
url.pathname = '/api/v1/metrics/ws';
|
||||
url.pathname = pathname;
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url.toString();
|
||||
} catch (_) {
|
||||
const fallback = defaultPhoenixBaseUrl();
|
||||
return fallback.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:') + '/api/v1/metrics/ws';
|
||||
return fallback.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:') + pathname;
|
||||
}
|
||||
}
|
||||
|
||||
function closePhoenixSocket() {
|
||||
if (!phoenixSocket) return;
|
||||
try {
|
||||
phoenixSocket.onopen = null;
|
||||
phoenixSocket.onmessage = null;
|
||||
phoenixSocket.onerror = null;
|
||||
phoenixSocket.onclose = null;
|
||||
phoenixSocket.close();
|
||||
} catch (_) {}
|
||||
if (phoenixSocket) {
|
||||
try {
|
||||
phoenixSocket.onopen = null;
|
||||
phoenixSocket.onmessage = null;
|
||||
phoenixSocket.onerror = null;
|
||||
phoenixSocket.onclose = null;
|
||||
phoenixSocket.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
phoenixSocket = null;
|
||||
if (phoenixSpectroRetryTimer) clearTimeout(phoenixSpectroRetryTimer);
|
||||
phoenixSpectroRetryTimer = null;
|
||||
if (phoenixSpectroSocket) {
|
||||
try {
|
||||
phoenixSpectroSocket.onopen = null;
|
||||
phoenixSpectroSocket.onmessage = null;
|
||||
phoenixSpectroSocket.onerror = null;
|
||||
phoenixSpectroSocket.onclose = null;
|
||||
phoenixSpectroSocket.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
phoenixSpectroSocket = null;
|
||||
pendingSpectroBuffer = null;
|
||||
if (phoenixSpectroRaf) cancelAnimationFrame(phoenixSpectroRaf);
|
||||
phoenixSpectroRaf = 0;
|
||||
if (phoenixVisualsRetryTimer) clearTimeout(phoenixVisualsRetryTimer);
|
||||
phoenixVisualsRetryTimer = null;
|
||||
if (phoenixVisualsSocket) {
|
||||
try {
|
||||
phoenixVisualsSocket.onopen = null;
|
||||
phoenixVisualsSocket.onmessage = null;
|
||||
phoenixVisualsSocket.onerror = null;
|
||||
phoenixVisualsSocket.onclose = null;
|
||||
phoenixVisualsSocket.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
phoenixVisualsSocket = null;
|
||||
pendingVisualsBuffer = null;
|
||||
if (phoenixVisualsRaf) cancelAnimationFrame(phoenixVisualsRaf);
|
||||
phoenixVisualsRaf = 0;
|
||||
pendingPhoenixFrame = null;
|
||||
if (phoenixPacketRaf) cancelAnimationFrame(phoenixPacketRaf);
|
||||
phoenixPacketRaf = 0;
|
||||
phoenixPacketBusy = false;
|
||||
}
|
||||
|
||||
async function requestPhoenixRtaConfig(baseUrl, config) {
|
||||
@@ -164,14 +211,20 @@ function applyRmsActivity(env, rmsL, rmsR, sampleTs) {
|
||||
if (hit) env.audio.lastSignalTs = sampleTs;
|
||||
}
|
||||
|
||||
function copyPhoenixSpectroBins(audioState, spectro) {
|
||||
if (!audioState) return;
|
||||
const src = Array.isArray(spectro?.bins) ? spectro.bins : null;
|
||||
function copyPhoenixSpectroBins(audioState, spectro, frameDelta = 1) {
|
||||
if (!audioState) return false;
|
||||
const src = Array.isArray(spectro?.bins) || ArrayBuffer.isView(spectro?.bins)
|
||||
? spectro.bins
|
||||
: null;
|
||||
if (!src || !src.length) {
|
||||
audioState.phoenixSpectroBuffer = null;
|
||||
audioState.phoenixSpectroMeta = null;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const sourceSeq = Number(spectro?.seq);
|
||||
const previousSourceSeq = Number(audioState.phoenixSpectroSourceSeq || 0);
|
||||
if (Number.isFinite(sourceSeq) && sourceSeq > 0
|
||||
&& previousSourceSeq > 0 && sourceSeq <= previousSourceSeq) return false;
|
||||
let target = audioState.phoenixSpectroBuffer;
|
||||
if (!(target instanceof Float32Array) || target.length !== src.length) {
|
||||
target = new Float32Array(src.length);
|
||||
@@ -190,7 +243,118 @@ function copyPhoenixSpectroBins(audioState, spectro) {
|
||||
fftSize,
|
||||
frequencyBinCount: target.length,
|
||||
};
|
||||
audioState.phoenixSpectroSeq = (audioState.phoenixSpectroSeq || 0) + 1;
|
||||
let delta = Number.isFinite(Number(frameDelta)) ? Math.max(1, Math.floor(Number(frameDelta))) : 1;
|
||||
if (Number.isFinite(sourceSeq) && sourceSeq > 0) {
|
||||
if (previousSourceSeq > 0) delta = Math.max(1, Math.floor(sourceSeq - previousSourceSeq));
|
||||
audioState.phoenixSpectroSourceSeq = sourceSeq;
|
||||
}
|
||||
audioState.phoenixSpectroSeq = (audioState.phoenixSpectroSeq || 0) + delta;
|
||||
return true;
|
||||
}
|
||||
|
||||
function openPhoenixSpectroSocket(baseUrl, env) {
|
||||
if (phoenixSpectroSocket && (
|
||||
phoenixSpectroSocket.readyState === WebSocket.OPEN
|
||||
|| phoenixSpectroSocket.readyState === WebSocket.CONNECTING
|
||||
)) return;
|
||||
const socket = new WebSocket(buildPhoenixWsUrl(baseUrl, '/api/v1/spectro/ws'));
|
||||
socket.binaryType = 'arraybuffer';
|
||||
phoenixSpectroSocket = socket;
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
if (!(event.data instanceof ArrayBuffer)) return;
|
||||
pendingSpectroBuffer = event.data;
|
||||
scheduleSpectroBufferPump(env);
|
||||
};
|
||||
|
||||
const retry = () => {
|
||||
if (phoenixSpectroSocket === socket) phoenixSpectroSocket = null;
|
||||
if (!phoenixSocket || phoenixSocket.readyState !== WebSocket.OPEN) return;
|
||||
if (phoenixSpectroRetryTimer) clearTimeout(phoenixSpectroRetryTimer);
|
||||
phoenixSpectroRetryTimer = setTimeout(() => {
|
||||
phoenixSpectroRetryTimer = null;
|
||||
openPhoenixSpectroSocket(baseUrl, env);
|
||||
}, 1000);
|
||||
};
|
||||
socket.onerror = retry;
|
||||
socket.onclose = retry;
|
||||
}
|
||||
|
||||
function scheduleSpectroBufferPump(env) {
|
||||
if (phoenixSpectroRaf || !pendingSpectroBuffer) return;
|
||||
phoenixSpectroRaf = requestAnimationFrame(() => {
|
||||
phoenixSpectroRaf = 0;
|
||||
const buffer = pendingSpectroBuffer;
|
||||
pendingSpectroBuffer = null;
|
||||
try {
|
||||
const decoded = decodePhoenixSpectroBuffer(buffer);
|
||||
if (!decoded) return;
|
||||
if (!copyPhoenixSpectroBins(env.audio, decoded)) return;
|
||||
env.requestRender?.('spectro');
|
||||
} catch (err) {
|
||||
console.warn('Phoenix spectrogram packet error:', err);
|
||||
} finally {
|
||||
if (pendingSpectroBuffer) scheduleSpectroBufferPump(env);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openPhoenixVisualsSocket(baseUrl, env) {
|
||||
if (phoenixVisualsSocket && (
|
||||
phoenixVisualsSocket.readyState === WebSocket.OPEN
|
||||
|| phoenixVisualsSocket.readyState === WebSocket.CONNECTING
|
||||
)) return;
|
||||
const socket = new WebSocket(buildPhoenixWsUrl(baseUrl, '/api/v1/visuals/ws'));
|
||||
socket.binaryType = 'arraybuffer';
|
||||
phoenixVisualsSocket = socket;
|
||||
socket.onmessage = (event) => {
|
||||
if (!(event.data instanceof ArrayBuffer)) return;
|
||||
pendingVisualsBuffer = event.data;
|
||||
scheduleVisualsBufferPump(env);
|
||||
};
|
||||
const retry = () => {
|
||||
if (phoenixVisualsSocket === socket) phoenixVisualsSocket = null;
|
||||
if (!phoenixSocket || phoenixSocket.readyState !== WebSocket.OPEN) return;
|
||||
if (phoenixVisualsRetryTimer) clearTimeout(phoenixVisualsRetryTimer);
|
||||
phoenixVisualsRetryTimer = setTimeout(() => {
|
||||
phoenixVisualsRetryTimer = null;
|
||||
openPhoenixVisualsSocket(baseUrl, env);
|
||||
}, 1000);
|
||||
};
|
||||
socket.onerror = retry;
|
||||
socket.onclose = retry;
|
||||
}
|
||||
|
||||
function scheduleVisualsBufferPump(env) {
|
||||
if (phoenixVisualsRaf || !pendingVisualsBuffer) return;
|
||||
phoenixVisualsRaf = requestAnimationFrame(() => {
|
||||
phoenixVisualsRaf = 0;
|
||||
const buffer = pendingVisualsBuffer;
|
||||
pendingVisualsBuffer = null;
|
||||
try {
|
||||
applyVisualsBuffer(env, buffer);
|
||||
} catch (err) {
|
||||
console.warn('Phoenix visual packet error:', err);
|
||||
} finally {
|
||||
if (pendingVisualsBuffer) scheduleVisualsBufferPump(env);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyVisualsBuffer(env, buffer) {
|
||||
const decoded = decodePhoenixVisualsBuffer(buffer);
|
||||
if (!decoded) return;
|
||||
const { seq, left, right, wave } = decoded;
|
||||
const previousSeq = Number(env.audio?.phoenixVisualsSeq || 0);
|
||||
if (previousSeq > 0 && seq <= previousSeq) return;
|
||||
if (left && right) {
|
||||
env.audio.xyL = left;
|
||||
env.audio.xyR = right;
|
||||
env.audio.xySeq = seq;
|
||||
}
|
||||
if (wave) updateWaveformEnvelopeStore(env.audio, wave);
|
||||
env.audio.phoenixVisualsSeq = seq;
|
||||
env.requestRender?.('visuals');
|
||||
}
|
||||
|
||||
async function updateActiveMeters(env, packet, CONFIG) {
|
||||
@@ -230,21 +394,33 @@ async function syncPhoenixGlobalConfig(env, revision) {
|
||||
async function applyIncomingAudioPacket(env, packet, CONFIG, sampleTs = performance.now()) {
|
||||
const d = packet || {};
|
||||
if (!env?.audio) return;
|
||||
const seq = Number(d.seq);
|
||||
const previousSeq = Number(env.audio.phoenixMetricsSeq || 0);
|
||||
if (Number.isFinite(seq) && seq > 0 && previousSeq > 0 && seq <= previousSeq) return;
|
||||
if (Number.isFinite(seq) && seq > 0) env.audio.phoenixMetricsSeq = seq;
|
||||
env.requestRender?.('audio');
|
||||
env.audio.lastSampleTs = sampleTs;
|
||||
env.audio.alive = true;
|
||||
if (Number.isFinite(Number(d.sampleRate)) && Number(d.sampleRate) > 0) {
|
||||
env.audio.sampleRate = Number(d.sampleRate);
|
||||
env.audio.nyq = Number(d.sampleRate) / 2;
|
||||
}
|
||||
if (d.xyL && d.xyR) {
|
||||
env.audio.xyL = d.xyL;
|
||||
env.audio.xyR = d.xyR;
|
||||
env.audio.xySeq = Number.isFinite(d.seq) ? d.seq : (env.audio.xySeq || 0);
|
||||
}
|
||||
if (typeof d.correlation === 'number') env.audio.correlation = d.correlation;
|
||||
if (typeof d.correlationNegativePeak === 'number') {
|
||||
env.audio.correlationNegativePeak = d.correlationNegativePeak;
|
||||
}
|
||||
if (d.waveL && env.audio.pushWaveSamples) {
|
||||
const channelCount = d.waveChannels || (d.waveR ? 2 : 1);
|
||||
env.audio.sampleRate = d.sampleRate || env.audio.sampleRate || 48000;
|
||||
env.audio.pushWaveSamples(d.waveL, d.waveR || null, channelCount, d.sampleRate);
|
||||
}
|
||||
if (d.rta) env.audio.rtaData = d.rta;
|
||||
if (d.spectro) copyPhoenixSpectroBins(env.audio, d.spectro);
|
||||
if (d.spectro) copyPhoenixSpectroBins(env.audio, d.spectro, d.spectroFrameDelta);
|
||||
if (typeof d.ppmDinL === 'number') env.audio.ppmDinL = d.ppmDinL;
|
||||
if (typeof d.ppmDinR === 'number') env.audio.ppmDinR = d.ppmDinR;
|
||||
if (typeof d.ppmEbuL === 'number') env.audio.ppmEbuL = d.ppmEbuL;
|
||||
@@ -294,6 +470,8 @@ function buildPhoenixMeterPacket(frame) {
|
||||
const lufsSR = Number(frame?.lufs_sr);
|
||||
const ppmBoxL = Number(frame?.ppm_box_l);
|
||||
const ppmBoxR = Number(frame?.ppm_box_r);
|
||||
const correlation = Number(frame?.correlation);
|
||||
const correlationNegativePeak = Number(frame?.correlation_negative_peak);
|
||||
const xyL = Array.isArray(frame?.xy_l) ? frame.xy_l : null;
|
||||
const xyR = Array.isArray(frame?.xy_r) ? frame.xy_r : null;
|
||||
const waveL = Array.isArray(frame?.wave_l) && frame.wave_l.length ? frame.wave_l : null;
|
||||
@@ -302,7 +480,11 @@ function buildPhoenixMeterPacket(frame) {
|
||||
const spectro = frame?.spectro && typeof frame.spectro === 'object' ? frame.spectro : null;
|
||||
const waveEnv = frame?.wave_env && typeof frame.wave_env === 'object' ? frame.wave_env : null;
|
||||
return {
|
||||
sampleRate: 48000,
|
||||
sampleRate: Number.isFinite(Number(frame?.sample_rate)) ? Number(frame.sample_rate) : 48000,
|
||||
correlation: Number.isFinite(correlation) ? Math.max(-1, Math.min(1, correlation)) : 0,
|
||||
correlationNegativePeak: Number.isFinite(correlationNegativePeak)
|
||||
? Math.max(-1, Math.min(1, correlationNegativePeak))
|
||||
: 0,
|
||||
rmsL: Number.isFinite(rmsL) ? rmsL : -120,
|
||||
rmsR: Number.isFinite(rmsR) ? rmsR : -120,
|
||||
tpL: Number.isFinite(tpL) ? tpL : -120,
|
||||
@@ -344,10 +526,14 @@ function buildPhoenixMeterPacket(frame) {
|
||||
sampleRate: Number.isFinite(Number(rta?.sample_rate)) ? Number(rta.sample_rate) : 48000,
|
||||
} : null,
|
||||
spectro: spectro ? {
|
||||
seq: Number.isFinite(Number(spectro?.seq)) ? Number(spectro.seq) : 0,
|
||||
bins: Array.isArray(spectro?.bins) ? spectro.bins : [],
|
||||
sampleRate: Number.isFinite(Number(spectro?.sample_rate)) ? Number(spectro.sample_rate) : 48000,
|
||||
fftSize: Number.isFinite(Number(spectro?.fft_size)) ? Number(spectro.fft_size) : 4096,
|
||||
} : null,
|
||||
spectroFrameDelta: Number.isFinite(Number(frame?.__spectro_frame_delta))
|
||||
? Math.max(1, Math.floor(Number(frame.__spectro_frame_delta)))
|
||||
: 1,
|
||||
waveEnv: waveEnv ? {
|
||||
data: Array.isArray(waveEnv?.data) ? waveEnv.data : [],
|
||||
columns: Number.isFinite(Number(waveEnv?.columns)) ? Number(waveEnv.columns) : 0,
|
||||
@@ -363,6 +549,57 @@ function buildPhoenixMeterPacket(frame) {
|
||||
};
|
||||
}
|
||||
|
||||
function mergePendingPhoenixFrame(previous, next) {
|
||||
if (!next) return previous;
|
||||
const previousSeq = Number(previous?.seq || 0);
|
||||
const nextSeq = Number(next?.seq || 0);
|
||||
if (previous && previousSeq > 0 && nextSeq > 0 && nextSeq <= previousSeq) return previous;
|
||||
if (!previous) {
|
||||
if (next?.spectro) next.__spectro_frame_delta = 1;
|
||||
return next;
|
||||
}
|
||||
const merged = next;
|
||||
if (!merged.wave_env && previous.wave_env) merged.wave_env = previous.wave_env;
|
||||
if ((!merged.xy_l || !merged.xy_l.length) && previous.xy_l?.length) {
|
||||
merged.xy_l = previous.xy_l;
|
||||
merged.xy_r = previous.xy_r;
|
||||
}
|
||||
if (!merged.rta && previous.rta) merged.rta = previous.rta;
|
||||
const previousSpectroCount = Math.max(0, Number(previous.__spectro_frame_delta) || 0);
|
||||
if (merged.spectro) {
|
||||
merged.__spectro_frame_delta = previousSpectroCount + 1;
|
||||
} else if (previous.spectro) {
|
||||
merged.spectro = previous.spectro;
|
||||
merged.__spectro_frame_delta = Math.max(1, previousSpectroCount);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function schedulePhoenixPacketPump(env, CONFIG) {
|
||||
if (phoenixPacketRaf || phoenixPacketBusy || !pendingPhoenixFrame) return;
|
||||
phoenixPacketRaf = requestAnimationFrame(async () => {
|
||||
phoenixPacketRaf = 0;
|
||||
const frame = pendingPhoenixFrame;
|
||||
pendingPhoenixFrame = null;
|
||||
if (!frame) return;
|
||||
phoenixPacketBusy = true;
|
||||
try {
|
||||
const packet = buildPhoenixMeterPacket(frame);
|
||||
await applyIncomingAudioPacket(env, packet, CONFIG, performance.now());
|
||||
} catch (err) {
|
||||
console.warn('Phoenix packet error:', err);
|
||||
} finally {
|
||||
phoenixPacketBusy = false;
|
||||
if (pendingPhoenixFrame) schedulePhoenixPacketPump(env, CONFIG);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enqueuePhoenixFrame(env, CONFIG, frame) {
|
||||
pendingPhoenixFrame = mergePendingPhoenixFrame(pendingPhoenixFrame, frame);
|
||||
schedulePhoenixPacketPump(env, CONFIG);
|
||||
}
|
||||
|
||||
function bindLifecycleHandlers() {
|
||||
if (lifecycleHandlersBound) return;
|
||||
|
||||
@@ -625,33 +862,39 @@ function updateWaveformEnvelopeStore(audioState, payload) {
|
||||
appendWaveformEnvelope(audioState.waveEnvStore, payload);
|
||||
}
|
||||
|
||||
function buildRtaRuntimeConfig(CONFIG = {}) {
|
||||
const bpoMode = CONFIG.RTA_BPO_MODE || '1_6';
|
||||
export function buildRtaRuntimeConfig(CONFIG = {}) {
|
||||
const bpoMode = CONFIG.RTA_BPO_MODE || '1_3';
|
||||
const layout = CONFIG.RTA_BAR_LAYOUT === 'rtw' ? 'rtw' : 'iec';
|
||||
const runtimeBpoMode = layout === 'rtw' ? '1_12' : bpoMode;
|
||||
const rtwProfile = layout === 'rtw';
|
||||
const runtimeBpoMode = rtwProfile ? '1_3' : bpoMode;
|
||||
return {
|
||||
engine: CONFIG.RTA_ENGINE || 'fft',
|
||||
engine: rtwProfile ? 'iir' : (CONFIG.RTA_ENGINE || 'iir'),
|
||||
fftSize: CONFIG.FFT_SIZE || 4096,
|
||||
monoInput: !!CONFIG.MONO_INPUT,
|
||||
lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED,
|
||||
lrFractionalDelaySamples: Number.isFinite(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES) ? CONFIG.LR_FRACTIONAL_DELAY_SAMPLES : 0,
|
||||
bpo: runtimeBpoMode,
|
||||
freqRange: CONFIG.RTA_FREQ_RANGE || 'norm',
|
||||
freqRange: rtwProfile ? 'norm' : (CONFIG.RTA_FREQ_RANGE || 'norm'),
|
||||
weighting: CONFIG.RTA_WEIGHTING || 'z',
|
||||
order: CONFIG.RTA_IIR_ORDER || 4,
|
||||
order: rtwProfile ? 6 : (CONFIG.RTA_IIR_ORDER || 6),
|
||||
tauFast: CONFIG.RTA_IIR_TAU_FAST || 0.12,
|
||||
tauSlow: CONFIG.RTA_IIR_TAU_SLOW || 1.0,
|
||||
integration: CONFIG.RTA_INTEGRATION || 'fast',
|
||||
layout,
|
||||
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: Number.isFinite(CONFIG.PPM_DIN_ATTACK_MS) ? CONFIG.PPM_DIN_ATTACK_MS : 5,
|
||||
ppmDinDecayDbPerS: Number.isFinite(CONFIG.PPM_DIN_DECAY_DB_PER_S) ? CONFIG.PPM_DIN_DECAY_DB_PER_S : (20 / 1.7),
|
||||
ppmDinAttackMs: 10,
|
||||
ppmDinDecayDbPerS: 20 / 1.5,
|
||||
ppmDinFastAttack: !!CONFIG.PPM_DIN_FAST_ATTACK,
|
||||
ppmEbuAttackMs: Number.isFinite(CONFIG.PPM_EBU_ATTACK_MS) ? CONFIG.PPM_EBU_ATTACK_MS : 10,
|
||||
ppmEbuDecayDbPerS: Number.isFinite(CONFIG.PPM_EBU_DECAY_DB_PER_S) ? CONFIG.PPM_EBU_DECAY_DB_PER_S : (24 / 2.8),
|
||||
ppmEbuAttackMs: 10,
|
||||
ppmEbuDecayDbPerS: 24 / 2.8,
|
||||
lufsIWindowMin: Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN) ? CONFIG.LUFS_I_WINDOW_MIN : 4,
|
||||
lufsINormEnabled: !!CONFIG.LUFS_I_NORM_ENABLED,
|
||||
correlationResponseS: Number(CONFIG.CORR_RESPONSE_S) >= 1.75 ? 2.5 : 1.0,
|
||||
correlationResetToken: Math.max(0, Math.floor(Number(CONFIG.CORR_RESET_TOKEN) || 0)),
|
||||
xyPoints: [128, 256, 512, 1024, 2048].includes(Number(CONFIG.XY_POINTS))
|
||||
? Number(CONFIG.XY_POINTS)
|
||||
: 1024,
|
||||
rtwCenters: layout === 'rtw' ? getRtwCenters(runtimeBpoMode) : null,
|
||||
};
|
||||
}
|
||||
@@ -700,6 +943,9 @@ async function initPhoenixAudio(env) {
|
||||
env.audio.phoenixSpectroScratch = null;
|
||||
env.audio.phoenixSpectroMeta = null;
|
||||
env.audio.phoenixSpectroSeq = 0;
|
||||
env.audio.phoenixSpectroSourceSeq = 0;
|
||||
env.audio.phoenixVisualsSeq = 0;
|
||||
env.audio.phoenixMetricsSeq = 0;
|
||||
env.audio.rmsDb = { L: -120, R: -120, mono: -120 };
|
||||
|
||||
const phoenixAnalyser = {
|
||||
@@ -852,16 +1098,15 @@ async function initPhoenixAudio(env) {
|
||||
};
|
||||
|
||||
socket.onopen = () => {
|
||||
openPhoenixSpectroSocket(baseUrl, env);
|
||||
openPhoenixVisualsSocket(baseUrl, env);
|
||||
finish(true);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const frame = JSON.parse(event.data);
|
||||
const packet = buildPhoenixMeterPacket(frame);
|
||||
applyIncomingAudioPacket(env, packet, CONFIG, performance.now()).catch((err) => {
|
||||
console.warn('Phoenix packet error:', err);
|
||||
});
|
||||
enqueuePhoenixFrame(env, CONFIG, frame);
|
||||
} catch (err) {
|
||||
console.warn('Phoenix metrics parse error:', err);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Phoenix binary WebSocket packet decoders. Keep this module dependency-free so
|
||||
// protocol compatibility can be tested without a browser.
|
||||
|
||||
export function decodePhoenixSpectroBuffer(buffer) {
|
||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 20) return null;
|
||||
const view = new DataView(buffer);
|
||||
if (view.getUint32(0, true) !== 0x50585350) return null;
|
||||
const seq = view.getUint32(4, true);
|
||||
const sampleRate = view.getUint32(8, true);
|
||||
const fftSize = view.getUint32(12, true);
|
||||
const count = view.getUint32(16, true);
|
||||
if (count < 1 || buffer.byteLength !== 20 + count * 4) return null;
|
||||
return {
|
||||
seq,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
bins: new Float32Array(buffer, 20, count),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodePhoenixVisualsBuffer(buffer) {
|
||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 32) return null;
|
||||
const view = new DataView(buffer);
|
||||
if (view.getUint32(0, true) !== 0x50585653 || view.getUint16(4, true) !== 1) return null;
|
||||
const flags = view.getUint16(6, true);
|
||||
const seq = view.getUint32(8, true);
|
||||
const xyCount = view.getUint32(12, true);
|
||||
const waveColumns = view.getUint32(16, true);
|
||||
const waveChannels = view.getUint16(20, true);
|
||||
const waveColumnSamples = view.getUint32(24, true);
|
||||
const waveSampleRate = view.getUint32(28, true);
|
||||
if (waveChannels > 2) return null;
|
||||
const xyBytes = xyCount * 8;
|
||||
const waveValues = waveColumns * waveChannels * 2;
|
||||
const expectedBytes = 32 + xyBytes + waveValues * 4;
|
||||
if (!Number.isSafeInteger(expectedBytes) || buffer.byteLength !== expectedBytes) return null;
|
||||
|
||||
let offset = 32;
|
||||
let left = null;
|
||||
let right = null;
|
||||
if ((flags & 1) && xyCount > 0) {
|
||||
left = new Float32Array(xyCount);
|
||||
right = new Float32Array(xyCount);
|
||||
for (let index = 0; index < xyCount; index++) {
|
||||
left[index] = view.getFloat32(offset, true);
|
||||
right[index] = view.getFloat32(offset + 4, true);
|
||||
offset += 8;
|
||||
}
|
||||
} else {
|
||||
offset += xyBytes;
|
||||
}
|
||||
|
||||
let wave = null;
|
||||
if ((flags & 2) && waveValues > 0 && (waveChannels === 1 || waveChannels === 2)) {
|
||||
const data = new Float32Array(waveValues);
|
||||
for (let index = 0; index < waveValues; index++) {
|
||||
data[index] = view.getFloat32(offset + index * 4, true);
|
||||
}
|
||||
wave = {
|
||||
data,
|
||||
columns: waveColumns,
|
||||
channels: waveChannels,
|
||||
columnSamples: waveColumnSamples,
|
||||
sampleRate: waveSampleRate,
|
||||
};
|
||||
}
|
||||
return { seq, left, right, wave };
|
||||
}
|
||||
+60
-25
@@ -51,20 +51,20 @@ const CONFIG = {
|
||||
REALTIME_BAR_DECAY_DB_PER_S: 20,
|
||||
RTA_ENGINE: 'iir', // 'fft' | 'iir'
|
||||
RTA_FREQ_RANGE: 'norm', // 'norm' | 'lf'
|
||||
RTA_BPO_MODE: '1_6',
|
||||
RTA_BPO_MODE: '1_3',
|
||||
RTA_WEIGHTING: 'z', // 'z' | 'a' | 'c'
|
||||
RTA_DISPLAY_GAIN_FFT_DB: 20,
|
||||
RTA_DISPLAY_GAIN_IIR_DB: 20,
|
||||
RTA_BAR_LAYOUT: 'rtw', // 'iec' | 'rtw'
|
||||
RTA_BAR_BASE_COLOR: '#ffe066',
|
||||
HEADER_TEXT_COLOR: '#ffe066',
|
||||
RTA_INTEGRATION: 'fast', // 'impulse' | 'fast' | 'slow' | 'peak'
|
||||
RTA_INTEGRATION: 'fast', // 'impulse' | 'fast' | 'medium' | 'slow' | 'average' | 'peak'
|
||||
RTA_BALLISTICS_MODE: 'average', // 'average' | 'peak' | 'both'
|
||||
RTA_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual'
|
||||
RTA_PEAK_HOLD_SEC: 2,
|
||||
RTA_PEAK_HOLD_SEC: 2.5,
|
||||
RTA_PEAK_DECAY_DB_PER_S: 20,
|
||||
RTA_DISPLAY_HOLD_SEC: 0,
|
||||
RTA_IIR_ORDER: 4,
|
||||
RTA_IIR_ORDER: 6,
|
||||
RTA_IIR_TAU_FAST: 0.12,
|
||||
RTA_IIR_TAU_SLOW: 1.0,
|
||||
SPECTRO_GAMMA: 0.4,
|
||||
@@ -156,20 +156,19 @@ const CONFIG = {
|
||||
PPM_DIN_TOP: +5,
|
||||
PPM_DIN_BOTTOM: -50,
|
||||
PPM_DIN_RED_START: 0,
|
||||
// DIN PPM attack and decay times: 5 ms integration and 20 dB in 1.7 s
|
||||
// (≈11.8 dB/s) return, per IEC 60268‑10 Type I.
|
||||
PPM_DIN_ATTACK_MS: 5,
|
||||
// Fixed RTW/DIN profile: 10 ms integration and 20 dB return in 1.5 s.
|
||||
PPM_DIN_ATTACK_MS: 10,
|
||||
// Non-normative: when enabled, PPM DIN attack becomes instant (no integration).
|
||||
// Useful to compensate perceived meter lag caused by device / pipeline latency.
|
||||
PPM_DIN_FAST_ATTACK: false,
|
||||
PPM_DIN_DECAY_DB_PER_S: 11.8,
|
||||
PPM_DIN_DECAY_DB_PER_S: 20 / 1.5,
|
||||
// Hold time for DIN PPM: verlängert auf 1000 ms (1 s), damit der Peak‑Hold
|
||||
// auch bei kurzen Transienten deutlich sichtbar bleibt. Gemäß
|
||||
// Rundfunkpraxis werden Peak‑Hold‑Striche häufig 1–2 Sekunden gehalten.
|
||||
PPM_DIN_HOLD_MS: 1000,
|
||||
// Decay rate for the DIN PPM hold. When the held peak decays, it uses
|
||||
// the same slope as the main meter (≈11.8 dB/s).
|
||||
PPM_DIN_HOLD_DECAY_DB_PER_S: 11.8,
|
||||
// the same slope as the main meter (≈13.33 dB/s).
|
||||
PPM_DIN_HOLD_DECAY_DB_PER_S: 20 / 1.5,
|
||||
|
||||
// Offsets für die PPM-Meter (DIN/EBU-spezifisch). Die EBU/DIN Offsets
|
||||
// ersetzen den früheren globalen PPM_OFFSET.
|
||||
@@ -178,16 +177,18 @@ const CONFIG = {
|
||||
PPM_REF_DBFS_PEAK_FOR_0_DBU: -15.0,
|
||||
|
||||
// Ballistic parameters for the EBU PPM (Type IIb). Attack ~10 ms and
|
||||
// decay 24 dB in 2.8 s (≈8.6 dB/s), per IEC 60268‑10 Type II. A short
|
||||
// decay 24 dB in 2.8 s (≈8.57 dB/s), per EBU Tech 3205-E Type IIb. A short
|
||||
// peak-hold (~750 ms) helps visualise transients. Hold decay matches
|
||||
// the main decay rate.
|
||||
PPM_EBU_ATTACK_MS: 10,
|
||||
PPM_EBU_DECAY_DB_PER_S: 8.6,
|
||||
PPM_EBU_DECAY_DB_PER_S: 24 / 2.8,
|
||||
PPM_EBU_HOLD_MS: 750,
|
||||
PPM_EBU_HOLD_DECAY_DB_PER_S: 8.6,
|
||||
PPM_EBU_HOLD_DECAY_DB_PER_S: 24 / 2.8,
|
||||
CORR_SMOOTH: 0.85,
|
||||
CORR_ZERO_ON_SILENCE: false,
|
||||
CORR_SILENCE_THRESHOLD_RMS_DBFS: -75,
|
||||
CORR_RESPONSE_S: 1.0,
|
||||
CORR_RESET_TOKEN: 0,
|
||||
XY_POINTS: 1024,
|
||||
XY_STYLE: 'lines',
|
||||
XY_SILENCE_GATE_ENABLED: true,
|
||||
@@ -198,6 +199,7 @@ const CONFIG = {
|
||||
GONIO_MANUAL_GAIN_DB: -5,
|
||||
GONIO_AGC_ENABLED: false,
|
||||
GONIO_LINE_FADE_MS: 50,
|
||||
GONIO_PERSISTENCE_MODE: 'fast',
|
||||
PHASE_DISPLAY_GAIN_DB: 0,
|
||||
PHASE_AGC_ENABLED: false,
|
||||
PHASE_TRAIL_ENABLED: true,
|
||||
@@ -599,6 +601,8 @@ const PHOENIX_GLOBAL_OPTION_KEYS = Object.freeze([
|
||||
'CLOCK_LED_COLOR',
|
||||
'HEADER_TEXT_COLOR',
|
||||
'RTA_BAR_BASE_COLOR',
|
||||
'SPECTRO_GAMMA',
|
||||
'SPECTRO_SCROLL_MODE',
|
||||
'PEAK_HISTORY_SCROLL_MODE',
|
||||
'PEAK_HISTORY_FILL_ENABLED',
|
||||
'PEAK_HISTORY_FILL_INVERT',
|
||||
@@ -774,6 +778,14 @@ function loadConfig(opts = {}) {
|
||||
if (typeof saved.PPM_DIN_DECAY_DB_PER_S === 'number') CONFIG.PPM_DIN_DECAY_DB_PER_S = saved.PPM_DIN_DECAY_DB_PER_S;
|
||||
if (typeof saved.PPM_DIN_HOLD_MS === 'number') CONFIG.PPM_DIN_HOLD_MS = saved.PPM_DIN_HOLD_MS;
|
||||
if (CONFIG.RTA_BAR_LAYOUT === 'classic') CONFIG.RTA_BAR_LAYOUT = 'rtw';
|
||||
if (CONFIG.RTA_BAR_LAYOUT === 'rtw') {
|
||||
CONFIG.RTA_ENGINE = 'iir';
|
||||
CONFIG.RTA_BPO_MODE = '1_3';
|
||||
CONFIG.RTA_IIR_ORDER = 6;
|
||||
CONFIG.RTA_FREQ_RANGE = 'norm';
|
||||
const hold = Number(CONFIG.RTA_PEAK_HOLD_SEC);
|
||||
CONFIG.RTA_PEAK_HOLD_SEC = hold >= 3.25 ? 4.0 : 2.5;
|
||||
}
|
||||
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.
|
||||
@@ -827,6 +839,14 @@ function loadConfig(opts = {}) {
|
||||
CONFIG.LR_FRACTIONAL_DELAY_SAMPLES = Math.max(-1.5, Math.min(1.5, Number(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES)));
|
||||
CONFIG.PHOENIX_BASE_URL = normalizePhoenixBaseUrlForCurrentClient(CONFIG.PHOENIX_BASE_URL || CONFIG_DEFAULTS.PHOENIX_BASE_URL || DEFAULT_PHOENIX_BASE_URL);
|
||||
CONFIG.PPM_DIN_FAST_ATTACK = !!CONFIG.PPM_DIN_FAST_ATTACK;
|
||||
// Migrate stale profiles: detector ballistics are standards profiles,
|
||||
// not user-adjustable display preferences.
|
||||
CONFIG.PPM_DIN_ATTACK_MS = 10;
|
||||
CONFIG.PPM_DIN_DECAY_DB_PER_S = 20 / 1.5;
|
||||
CONFIG.PPM_DIN_HOLD_DECAY_DB_PER_S = 20 / 1.5;
|
||||
CONFIG.PPM_EBU_ATTACK_MS = 10;
|
||||
CONFIG.PPM_EBU_DECAY_DB_PER_S = 24 / 2.8;
|
||||
CONFIG.PPM_EBU_HOLD_DECAY_DB_PER_S = 24 / 2.8;
|
||||
CONFIG.HIFI_PEAK_ALIGNMENT = (CONFIG.HIFI_PEAK_ALIGNMENT === 'ppm_din_zero' || CONFIG.HIFI_PEAK_ALIGNMENT === 'dbfs_zero')
|
||||
? 'ppm_din_zero'
|
||||
: 'ppm_din_minus5';
|
||||
@@ -969,6 +989,10 @@ function buildPhoenixGlobalConfigPayload() {
|
||||
if (raw === 'stopwatch') return 'stopwatch';
|
||||
return fallback;
|
||||
};
|
||||
const normalizeSpectroScroll = (value) => {
|
||||
const number = Number(value);
|
||||
return [0.5, 1, 2, 4, 6].includes(number) ? number : 1;
|
||||
};
|
||||
const normalizePeakHistoryScroll = (value) => {
|
||||
const allowed = new Set([0.5, 1, 2, 4, 6]);
|
||||
const num = Number(value);
|
||||
@@ -990,11 +1014,11 @@ function buildPhoenixGlobalConfigPayload() {
|
||||
monoInput: !!CONFIG.MONO_INPUT,
|
||||
lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED,
|
||||
lrFractionalDelaySamples: Number.isFinite(CONFIG.LR_FRACTIONAL_DELAY_SAMPLES) ? CONFIG.LR_FRACTIONAL_DELAY_SAMPLES : (CONFIG_DEFAULTS.LR_FRACTIONAL_DELAY_SAMPLES ?? 0.996),
|
||||
ppmDinAttackMs: Number.isFinite(CONFIG.PPM_DIN_ATTACK_MS) ? CONFIG.PPM_DIN_ATTACK_MS : (CONFIG_DEFAULTS.PPM_DIN_ATTACK_MS ?? 5),
|
||||
ppmDinDecayDbPerS: Number.isFinite(CONFIG.PPM_DIN_DECAY_DB_PER_S) ? CONFIG.PPM_DIN_DECAY_DB_PER_S : (CONFIG_DEFAULTS.PPM_DIN_DECAY_DB_PER_S ?? 11.8),
|
||||
ppmDinAttackMs: 10,
|
||||
ppmDinDecayDbPerS: 20 / 1.5,
|
||||
ppmDinFastAttack: !!CONFIG.PPM_DIN_FAST_ATTACK,
|
||||
ppmEbuAttackMs: Number.isFinite(CONFIG.PPM_EBU_ATTACK_MS) ? CONFIG.PPM_EBU_ATTACK_MS : (CONFIG_DEFAULTS.PPM_EBU_ATTACK_MS ?? 10),
|
||||
ppmEbuDecayDbPerS: Number.isFinite(CONFIG.PPM_EBU_DECAY_DB_PER_S) ? CONFIG.PPM_EBU_DECAY_DB_PER_S : (CONFIG_DEFAULTS.PPM_EBU_DECAY_DB_PER_S ?? 8.6),
|
||||
ppmEbuAttackMs: 10,
|
||||
ppmEbuDecayDbPerS: 24 / 2.8,
|
||||
lufsIWindowMin: Number.isFinite(CONFIG.LUFS_I_WINDOW_MIN) ? CONFIG.LUFS_I_WINDOW_MIN : (CONFIG_DEFAULTS.LUFS_I_WINDOW_MIN ?? 4),
|
||||
lufsINormEnabled: !!CONFIG.LUFS_I_NORM_ENABLED,
|
||||
ppmDinLoudnessBoxes: !!CONFIG.PPM_DIN_LOUDNESS_BOXES,
|
||||
@@ -1016,6 +1040,8 @@ function buildPhoenixGlobalConfigPayload() {
|
||||
clockLedColor: normalizeColor(CONFIG.CLOCK_LED_COLOR, CONFIG_DEFAULTS.CLOCK_LED_COLOR || '#ff0000'),
|
||||
headerTextColor: normalizeColor(CONFIG.HEADER_TEXT_COLOR, CONFIG_DEFAULTS.HEADER_TEXT_COLOR || '#ffe066'),
|
||||
rtaBarBaseColor: normalizeColor(CONFIG.RTA_BAR_BASE_COLOR, CONFIG_DEFAULTS.RTA_BAR_BASE_COLOR || '#ffe066'),
|
||||
spectroGamma: Number.isFinite(CONFIG.SPECTRO_GAMMA) ? Math.max(0.3, Math.min(1.2, CONFIG.SPECTRO_GAMMA)) : 0.9,
|
||||
spectroScrollMode: normalizeSpectroScroll(CONFIG.SPECTRO_SCROLL_MODE),
|
||||
peakHistoryScrollMode: normalizePeakHistoryScroll(CONFIG.PEAK_HISTORY_SCROLL_MODE),
|
||||
peakHistoryFillEnabled: !!CONFIG.PEAK_HISTORY_FILL_ENABLED,
|
||||
peakHistoryFillInvert: !!CONFIG.PEAK_HISTORY_FILL_INVERT,
|
||||
@@ -1063,6 +1089,10 @@ function applyPhoenixGlobalConfig(payload = {}) {
|
||||
if (raw === 'stopwatch') return 'stopwatch';
|
||||
return fallback;
|
||||
};
|
||||
const normalizeSpectroScroll = (value) => {
|
||||
const number = Number(value);
|
||||
return [0.5, 1, 2, 4, 6].includes(number) ? number : 1;
|
||||
};
|
||||
const fft = Number(payload.fftSize);
|
||||
if (Number.isFinite(fft)) {
|
||||
CONFIG.FFT_SIZE = (fft === 2048 || fft === 4096 || fft === 8192 || fft === 16384)
|
||||
@@ -1073,15 +1103,20 @@ function applyPhoenixGlobalConfig(payload = {}) {
|
||||
const mode = String(payload.rtaBpoMode || '').trim();
|
||||
CONFIG.RTA_BPO_MODE = (mode === '1_3' || mode === '1_6' || mode === '1_12') ? mode : '1_6';
|
||||
}
|
||||
const dinAttack = Number(payload.ppmDinAttackMs);
|
||||
if (Number.isFinite(dinAttack)) CONFIG.PPM_DIN_ATTACK_MS = Math.max(0.1, Math.min(100, dinAttack));
|
||||
const dinDecay = Number(payload.ppmDinDecayDbPerS);
|
||||
if (Number.isFinite(dinDecay)) CONFIG.PPM_DIN_DECAY_DB_PER_S = Math.max(0.1, Math.min(120, dinDecay));
|
||||
if (CONFIG.RTA_BAR_LAYOUT === 'rtw') {
|
||||
CONFIG.RTA_ENGINE = 'iir';
|
||||
CONFIG.RTA_BPO_MODE = '1_3';
|
||||
CONFIG.RTA_IIR_ORDER = 6;
|
||||
CONFIG.RTA_FREQ_RANGE = 'norm';
|
||||
}
|
||||
const spectroGamma = Number(payload.spectroGamma);
|
||||
if (Number.isFinite(spectroGamma)) CONFIG.SPECTRO_GAMMA = Math.max(0.3, Math.min(1.2, spectroGamma));
|
||||
if ('spectroScrollMode' in payload) CONFIG.SPECTRO_SCROLL_MODE = normalizeSpectroScroll(payload.spectroScrollMode);
|
||||
CONFIG.PPM_DIN_ATTACK_MS = 10;
|
||||
CONFIG.PPM_DIN_DECAY_DB_PER_S = 20 / 1.5;
|
||||
if ('ppmDinFastAttack' in payload) CONFIG.PPM_DIN_FAST_ATTACK = !!payload.ppmDinFastAttack;
|
||||
const ebuAttack = Number(payload.ppmEbuAttackMs);
|
||||
if (Number.isFinite(ebuAttack)) CONFIG.PPM_EBU_ATTACK_MS = Math.max(0.1, Math.min(100, ebuAttack));
|
||||
const ebuDecay = Number(payload.ppmEbuDecayDbPerS);
|
||||
if (Number.isFinite(ebuDecay)) CONFIG.PPM_EBU_DECAY_DB_PER_S = Math.max(0.1, Math.min(120, ebuDecay));
|
||||
CONFIG.PPM_EBU_ATTACK_MS = 10;
|
||||
CONFIG.PPM_EBU_DECAY_DB_PER_S = 24 / 2.8;
|
||||
const lufsWin = Number(payload.lufsIWindowMin);
|
||||
if (Number.isFinite(lufsWin)) CONFIG.LUFS_I_WINDOW_MIN = Math.max(1, Math.min(10, Math.round(lufsWin)));
|
||||
if ('lufsINormEnabled' in payload) CONFIG.LUFS_I_NORM_ENABLED = !!payload.lufsINormEnabled;
|
||||
|
||||
Reference in New Issue
Block a user