Correct audio metering and realtime displays
This commit is contained in:
+69
-351
@@ -13,30 +13,19 @@ const METER_SLOTS = 3;
|
||||
const METER_PAD_TOP = 30;
|
||||
const METER_PAD_BOTTOM = 20;
|
||||
const METER_EXTRA_BOTTOM_PAD = 6;
|
||||
const TARGET_POINTS = 1024;
|
||||
const COLOR_STOPS = [
|
||||
{ t: 0.0, color: [0, 0, 50] },
|
||||
{ t: 0.3, color: [0, 120, 180] },
|
||||
{ t: 0.6, color: [0, 205, 120] },
|
||||
{ t: 0.8, color: [210, 220, 0] },
|
||||
{ t: 1.0, color: [255, 120, 0] },
|
||||
];
|
||||
const RING_DBFS = [0, -8, -16, -24, -32, -40];
|
||||
const RING_PPM_DIN = [+5, 0, -10, -20, -30, -40, -50];
|
||||
const PHASE_GAIN_MIN_DB = -35;
|
||||
const PHASE_GAIN_MAX_DB = 35;
|
||||
const PHASE_ALIGN_TARGET = Math.pow(10, -15 / 20);
|
||||
const PHASE_AGC_TARGET_DB = linearToDb(PHASE_ALIGN_TARGET);
|
||||
const PHASE_AGC_BASE_GAIN = 1 / PHASE_ALIGN_TARGET;
|
||||
const PHASE_AGC_ATTACK_S = 0.02;
|
||||
const PHASE_AGC_RELEASE_DB_PER_S = 12;
|
||||
const PHASE_LEVEL_THRESHOLD_DB = -60;
|
||||
const PHASE_LEVEL_THRESHOLD = dbToLinear(PHASE_LEVEL_THRESHOLD_DB);
|
||||
const PHASE_IDLE_DECAY = 0.85;
|
||||
const PHASE_PHASE_SMOOTH_ALPHA = 0.08;
|
||||
const PHASE_RADIUS_SMOOTH_ALPHA = 0.18;
|
||||
const PHASE_BANDPASS_LOW_HZ = 300;
|
||||
const PHASE_BANDPASS_HIGH_HZ = 5000;
|
||||
const PHASE_IDLE_TAU_S = 0.103;
|
||||
const PHASE_SMOOTH_TAU_S = 0.2;
|
||||
const PHASE_RADIUS_TAU_S = 0.085;
|
||||
const PHASE_TRAIL_FADE_MS = 2000;
|
||||
const PHASE_TRAIL_MIN_STEP_MS = 1000 / 45;
|
||||
const PHASE_TRAIL_MIN_DIST_PX = 1.5;
|
||||
@@ -48,32 +37,10 @@ const PHASE_SECTORS = [
|
||||
{ startDeg: -180, endDeg: -60, color: 'rgba(255,120,120,0.25)' },
|
||||
];
|
||||
|
||||
// Numerisch stabilere Hilbert-Kernel-Erstellung
|
||||
const HILBERT_KERNEL = buildHilbertKernel(33);
|
||||
const HILBERT_HALF = (HILBERT_KERNEL.length - 1) / 2;
|
||||
|
||||
// Lookup-Tables für häufig verwendete Werte
|
||||
const ANGLE_COS = new Float32Array(360);
|
||||
const ANGLE_SIN = new Float32Array(360);
|
||||
for (let i = 0; i < 360; i++) {
|
||||
const rad = (i * Math.PI) / 180;
|
||||
ANGLE_COS[i] = Math.cos(rad);
|
||||
ANGLE_SIN[i] = Math.sin(rad);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
traceBuffer: new Float32Array(0),
|
||||
ampBuffer: new Float32Array(0),
|
||||
filteredL: new Float32Array(0),
|
||||
filteredR: new Float32Array(0),
|
||||
phaseAngleBuffer: new Float32Array(0),
|
||||
phaseAmplitudeBuffer: new Float32Array(0),
|
||||
phaseWeightBuffer: new Float32Array(0),
|
||||
phaseAnalysisSeq: -1,
|
||||
phaseAnalysisLength: 0,
|
||||
phaseAnalysisSampleRate: 0,
|
||||
phaseAnalysisCount: 0,
|
||||
phaseDisplaySeq: -1,
|
||||
phaseDisplayLastTs: 0,
|
||||
phaseConfidence: 0,
|
||||
currentPhase: null,
|
||||
currentRadius: 0,
|
||||
@@ -82,9 +49,7 @@ export function init() {
|
||||
phaseAgcEnv: 1e-3,
|
||||
phaseAgcGainDb: 0,
|
||||
phaseAgcLastTs: 0,
|
||||
bandpass: createBandpassState(),
|
||||
bufferGrowthCount: 0,
|
||||
maxBufferSize: 0,
|
||||
phaseAgcSeq: -1,
|
||||
phaseTrail: [],
|
||||
staticLayerCanvas: null,
|
||||
staticLayerKey: '',
|
||||
@@ -107,13 +72,13 @@ export async function render(env, state) {
|
||||
const now = getNow();
|
||||
|
||||
drawStaticLayer(g, state, rect, layout, CONFIG, slots.length);
|
||||
const xyData = extractXYData(audio);
|
||||
if (xyData.ready) {
|
||||
const gainCtrl = resolvePhaseGain(state, xyData, CONFIG);
|
||||
const trace = buildWheelTrace(state, xyData, layout.wheel, gainCtrl.gain, CONFIG, audio);
|
||||
renderWheel(g, trace, layout.wheel, state, CONFIG, now);
|
||||
const phaseData = extractPhaseData(audio);
|
||||
if (phaseData.ready) {
|
||||
const gainCtrl = resolvePhaseGain(state, phaseData, CONFIG);
|
||||
const updated = updatePhasePointer(state, phaseData, gainCtrl.gain, CONFIG, audio, now);
|
||||
renderWheel(g, updated, layout.wheel, state, CONFIG, now);
|
||||
} else {
|
||||
decayPhasePointer(state);
|
||||
updatePhasePointer(state, phaseData, 1, CONFIG, audio, now);
|
||||
trimPhaseTrail(state, CONFIG, now);
|
||||
drawPhaseTrail(g, layout.wheel, state, CONFIG, now);
|
||||
drawPhasePointer(g, layout.wheel, state);
|
||||
@@ -327,77 +292,54 @@ function radToDeg(rad) {
|
||||
return (rad * 180) / Math.PI;
|
||||
}
|
||||
|
||||
function extractXYData(audio) {
|
||||
const xyL = audio?.xyL;
|
||||
const xyR = audio?.xyR;
|
||||
const isVec = (v) => v && (Array.isArray(v) || ArrayBuffer.isView(v));
|
||||
const ready = !!(audio?.alive && isVec(xyL) && isVec(xyR) && xyL.length && xyR.length);
|
||||
function extractPhaseData(audio) {
|
||||
const angle = Number(audio?.phaseAngleRad);
|
||||
const level = Number(audio?.phaseLevel);
|
||||
const peak = Number(audio?.phasePeak);
|
||||
return {
|
||||
ready,
|
||||
xyL,
|
||||
xyR,
|
||||
length: ready ? Math.min(xyL.length, xyR.length) : 0,
|
||||
sampleRate: audio?.sampleRate || 48000,
|
||||
seq: Number(audio?.xySeq) || 0,
|
||||
ready: !!audio?.alive,
|
||||
angle: Number.isFinite(angle) ? angle : null,
|
||||
coherence: clamp01(Number(audio?.phaseCoherence) || 0),
|
||||
level: Number.isFinite(level) ? Math.max(0, level) : 0,
|
||||
peak: Number.isFinite(peak) ? Math.max(0, peak) : 0,
|
||||
seq: Number(audio?.phaseSeq) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWheelTrace(state, xyData, wheel, gain = 1, CONFIG, audio) {
|
||||
if (!xyData.ready || !xyData.length) {
|
||||
decayPhasePointer(state);
|
||||
return null;
|
||||
}
|
||||
const analysis = preparePhaseAnalysis(state, xyData);
|
||||
function updatePhasePointer(state, phaseData, gain = 1, CONFIG, audio, nowMs) {
|
||||
const seq = Number(phaseData?.seq) || 0;
|
||||
if (seq > 0 && state.phaseDisplaySeq === seq) return false;
|
||||
const now = Number.isFinite(nowMs) ? nowMs : getNow();
|
||||
const previousTs = Number.isFinite(state.phaseDisplayLastTs) && state.phaseDisplayLastTs > 0
|
||||
? state.phaseDisplayLastTs
|
||||
: now - (1000 / 60);
|
||||
const dt = Math.max(1 / 240, Math.min(0.25, (now - previousTs) / 1000));
|
||||
state.phaseDisplayLastTs = now;
|
||||
state.phaseDisplaySeq = seq;
|
||||
|
||||
const amplitudeMode = getPhaseAmplitudeMode(CONFIG);
|
||||
const ringDbValues = getRingDbValues(CONFIG);
|
||||
const ppmRadiusNorm = amplitudeMode === 'ppm-din'
|
||||
? computePpmDinRadiusNorm(audio, CONFIG, ringDbValues)
|
||||
: 0;
|
||||
const radius = wheel.radius;
|
||||
let ampIdx = 0;
|
||||
let sumRadius = 0;
|
||||
let levelAcc = 0;
|
||||
const gainLinear = Number.isFinite(gain) ? gain : 1;
|
||||
|
||||
for (let i = 0; i < analysis.count; i++) {
|
||||
const angle = analysis.angles[i];
|
||||
const amp = analysis.amplitudes[i];
|
||||
const ampScaled = Math.min(1, amp * gainLinear);
|
||||
const radiusNorm = amplitudeMode === 'ppm-din'
|
||||
? ppmRadiusNorm
|
||||
: linearToRadiusNorm(ampScaled, ringDbValues);
|
||||
ampIdx++;
|
||||
sumRadius += radiusNorm;
|
||||
levelAcc += amp * amp;
|
||||
}
|
||||
|
||||
if (ampIdx > 0) {
|
||||
const invCount = 1 / ampIdx;
|
||||
const phaseSummary = summarizeWeightedPhase(
|
||||
analysis.angles,
|
||||
analysis.weights,
|
||||
analysis.count,
|
||||
);
|
||||
const avgRadius = amplitudeMode === 'ppm-din' ? ppmRadiusNorm : (sumRadius * invCount);
|
||||
const blockLevel = Math.sqrt(levelAcc * invCount);
|
||||
if (blockLevel >= PHASE_LEVEL_THRESHOLD && phaseSummary) {
|
||||
const avgAngle = phaseSummary.angle;
|
||||
const prevPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : avgAngle;
|
||||
const prevRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : avgRadius;
|
||||
state.currentPhase = avgAngle;
|
||||
state.currentRadius = avgRadius;
|
||||
state.phaseConfidence = phaseSummary.coherence;
|
||||
state.smoothPhase = smoothAngle(prevPhase, avgAngle, PHASE_PHASE_SMOOTH_ALPHA);
|
||||
state.smoothRadius = lerp(prevRadius, avgRadius, PHASE_RADIUS_SMOOTH_ALPHA);
|
||||
} else {
|
||||
state.phaseConfidence = 0;
|
||||
decayPhasePointer(state);
|
||||
}
|
||||
const targetRadius = amplitudeMode === 'ppm-din'
|
||||
? ppmRadiusNorm
|
||||
: linearToRadiusNorm(Math.min(1, phaseData.level * gainLinear), ringDbValues);
|
||||
if (Number.isFinite(phaseData.angle) && phaseData.level >= PHASE_LEVEL_THRESHOLD) {
|
||||
const targetPhase = wrapAngle(phaseData.angle - Math.PI / 2);
|
||||
const prevPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : targetPhase;
|
||||
const prevRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : targetRadius;
|
||||
state.currentPhase = targetPhase;
|
||||
state.currentRadius = targetRadius;
|
||||
state.phaseConfidence = phaseData.coherence;
|
||||
state.smoothPhase = smoothAngle(prevPhase, targetPhase, smoothingAlpha(dt, PHASE_SMOOTH_TAU_S));
|
||||
state.smoothRadius = lerp(prevRadius, targetRadius, smoothingAlpha(dt, PHASE_RADIUS_TAU_S));
|
||||
} else {
|
||||
state.phaseConfidence = 0;
|
||||
decayPhasePointer(state);
|
||||
decayPhasePointer(state, dt);
|
||||
}
|
||||
return { count: ampIdx, radius };
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderWheel(g, trace, wheel, state, CONFIG, timestamp) {
|
||||
@@ -650,11 +592,6 @@ function trailColorForAngle(angle) {
|
||||
return '#ff7a78';
|
||||
}
|
||||
|
||||
function clamp1(value) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(-1, Math.min(1, value));
|
||||
}
|
||||
|
||||
function ringFraction(index, ringCount) {
|
||||
const n = Math.max(1, ringCount | 0);
|
||||
const idx = Math.max(0, Math.min(n - 1, index | 0));
|
||||
@@ -722,205 +659,32 @@ function computePpmDinRadiusNorm(audio, cfg, ringDbValues) {
|
||||
return dbToRadiusNorm(db, ringDbValues);
|
||||
}
|
||||
|
||||
function createBandpassState() {
|
||||
return {
|
||||
sampleRate: 0,
|
||||
hpAlpha: 0,
|
||||
lpAlpha: 0,
|
||||
channels: {
|
||||
L: { hpX: 0, hpY: 0, lpY: 0 },
|
||||
R: { hpX: 0, hpY: 0, lpY: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureBandpassCoeffs(state, sampleRate) {
|
||||
if (!state.bandpass) state.bandpass = createBandpassState();
|
||||
const sr = Math.max(8000, Math.round(sampleRate) || 48000);
|
||||
if (state.bandpass.sampleRate === sr) return;
|
||||
state.bandpass.sampleRate = sr;
|
||||
state.bandpass.hpAlpha = computeHighpassAlpha(sr, PHASE_BANDPASS_LOW_HZ);
|
||||
state.bandpass.lpAlpha = computeLowpassAlpha(sr, PHASE_BANDPASS_HIGH_HZ);
|
||||
}
|
||||
|
||||
function computeHighpassAlpha(sampleRate, cutoff) {
|
||||
const rc = 1 / (2 * Math.PI * Math.max(1, cutoff));
|
||||
const dt = 1 / Math.max(1, sampleRate);
|
||||
return Math.max(0, Math.min(1, rc / (rc + dt)));
|
||||
}
|
||||
|
||||
function computeLowpassAlpha(sampleRate, cutoff) {
|
||||
const rc = 1 / (2 * Math.PI * Math.max(1, cutoff));
|
||||
const dt = 1 / Math.max(1, sampleRate);
|
||||
return Math.max(0, Math.min(1, dt / (rc + dt)));
|
||||
}
|
||||
|
||||
function ensureFilteredBuffers(state, length) {
|
||||
const neededLength = Math.ceil(length * 1.1); // 10% Puffer für Stabilität
|
||||
if (!state.filteredL || state.filteredL.length < neededLength) {
|
||||
state.filteredL = new Float32Array(neededLength);
|
||||
}
|
||||
if (!state.filteredR || state.filteredR.length < neededLength) {
|
||||
state.filteredR = new Float32Array(neededLength);
|
||||
}
|
||||
return { L: state.filteredL, R: state.filteredR };
|
||||
}
|
||||
|
||||
function applyBandpassSample(sample, channelState, bandpassState) {
|
||||
const hpAlpha = bandpassState.hpAlpha;
|
||||
const lpAlpha = bandpassState.lpAlpha;
|
||||
|
||||
if (!Number.isFinite(sample)) sample = 0;
|
||||
|
||||
const hpY = hpAlpha * (channelState.hpY + sample - channelState.hpX);
|
||||
channelState.hpY = Number.isFinite(hpY) ? hpY : 0;
|
||||
channelState.hpX = sample;
|
||||
|
||||
const lpY = lpAlpha * hpY + (1 - lpAlpha) * channelState.lpY;
|
||||
channelState.lpY = Number.isFinite(lpY) ? lpY : 0;
|
||||
|
||||
return lpY;
|
||||
}
|
||||
|
||||
function preparePhaseFilteredBuffers(state, xyData) {
|
||||
ensureBandpassCoeffs(state, xyData.sampleRate || 48000);
|
||||
const filtered = ensureFilteredBuffers(state, xyData.length);
|
||||
|
||||
// Reset channel states if they contain NaN/Infinity
|
||||
if (!Number.isFinite(state.bandpass.channels.L.hpY)) {
|
||||
state.bandpass.channels.L = { hpX: 0, hpY: 0, lpY: 0 };
|
||||
}
|
||||
if (!Number.isFinite(state.bandpass.channels.R.hpY)) {
|
||||
state.bandpass.channels.R = { hpX: 0, hpY: 0, lpY: 0 };
|
||||
}
|
||||
|
||||
for (let i = 0; i < xyData.length; i++) {
|
||||
filtered.L[i] = applyBandpassSample(
|
||||
xyData.xyL[i],
|
||||
state.bandpass.channels.L,
|
||||
state.bandpass
|
||||
);
|
||||
filtered.R[i] = applyBandpassSample(
|
||||
xyData.xyR[i],
|
||||
state.bandpass.channels.R,
|
||||
state.bandpass
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function ensurePhaseAnalysisBuffers(state, length) {
|
||||
if (!state.phaseAngleBuffer || state.phaseAngleBuffer.length < length) {
|
||||
state.phaseAngleBuffer = new Float32Array(length);
|
||||
}
|
||||
if (!state.phaseAmplitudeBuffer || state.phaseAmplitudeBuffer.length < length) {
|
||||
state.phaseAmplitudeBuffer = new Float32Array(length);
|
||||
}
|
||||
if (!state.phaseWeightBuffer || state.phaseWeightBuffer.length < length) {
|
||||
state.phaseWeightBuffer = new Float32Array(length);
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeWeightedPhase(angles, weights, count) {
|
||||
const limit = Math.max(0, Math.min(
|
||||
Number.isFinite(count) ? Math.floor(count) : 0,
|
||||
angles?.length || 0,
|
||||
weights?.length || 0,
|
||||
));
|
||||
let sumSin = 0;
|
||||
let sumCos = 0;
|
||||
let sumWeight = 0;
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const angle = Number(angles[i]);
|
||||
const weight = Number(weights[i]);
|
||||
if (!Number.isFinite(angle) || !Number.isFinite(weight) || weight <= 0) continue;
|
||||
sumSin += Math.sin(angle) * weight;
|
||||
sumCos += Math.cos(angle) * weight;
|
||||
sumWeight += weight;
|
||||
}
|
||||
if (!(sumWeight > 1e-12)) return null;
|
||||
const resultant = Math.hypot(sumSin, sumCos);
|
||||
return {
|
||||
angle: Math.atan2(sumSin, sumCos),
|
||||
coherence: Math.max(0, Math.min(1, resultant / sumWeight)),
|
||||
weight: sumWeight,
|
||||
};
|
||||
}
|
||||
|
||||
function preparePhaseAnalysis(state, xyData) {
|
||||
const sampleRate = Math.max(1, Math.round(xyData.sampleRate) || 48000);
|
||||
const seq = Number(xyData.seq) || 0;
|
||||
const canReuse = seq > 0
|
||||
&& state.phaseAnalysisSeq === seq
|
||||
&& state.phaseAnalysisLength === xyData.length
|
||||
&& state.phaseAnalysisSampleRate === sampleRate;
|
||||
if (canReuse) {
|
||||
return {
|
||||
angles: state.phaseAngleBuffer,
|
||||
amplitudes: state.phaseAmplitudeBuffer,
|
||||
weights: state.phaseWeightBuffer,
|
||||
count: state.phaseAnalysisCount,
|
||||
};
|
||||
}
|
||||
|
||||
const filtered = preparePhaseFilteredBuffers(state, xyData);
|
||||
const targetPoints = Math.min(TARGET_POINTS, xyData.length);
|
||||
const step = Math.max(1, Math.floor(xyData.length / targetPoints));
|
||||
const sampleCount = Math.ceil(xyData.length / step);
|
||||
ensurePhaseAnalysisBuffers(state, sampleCount);
|
||||
let count = 0;
|
||||
for (let i = 0; i < xyData.length; i += step) {
|
||||
const lRe = clamp1(filtered.L[i]);
|
||||
const rRe = clamp1(filtered.R[i]);
|
||||
const lIm = hilbertAt(filtered.L, i);
|
||||
const rIm = hilbertAt(filtered.R, i);
|
||||
const phaseL = Math.atan2(lIm, lRe);
|
||||
const phaseR = Math.atan2(rIm, rRe);
|
||||
let phaseDiff = phaseL - phaseR;
|
||||
if (!Number.isFinite(phaseDiff)) continue;
|
||||
phaseDiff = wrapAngle(phaseDiff);
|
||||
const magL = Math.min(1, Math.hypot(lRe, lIm));
|
||||
const magR = Math.min(1, Math.hypot(rRe, rIm));
|
||||
state.phaseAngleBuffer[count] = phaseDiff - Math.PI / 2;
|
||||
state.phaseAmplitudeBuffer[count] = Math.min(1, 0.5 * (magL + magR));
|
||||
// The phase of a strong component must contribute more than the phase of
|
||||
// a quiet or one-sided component. magL * magR is the magnitude of the
|
||||
// complex L/R cross power for this analytic sample.
|
||||
state.phaseWeightBuffer[count] = magL * magR;
|
||||
count++;
|
||||
}
|
||||
state.phaseAnalysisSeq = seq;
|
||||
state.phaseAnalysisLength = xyData.length;
|
||||
state.phaseAnalysisSampleRate = sampleRate;
|
||||
state.phaseAnalysisCount = count;
|
||||
return {
|
||||
angles: state.phaseAngleBuffer,
|
||||
amplitudes: state.phaseAmplitudeBuffer,
|
||||
weights: state.phaseWeightBuffer,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
function decayPhasePointer(state) {
|
||||
function decayPhasePointer(state, dt = 1 / 60) {
|
||||
const prevPhase = Number.isFinite(state.currentPhase) ? state.currentPhase : 0;
|
||||
const prevRadius = Number.isFinite(state.currentRadius) ? state.currentRadius : 0;
|
||||
const decayedRadius = prevRadius * PHASE_IDLE_DECAY;
|
||||
const decayedRadius = prevRadius * Math.exp(-Math.max(0, dt) / PHASE_IDLE_TAU_S);
|
||||
state.currentPhase = prevPhase;
|
||||
state.currentRadius = decayedRadius;
|
||||
const prevSmoothPhase = Number.isFinite(state.smoothPhase) ? state.smoothPhase : prevPhase;
|
||||
const prevSmoothRadius = Number.isFinite(state.smoothRadius) ? state.smoothRadius : decayedRadius;
|
||||
state.smoothPhase = smoothAngle(prevSmoothPhase, prevPhase, PHASE_PHASE_SMOOTH_ALPHA * 0.5);
|
||||
state.smoothRadius = lerp(prevSmoothRadius, decayedRadius, PHASE_RADIUS_SMOOTH_ALPHA);
|
||||
state.smoothPhase = prevSmoothPhase;
|
||||
state.smoothRadius = lerp(prevSmoothRadius, decayedRadius, smoothingAlpha(dt, PHASE_RADIUS_TAU_S));
|
||||
}
|
||||
|
||||
function resolvePhaseGain(state, xyData, CONFIG) {
|
||||
function resolvePhaseGain(state, phaseData, CONFIG) {
|
||||
const gainDb = clampPhaseGain(CONFIG?.PHASE_DISPLAY_GAIN_DB ?? 0);
|
||||
const allowAgc = CONFIG?.PHASE_AGC_ENABLED && getPhaseAmplitudeMode(CONFIG) !== 'ppm-din';
|
||||
if (allowAgc && xyData?.ready) {
|
||||
const auto = computePhaseAgcGain(state, xyData);
|
||||
return { gainDb: auto.gainDb, gain: auto.gain * PHASE_AGC_BASE_GAIN };
|
||||
if (allowAgc && phaseData?.ready) {
|
||||
const seq = Number(phaseData.seq) || 0;
|
||||
if (seq > 0 && state.phaseAgcSeq === seq) {
|
||||
return { gainDb: state.phaseAgcGainDb, gain: dbToLinear(state.phaseAgcGainDb) };
|
||||
}
|
||||
const auto = computePhaseAgcGain(state, phaseData);
|
||||
state.phaseAgcSeq = seq;
|
||||
return { gainDb: auto.gainDb, gain: auto.gain };
|
||||
}
|
||||
state.phaseAgcGainDb = gainDb;
|
||||
state.phaseAgcSeq = Number(phaseData?.seq) || 0;
|
||||
return { gainDb, gain: dbToLinear(gainDb) };
|
||||
}
|
||||
|
||||
@@ -949,51 +713,22 @@ function lerp(a, b, t) {
|
||||
return a + (b - a) * clampedT;
|
||||
}
|
||||
|
||||
function hilbertAt(buffer, idx) {
|
||||
if (!buffer || idx < 0 || idx >= buffer.length) return 0;
|
||||
|
||||
let acc = 0;
|
||||
for (let k = 0; k < HILBERT_KERNEL.length; k++) {
|
||||
const src = idx + k - HILBERT_HALF;
|
||||
if (src < 0 || src >= buffer.length) continue;
|
||||
const sample = buffer[src];
|
||||
const kernel = HILBERT_KERNEL[k];
|
||||
if (Number.isFinite(sample) && Number.isFinite(kernel)) {
|
||||
acc += sample * kernel;
|
||||
}
|
||||
}
|
||||
return Number.isFinite(acc) ? acc : 0;
|
||||
export function smoothingAlpha(dt, tau) {
|
||||
const seconds = Number.isFinite(dt) ? Math.max(0, dt) : 0;
|
||||
const timeConstant = Number.isFinite(tau) ? Math.max(1e-6, tau) : 1e-6;
|
||||
return 1 - Math.exp(-seconds / timeConstant);
|
||||
}
|
||||
|
||||
function buildHilbertKernel(size = 33) {
|
||||
const taps = size % 2 === 0 ? size + 1 : size;
|
||||
const mid = (taps - 1) / 2;
|
||||
const kernel = new Float32Array(taps);
|
||||
|
||||
for (let n = 0; n < taps; n++) {
|
||||
const k = n - mid;
|
||||
|
||||
// Verbesserte numerische Stabilität + even k = 0 wie idealer Hilbert-Kernel
|
||||
if (Math.abs(k) < 1e-10 || k % 2 === 0) {
|
||||
kernel[n] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const window = 0.54 - 0.46 * Math.cos((2 * Math.PI * n) / Math.max(1, taps - 1));
|
||||
const value = (2 / (Math.PI * k)) * window;
|
||||
|
||||
// Sicherstellen, dass der Wert finite ist
|
||||
kernel[n] = Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
return kernel;
|
||||
function clamp01(value) {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
||||
}
|
||||
|
||||
function computePhaseAgcGain(state, xyData) {
|
||||
function computePhaseAgcGain(state, phaseData) {
|
||||
const now = getNow();
|
||||
const dt = state.phaseAgcLastTs ? Math.max(0, (now - state.phaseAgcLastTs) / 1000) : 0;
|
||||
state.phaseAgcLastTs = now;
|
||||
|
||||
const peak = measurePhasePeak(xyData);
|
||||
const peak = Math.max(1e-8, Number(phaseData?.peak) || 0);
|
||||
let env = Number.isFinite(state.phaseAgcEnv) && state.phaseAgcEnv > 0 ? state.phaseAgcEnv : 1e-3;
|
||||
|
||||
if (peak >= env) {
|
||||
@@ -1017,23 +752,6 @@ function computePhaseAgcGain(state, xyData) {
|
||||
return { gainDb, gain: dbToLinear(gainDb) };
|
||||
}
|
||||
|
||||
function measurePhasePeak(xyData) {
|
||||
if (!xyData || !xyData.ready) return 1e-4; // Verbesserter Default-Wert
|
||||
|
||||
let peak = 1e-8; // Höhere Präzision
|
||||
const len = Math.min(xyData.length, 1000); // Begrenzung für Performance
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const sample = Math.max(
|
||||
Math.abs(xyData.xyL[i] || 0),
|
||||
Math.abs(xyData.xyR[i] || 0)
|
||||
);
|
||||
if (sample > peak) peak = sample;
|
||||
}
|
||||
|
||||
return Math.max(1e-8, peak); // Sicherstellen, dass nicht 0 zurückgegeben wird
|
||||
}
|
||||
|
||||
function clampPhaseGain(db) {
|
||||
let val = Number(db);
|
||||
if (!Number.isFinite(val)) val = 0;
|
||||
|
||||
Reference in New Issue
Block a user