fix(player): real audio level on VU; full-width spectrum; hide canvas under vinyl
VU needle reflects actual audio output - Was just a synthetic wobble bounded by the volume slider value. Now reads RMS of the FFT bins (skipping bin 0 / DC) the visualizer feeds in, multiplies by current volume, and applies attack/release smoothing for analog-feeling ballistics. - Falls back to the synthetic wobble when audio capture isn't running so the needle still looks alive on the static fallback. - When playback stops, needle settles to the bottom of the swing (-45deg) instead of holding the volume position. Spectrum width — actually fixed this time - Root cause: CSS repeat() does NOT accept a CSS variable for its count argument, so my `repeat(var(--spectrum-bars), 1fr)` rule was invalid and silently dropped, leaving the legacy/auto sizing behavior. Set grid-template-columns directly from JS to `repeat(40, minmax(0, 1fr))`. - CSS retains a `repeat(40, minmax(0, 1fr))` literal as a default so the row renders sane even before JS executes. Spectrogram canvas under vinyl - Hidden via display: none — the editorial .spectrum row already shows the audio spectrum; the canvas was redundant and ugly. Element stays in DOM so the visualizer JS keeps rendering (drives album-art bass-pulse + dynamic background bands).
This commit is contained in:
@@ -795,13 +795,30 @@ export function updateUI(status) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── VU needle synthetic wobble ──────────────────────────────
|
||||
// Real audio-level analysis is only available when the visualizer
|
||||
// is enabled. For the common case (visualizer off), drive the needle
|
||||
// with a low-frequency pseudo-random walk that's bounded by current
|
||||
// volume, so it looks alive without being noisy.
|
||||
// ─── VU needle ───────────────────────────────────────────────
|
||||
// The needle reflects ACTUAL audio output level (computed from the
|
||||
// FFT data the visualizer feeds in). When audio capture isn't
|
||||
// running, fall back to a synthetic wobble bounded by the volume
|
||||
// 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
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function startVuWobble() {
|
||||
if (vuWobbleHandle) return;
|
||||
@@ -811,16 +828,30 @@ function startVuWobble() {
|
||||
if (needle) {
|
||||
const slider = document.getElementById('volume-slider');
|
||||
const vol = slider ? Number(slider.value) || 0 : 0;
|
||||
const base = -45 + (vol / 100) * 90;
|
||||
// Wobble magnitude scales with volume, capped at ~12deg either way.
|
||||
const mag = Math.max(2, Math.min(14, vol * 0.16));
|
||||
const t = (performance.now() - vuWobbleStart) / 1000;
|
||||
// Two combined sines + a tiny random component for organic motion.
|
||||
const wobble =
|
||||
Math.sin(t * 6.3) * mag * 0.55 +
|
||||
Math.sin(t * 11.7 + 1.3) * mag * 0.30 +
|
||||
(Math.random() - 0.5) * mag * 0.30;
|
||||
needle.style.transform = `rotate(${base + wobble}deg)`;
|
||||
// Volume slider modulates how loud the audio could be;
|
||||
// multiply against the captured level so muting drops the
|
||||
// needle even though the source is still playing.
|
||||
const audioLevel = readAudioLevel();
|
||||
let target;
|
||||
if (audioLevel != null) {
|
||||
// Real audio: scale by output volume & apply attack/release
|
||||
// smoothing for analog-feeling ballistics.
|
||||
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;
|
||||
} else {
|
||||
// Synthetic fallback: volume-mapped + sine wobble.
|
||||
const base = -45 + (vol / 100) * 90;
|
||||
const mag = Math.max(2, Math.min(14, vol * 0.16));
|
||||
const t = (performance.now() - vuWobbleStart) / 1000;
|
||||
target = base
|
||||
+ Math.sin(t * 6.3) * mag * 0.55
|
||||
+ Math.sin(t * 11.7 + 1.3) * mag * 0.30
|
||||
+ (Math.random() - 0.5) * mag * 0.30;
|
||||
}
|
||||
needle.style.transform = `rotate(${target}deg)`;
|
||||
}
|
||||
vuWobbleHandle = requestAnimationFrame(tick);
|
||||
};
|
||||
@@ -832,14 +863,10 @@ function stopVuWobble() {
|
||||
cancelAnimationFrame(vuWobbleHandle);
|
||||
vuWobbleHandle = null;
|
||||
}
|
||||
// Settle needle back to the static volume-mapped position.
|
||||
vuLevelSmoothed = 0;
|
||||
// Settle needle back to the bottom of the swing.
|
||||
const needle = document.getElementById('vuNeedle');
|
||||
const slider = document.getElementById('volume-slider');
|
||||
if (needle && slider) {
|
||||
const vol = Number(slider.value) || 0;
|
||||
const base = -45 + (vol / 100) * 90;
|
||||
needle.style.transform = `rotate(${base}deg)`;
|
||||
}
|
||||
if (needle) needle.style.transform = 'rotate(-45deg)';
|
||||
}
|
||||
|
||||
export function updatePlaybackState(state) {
|
||||
|
||||
Reference in New Issue
Block a user