Add configurable spectrum analyzer peak hold, fade, and snap behavior

This commit is contained in:
Armin 2026-07-08 19:05:12 +02:00
commit dd657c6ab3

View file

@ -228,6 +228,23 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
<option value="60">1s</option> <option value="60">1s</option>
<option value="120">2s</option> <option value="120">2s</option>
</select> </select>
<span style="font-size:11px;color:#6b7089;margin-left:8px">Spec Peak Hold:</span>
<select id="peakHoldSelect">
<option value="0">Off</option>
<option value="60">1s</option>
<option value="120" selected>2s</option>
<option value="180">3s</option>
<option value="240">4s</option>
</select>
<span style="font-size:11px;color:#6b7089;margin-left:8px">Peak Fade:</span>
<select id="peakFadeSelect">
<option value="0" selected>0ms</option>
<option value="12">200ms</option>
<option value="24">400ms</option>
<option value="36">600ms</option>
<option value="48">800ms</option>
<option value="60">1s</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>
@ -361,11 +378,15 @@ const canvas = document.getElementById('spectrum');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const bigTime = document.getElementById('bigTime'); const bigTime = document.getElementById('bigTime');
let peakData = []; let peakData = [];
let peakHoldCounters = [];
let peakFadeCounters = [];
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 vuPeakHoldL = 0, vuPeakHoldR = 0;
let vuPeakGrace = 0; let vuPeakGrace = 0;
let peakHoldGrace = 0;
let peakFadeGrace = 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++) {
@ -495,6 +516,8 @@ function drawSpectrum() {
} }
if (peakData.length !== NUM_BARS) { if (peakData.length !== NUM_BARS) {
peakData = new Float32Array(NUM_BARS); peakData = new Float32Array(NUM_BARS);
peakHoldCounters = new Uint32Array(NUM_BARS);
peakFadeCounters = new Uint32Array(NUM_BARS);
smoothedData = new Float32Array(NUM_BARS); smoothedData = new Float32Array(NUM_BARS);
} }
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@ -532,15 +555,23 @@ function drawSpectrum() {
ctx.fillRect(x, h - barH, barW, barH); ctx.fillRect(x, h - barH, barW, barH);
if (barH > peakData[i]) { if (barH > peakData[i]) {
peakData[i] = barH; peakData[i] = barH;
peakHoldCounters[i] = 0;
peakFadeCounters[i] = 0;
} else if (peakHoldCounters[i] < peakHoldGrace) {
peakHoldCounters[i]++;
} else if (peakFadeCounters[i] < peakFadeGrace) {
const remaining = peakFadeGrace - peakFadeCounters[i];
peakData[i] *= (remaining - 1) / remaining;
peakFadeCounters[i]++;
} else { } else {
peakData[i] *= peakDecay; peakData[i] = 0;
} }
} }
ctx.fillStyle = '#e27878'; ctx.fillStyle = '#e27878';
for (let i = 0; i < NUM_BARS; i++) { for (let i = 0; i < NUM_BARS; i++) {
const x = offset + i * totalBar; const x = offset + i * totalBar;
if (peakData[i] > 1) { if (peakData[i] > 1) {
ctx.fillRect(x, h - Math.round(peakData[i]), barW, 4); ctx.fillRect(x, Math.min(h - 4, h - Math.round(peakData[i])), barW, 4);
} }
} }
const vuCanvas = document.getElementById('vuMeter'); const vuCanvas = document.getElementById('vuMeter');
@ -743,6 +774,8 @@ 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 vuPeakHoldSelect = document.getElementById('vuPeakHoldSelect');
const peakHoldSelect = document.getElementById('peakHoldSelect');
const peakFadeSelect = document.getElementById('peakFadeSelect');
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');
@ -1706,7 +1739,12 @@ 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); }); vuPeakHoldSelect.addEventListener('change', () => { vuPeakGrace = parseInt(vuPeakHoldSelect.value, 10); });
peakHoldSelect.addEventListener('change', () => { peakHoldGrace = parseInt(peakHoldSelect.value, 10); });
peakFadeSelect.addEventListener('change', () => { peakFadeGrace = parseInt(peakFadeSelect.value, 10); });
crossfadeSelect.addEventListener('change', () => { crossfadeDuration = parseFloat(crossfadeSelect.value); }); crossfadeSelect.addEventListener('change', () => { crossfadeDuration = parseFloat(crossfadeSelect.value); });
vuPeakGrace = parseInt(vuPeakHoldSelect.value, 10);
peakHoldGrace = parseInt(peakHoldSelect.value, 10);
peakFadeGrace = parseInt(peakFadeSelect.value, 10);
crossfadeDuration = parseFloat(crossfadeSelect.value); crossfadeDuration = parseFloat(crossfadeSelect.value);
repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; }); repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; });
shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value === 'on'; }); shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value === 'on'; });