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:
parent
b2df95ed10
commit
5876701a64
4 changed files with 61 additions and 17 deletions
9
plugin/contents/ui/cava-start.sh
Executable file
9
plugin/contents/ui/cava-start.sh
Executable 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
|
||||||
14
plugin/contents/ui/cava.conf
Normal file
14
plugin/contents/ui/cava.conf
Normal 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
|
||||||
|
|
@ -873,33 +873,30 @@ const VU_PALETTE = [
|
||||||
'#00eebb','#00ddaa','#00cc99','#00bb88',
|
'#00eebb','#00ddaa','#00cc99','#00bb88',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MAX_BAR_H = 70;
|
||||||
|
|
||||||
for (let i = 0; i < VU_COUNT; i++) {
|
for (let i = 0; i < VU_COUNT; i++) {
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'vu-bar';
|
bar.className = 'vu-bar';
|
||||||
bar.style.background = VU_PALETTE[i % VU_PALETTE.length];
|
bar.style.background = VU_PALETTE[i % VU_PALETTE.length];
|
||||||
bar.style.boxShadow = `0 0 4px ${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.style.height = '3px';
|
||||||
bar._phase = Math.random() * Math.PI * 2;
|
bar.style.opacity = '0.15';
|
||||||
bar._speed = 1.5 + Math.random() * 4;
|
|
||||||
vuMeter.appendChild(bar);
|
vuMeter.appendChild(bar);
|
||||||
vuBars.push(bar);
|
vuBars.push(bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateVU(ts) {
|
// Called by QML with semicolon-separated CAVA values (0–100 per bar)
|
||||||
const t = ts * 0.001;
|
function updateBars(raw) {
|
||||||
vuBars.forEach(bar => {
|
if (!isPlaying) return;
|
||||||
if (isPlaying) {
|
const vals = raw.split(';');
|
||||||
const h = bar._maxH * Math.abs(Math.sin(t * bar._speed + bar._phase));
|
vuBars.forEach((bar, i) => {
|
||||||
bar.style.height = Math.max(3, h) + 'px';
|
const v = Math.min(100, parseInt(vals[i] || 0, 10));
|
||||||
bar.style.opacity = 0.55 + 0.45 * (h / bar._maxH);
|
const h = Math.max(3, (v / 100) * MAX_BAR_H);
|
||||||
} else {
|
bar.style.height = h + 'px';
|
||||||
bar.style.height = '3px';
|
bar.style.opacity = 0.3 + 0.7 * (v / 100);
|
||||||
bar.style.opacity = '0.15';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
requestAnimationFrame(animateVU);
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(animateVU);
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
FLOATING MUSIC NOTES
|
FLOATING MUSIC NOTES
|
||||||
|
|
@ -945,6 +942,7 @@ function updateMusic(artist, title) {
|
||||||
isPlaying = playing;
|
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';
|
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;
|
if (el.textContent === text) return;
|
||||||
el.textContent = text;
|
el.textContent = text;
|
||||||
if (playing) pickSkeleton();
|
if (playing) pickSkeleton();
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Polls playerctl every 2s and injects music info into the page
|
||||||
Plasma5Support.DataSource {
|
Plasma5Support.DataSource {
|
||||||
id: musicSource
|
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) + ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue