media controls: visualizer

This commit is contained in:
end-4 2025-06-11 22:30:43 +02:00
parent 4473129599
commit a3818322a6
4 changed files with 114 additions and 7 deletions

View file

@ -26,6 +26,7 @@ Scope {
property real contentPadding: 13
property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
property real artRounding: Appearance.rounding.verysmall
property list<real> visualizerPoints: []
property bool hasPlasmaIntegration: false
function isRealPlayer(player) {
@ -68,13 +69,32 @@ Scope {
return filtered;
}
Process {
id: cavaProc
running: mediaControlsLoader.active
onRunningChanged: {
if (!cavaProc.running) {
root.visualizerPoints = [];
}
}
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.config)}/quickshell/scripts/cava/raw_output_config.txt`]
stdout: SplitParser {
onRead: data => {
// Parse `;`-separated values into the visualizerPoints array
let allPoints = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
let points = allPoints.slice(Math.floor(allPoints.length / 2), allPoints.length);
root.visualizerPoints = points;
}
}
}
Loader {
id: mediaControlsLoader
active: false
sourceComponent: PanelWindow {
id: mediaControlsRoot
visible: mediaControlsLoader.active
visible: true
exclusiveZone: 0
implicitWidth: (
@ -112,6 +132,7 @@ Scope {
delegate: PlayerControl {
required property MprisPlayer modelData
player: modelData
visualizerPoints: root.visualizerPoints
}
}
}

View file

@ -25,6 +25,8 @@ Item { // Player instance
property string artFilePath: `${artDownloadLocation}/${artFileName}`
property color artDominantColor: colorQuantizer?.colors[0] || Appearance.m3colors.m3secondaryContainer
property bool downloaded: false
property list<real> visualizerPoints: []
property real maxVisualizerValue: 1000 // Max value in the data points
implicitWidth: widgetWidth
implicitHeight: widgetHeight
@ -150,6 +152,69 @@ Item { // Player instance
}
}
Canvas { // Visualizer
id: visualizerCanvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
var points = playerController.visualizerPoints;
var maxVal = playerController.maxVisualizerValue || 1;
var h = height;
var w = width;
var n = points.length;
if (n < 2) return;
// Smoothing: simple moving average (optional)
var smoothPoints = [];
var smoothWindow = 3; // adjust for more/less smoothing
for (var i = 0; i < n; ++i) {
var sum = 0, count = 0;
for (var j = -smoothWindow; j <= smoothWindow; ++j) {
var idx = Math.max(0, Math.min(n - 1, i + j));
sum += points[idx];
count++;
}
smoothPoints.push(sum / count);
}
ctx.beginPath();
ctx.moveTo(0, h);
for (var i = 0; i < n; ++i) {
var x = i * w / (n - 1);
var y = h - (smoothPoints[i] / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fillStyle = Qt.rgba(
blendedColors.colPrimary.r,
blendedColors.colPrimary.g,
blendedColors.colPrimary.b,
0.25
);
ctx.fill();
}
Connections {
target: playerController
function onVisualizerPointsChanged() {
visualizerCanvas.requestPaint()
}
}
layer.enabled: true
layer.effect: MultiEffect { // Blur a tiny bit to obscure away the points
source: visualizerCanvas
saturation: 0.2
blurEnabled: true
blurMax: 6
blur: 1
}
}
RowLayout {
anchors.fill: parent
anchors.margins: root.contentPadding
@ -160,7 +225,7 @@ Item { // Player instance
Layout.fillHeight: true
implicitWidth: height
radius: root.artRounding
color: blendedColors.colLayer1
color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
layer.enabled: true
layer.effect: OpacityMask {
@ -235,12 +300,18 @@ Item { // Player instance
iconName: "skip_previous"
onClicked: playerController.player?.previous()
}
StyledProgressBar {
id: slider
Item {
id: progressBarContainer
Layout.fillWidth: true
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
value: playerController.player?.position / playerController.player?.length
implicitHeight: progressBar.implicitHeight
StyledProgressBar {
id: progressBar
anchors.fill: parent
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
value: playerController.player?.position / playerController.player?.length
}
}
TrackChangeButton {
iconName: "skip_next"

View file

@ -0,0 +1,14 @@
[general]
mode = waves
framerate = 60
autosens = 1
bars = 60
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
[smoothing]
noise_reduction = 20

View file

@ -5,6 +5,7 @@ pkgdesc='Illogical Impulse Audio Dependencies'
arch=(any)
license=(None)
depends=(
cava
pavucontrol-qt
wireplumber
libdbusmenu-gtk3