Add configurable peak hold grace time for VU meter

This commit is contained in:
Armin 2026-07-08 18:26:26 +02:00
commit 34c0a4ccf4

View file

@ -219,6 +219,15 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
<option value="0.9">Fast</option> <option value="0.9">Fast</option>
<option value="0.8">Very Fast</option> <option value="0.8">Very Fast</option>
</select> </select>
<span style="font-size:11px;color:#6b7089;margin-left:8px">Peak Hold:</span>
<select id="vuPeakHoldSelect">
<option value="0" selected>Off</option>
<option value="6">100ms</option>
<option value="12">200ms</option>
<option value="30">500ms</option>
<option value="60">1s</option>
<option value="120">2s</option>
</select>
<span style="font-size:11px;color:#6b7089;margin-left:8px">Crossfade:</span> <span style="font-size:11px;color:#6b7089;margin-left:8px">Crossfade:</span>
<select id="crossfadeSelect"> <select id="crossfadeSelect">
<option value="0" selected>Off</option> <option value="0" selected>Off</option>
@ -355,6 +364,8 @@ let peakData = [];
let smoothedData = []; let smoothedData = [];
let vuPeakL = 0, vuPeakR = 0; let vuPeakL = 0, vuPeakR = 0;
let vuSmoothL = 0, vuSmoothR = 0; let vuSmoothL = 0, vuSmoothR = 0;
let vuPeakHoldL = 0, vuPeakHoldR = 0;
let vuPeakGrace = 0;
function bessi0(x) { function bessi0(x) {
let s = 1, t = 1; let s = 1, t = 1;
for (let k = 1; k <= 20; k++) { for (let k = 1; k <= 20; k++) {
@ -572,10 +583,11 @@ function drawSpectrum() {
if (levelR > vuSmoothR) vuSmoothR = levelR; else vuSmoothR = vuSmoothR * vuSmooth + levelR * (1 - vuSmooth); if (levelR > vuSmoothR) vuSmoothR = levelR; else vuSmoothR = vuSmoothR * vuSmooth + levelR * (1 - vuSmooth);
levelL = vuSmoothL; levelL = vuSmoothL;
levelR = vuSmoothR; levelR = vuSmoothR;
function drawVuRow(y, level, peak) { function drawVuRow(y, level, peak, hold) {
const lit = Math.round(level * segCount); const lit = Math.round(level * segCount);
if (level > peak) peak = level; if (level > peak) { peak = level; hold = 0; }
else peak *= 0.990; else if (hold < vuPeakGrace) { hold++; }
else { peak *= 0.990; }
for (let i = 0; i < segCount; i++) { for (let i = 0; i < segCount; i++) {
const sx = segLeft + i * (segW + segG); const sx = segLeft + i * (segW + segG);
const on = i < lit; const on = i < lit;
@ -594,10 +606,12 @@ function drawSpectrum() {
vuctx.fillRect(sx, y, segW, rowH); vuctx.fillRect(sx, y, segW, rowH);
} }
} }
return peak; return [peak, hold];
} }
vuPeakL = drawVuRow(0, levelL, vuPeakL); const rL = drawVuRow(0, levelL, vuPeakL, vuPeakHoldL);
vuPeakR = drawVuRow(gap + rowH, levelR, vuPeakR); vuPeakL = rL[0]; vuPeakHoldL = rL[1];
const rR = drawVuRow(gap + rowH, levelR, vuPeakR, vuPeakHoldR);
vuPeakR = rR[0]; vuPeakHoldR = rR[1];
} }
animId = requestAnimationFrame(drawSpectrum); animId = requestAnimationFrame(drawSpectrum);
} }
@ -723,6 +737,7 @@ const errorEl = document.getElementById('error');
const windowSelect = document.getElementById('windowSelect'); const windowSelect = document.getElementById('windowSelect');
const decaySelect = document.getElementById('decaySelect'); const decaySelect = document.getElementById('decaySelect');
const barDecaySelect = document.getElementById('barDecaySelect'); const barDecaySelect = document.getElementById('barDecaySelect');
const vuPeakHoldSelect = document.getElementById('vuPeakHoldSelect');
const crossfadeSelect = document.getElementById('crossfadeSelect'); const crossfadeSelect = document.getElementById('crossfadeSelect');
const infoBtn = document.getElementById('infoBtn'); const infoBtn = document.getElementById('infoBtn');
const tagEditorOverlay = document.getElementById('tagEditorOverlay'); const tagEditorOverlay = document.getElementById('tagEditorOverlay');
@ -1685,6 +1700,7 @@ sortSelect.addEventListener('change', () => {
windowSelect.addEventListener('change', () => { windowType = windowSelect.value; }); windowSelect.addEventListener('change', () => { windowType = windowSelect.value; });
decaySelect.addEventListener('change', () => { peakDecay = parseFloat(decaySelect.value); }); decaySelect.addEventListener('change', () => { peakDecay = parseFloat(decaySelect.value); });
barDecaySelect.addEventListener('change', () => { barDecay = parseFloat(barDecaySelect.value); }); barDecaySelect.addEventListener('change', () => { barDecay = parseFloat(barDecaySelect.value); });
vuPeakHoldSelect.addEventListener('change', () => { vuPeakGrace = parseInt(vuPeakHoldSelect.value, 10); });
crossfadeSelect.addEventListener('change', () => { crossfadeDuration = parseFloat(crossfadeSelect.value); }); crossfadeSelect.addEventListener('change', () => { crossfadeDuration = parseFloat(crossfadeSelect.value); });
crossfadeDuration = parseFloat(crossfadeSelect.value); crossfadeDuration = parseFloat(crossfadeSelect.value);
repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; }); repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; });