ui(vu): narrower 44deg swing, peak-based level, faster response; mini progress bar fix

- VU needle swings -22..+22deg instead of -45..+45 for a more realistic VU look
- Switch from RMS to peak frequency reading so the needle catches musical hits
- Faster attack (0.7) and release (0.25) so it swings rather than pinning
- Replace explicit grid lines with subtle repeating-conic-gradient ticks
- Scope mini progress bar styles to .mini-player; taller (3px), clickable
This commit is contained in:
2026-04-25 11:41:32 +03:00
parent 588a303c44
commit f2c82164e8
2 changed files with 26 additions and 51 deletions
+13 -19
View File
@@ -763,10 +763,10 @@ export function updateUI(status) {
dom.volumeDisplay.textContent = `${status.volume}%`;
dom.miniVolumeSlider.value = status.volume;
dom.miniVolumeDisplay.textContent = `${status.volume}%`;
// VU needle: map 0-100 volume to -45deg..+45deg rotation.
// VU needle: map 0-100 volume to -22deg..+22deg rotation.
const needle = document.getElementById('vuNeedle');
if (needle) {
const deg = -45 + (status.volume / 100) * 90;
const deg = -22 + (status.volume / 100) * 44;
needle.style.transform = `rotate(${deg}deg)`;
}
// Editorial VU readout: VOL XX% / OUT (SYS or MUTED)
@@ -802,22 +802,19 @@ export function updateUI(status) {
// slider position so the needle still looks alive.
let vuWobbleHandle = null;
let vuWobbleStart = 0;
let vuLevelSmoothed = 0; // Smoothed RMS of recent frequency frames
const VU_LEVEL_ATTACK = 0.55; // How fast needle climbs to a peak
const VU_LEVEL_RELEASE = 0.12; // How fast it falls back
let vuLevelSmoothed = 0;
const VU_LEVEL_ATTACK = 0.7; // Fast climb so the needle catches musical hits
const VU_LEVEL_RELEASE = 0.25; // Faster fall so it swings between hits, not pins
function readAudioLevel() {
// frequencyData is the WS-driven FFT payload from player.js scope.
if (!frequencyData || !frequencyData.frequencies) return null;
const bins = frequencyData.frequencies;
if (!bins.length) return null;
let sumSq = 0;
// Skip the very lowest bin (DC + sub-rumble) for cleaner level.
for (let i = 1; i < bins.length; i++) sumSq += bins[i] * bins[i];
const rms = Math.sqrt(sumSq / (bins.length - 1));
// The values are in 0..1 from the backend; gain a touch so quieter
// tracks still swing the needle.
return Math.min(1, rms * 1.6);
let peak = 0;
for (let i = 1; i < bins.length; i++) {
if (bins[i] > peak) peak = bins[i];
}
return Math.min(1, peak * 1.4);
}
function startVuWobble() {
@@ -839,11 +836,9 @@ function startVuWobble() {
const wanted = audioLevel * (vol / 100);
const k = wanted > vuLevelSmoothed ? VU_LEVEL_ATTACK : VU_LEVEL_RELEASE;
vuLevelSmoothed = vuLevelSmoothed * (1 - k) + wanted * k;
// Map 0..1 to -45deg..+45deg.
target = -45 + vuLevelSmoothed * 90;
target = -22 + vuLevelSmoothed * 44;
} else {
// Synthetic fallback: volume-mapped + sine wobble.
const base = -45 + (vol / 100) * 90;
const base = -22 + (vol / 100) * 44;
const mag = Math.max(2, Math.min(14, vol * 0.16));
const t = (performance.now() - vuWobbleStart) / 1000;
target = base
@@ -864,9 +859,8 @@ function stopVuWobble() {
vuWobbleHandle = null;
}
vuLevelSmoothed = 0;
// Settle needle back to the bottom of the swing.
const needle = document.getElementById('vuNeedle');
if (needle) needle.style.transform = 'rotate(-45deg)';
if (needle) needle.style.transform = 'rotate(-22deg)';
}
export function updatePlaybackState(state) {