From 5876701a645fcc160f16d1007e807119bf9978cc Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Sun, 7 Jun 2026 09:35:35 -0300 Subject: [PATCH] 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 --- plugin/contents/ui/cava-start.sh | 9 +++++++++ plugin/contents/ui/cava.conf | 14 ++++++++++++++ plugin/contents/ui/dancer.html | 32 +++++++++++++++----------------- plugin/contents/ui/main.qml | 23 +++++++++++++++++++++++ 4 files changed, 61 insertions(+), 17 deletions(-) create mode 100755 plugin/contents/ui/cava-start.sh create mode 100644 plugin/contents/ui/cava.conf diff --git a/plugin/contents/ui/cava-start.sh b/plugin/contents/ui/cava-start.sh new file mode 100755 index 0000000..521eed0 --- /dev/null +++ b/plugin/contents/ui/cava-start.sh @@ -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 diff --git a/plugin/contents/ui/cava.conf b/plugin/contents/ui/cava.conf new file mode 100644 index 0000000..d354af8 --- /dev/null +++ b/plugin/contents/ui/cava.conf @@ -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 diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index d2c31e5..71ca84e 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -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 (0–100 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(); diff --git a/plugin/contents/ui/main.qml b/plugin/contents/ui/main.qml index 7c6ba96..b25d0f2 100644 --- a/plugin/contents/ui/main.qml +++ b/plugin/contents/ui/main.qml @@ -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) + ")") + } + } + } }