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

@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Kill any previous instance tied to this config
pkill -f "cava -p.*skeledance" 2>/dev/null
sleep 0.2
CONF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cava.conf"
cava -p "$CONF" | while IFS= read -r line; do
printf '%s\n' "$line" > /tmp/skeledance_bars
done

View file

@ -0,0 +1,14 @@
[general]
bars = 38
framerate = 60
sleep_timer = 0
[input]
method = pulse
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
ascii_max_range = 100
bar_delimiter = 59

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();

View file

@ -24,6 +24,14 @@ WallpaperItem {
}
}
// Start CAVA on load
Plasma5Support.DataSource {
id: cavaStarter
engine: "executable"
connectedSources: ["bash " + Qt.resolvedUrl("cava-start.sh").toString().replace("file://", "") + " &"]
interval: 0
}
// Polls playerctl every 2s and injects music info into the page
Plasma5Support.DataSource {
id: musicSource
@ -42,4 +50,19 @@ WallpaperItem {
)
}
}
// Polls CAVA bars at ~60fps
Plasma5Support.DataSource {
id: cavaSource
engine: "executable"
connectedSources: ["cat /tmp/skeledance_bars 2>/dev/null || echo ''"]
interval: 80
onNewData: function(sourceName, data) {
var raw = data["stdout"].trim()
if (raw !== "") {
webView.runJavaScript("updateBars(" + JSON.stringify(raw) + ")")
}
}
}
}