Add real-time CAVA audio visualizer for VU bars

CAVA runs in background via cava-start.sh, writing bar values to
/tmp/skeledance_bars. A new QML DataSource polls that file at 80ms
and calls updateBars() in JS. Falls back to rest state when not playing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ltadeu6 2026-06-07 09:35:35 -03:00
parent b2df95ed10
commit 5876701a64
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D
4 changed files with 61 additions and 17 deletions

View file

@ -873,33 +873,30 @@ const VU_PALETTE = [
'#00eebb','#00ddaa','#00cc99','#00bb88',
];
const MAX_BAR_H = 70;
for (let i = 0; i < VU_COUNT; i++) {
const bar = document.createElement('div');
bar.className = 'vu-bar';
bar.style.background = VU_PALETTE[i % VU_PALETTE.length];
bar.style.boxShadow = `0 0 4px ${VU_PALETTE[i % VU_PALETTE.length]}`;
bar._maxH = 15 + Math.random() * 55;
bar._phase = Math.random() * Math.PI * 2;
bar._speed = 1.5 + Math.random() * 4;
bar.style.height = '3px';
bar.style.opacity = '0.15';
vuMeter.appendChild(bar);
vuBars.push(bar);
}
function animateVU(ts) {
const t = ts * 0.001;
vuBars.forEach(bar => {
if (isPlaying) {
const h = bar._maxH * Math.abs(Math.sin(t * bar._speed + bar._phase));
bar.style.height = Math.max(3, h) + 'px';
bar.style.opacity = 0.55 + 0.45 * (h / bar._maxH);
} else {
bar.style.height = '3px';
bar.style.opacity = '0.15';
}
// Called by QML with semicolon-separated CAVA values (0100 per bar)
function updateBars(raw) {
if (!isPlaying) return;
const vals = raw.split(';');
vuBars.forEach((bar, i) => {
const v = Math.min(100, parseInt(vals[i] || 0, 10));
const h = Math.max(3, (v / 100) * MAX_BAR_H);
bar.style.height = h + 'px';
bar.style.opacity = 0.3 + 0.7 * (v / 100);
});
requestAnimationFrame(animateVU);
}
requestAnimationFrame(animateVU);
/* ============================================================
FLOATING MUSIC NOTES
@ -943,8 +940,9 @@ function updateMusic(artist, title) {
: (title || artist || '— —');
const playing = !!(artist || title);
isPlaying = playing;
document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0';
document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0';
document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0';
if (!playing) vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; });
if (el.textContent === text) return;
el.textContent = text;
if (playing) pickSkeleton();