diff --git a/.config/quickshell/modules/mediaControls/MediaControls.qml b/.config/quickshell/modules/mediaControls/MediaControls.qml index 81b1d62d..0a85bde0 100644 --- a/.config/quickshell/modules/mediaControls/MediaControls.qml +++ b/.config/quickshell/modules/mediaControls/MediaControls.qml @@ -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 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 } } } diff --git a/.config/quickshell/modules/mediaControls/PlayerControl.qml b/.config/quickshell/modules/mediaControls/PlayerControl.qml index 16be41b8..842b681f 100644 --- a/.config/quickshell/modules/mediaControls/PlayerControl.qml +++ b/.config/quickshell/modules/mediaControls/PlayerControl.qml @@ -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 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" diff --git a/.config/quickshell/scripts/cava/raw_output_config.txt b/.config/quickshell/scripts/cava/raw_output_config.txt new file mode 100644 index 00000000..a223a31c --- /dev/null +++ b/.config/quickshell/scripts/cava/raw_output_config.txt @@ -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 + diff --git a/arch-packages/illogical-impulse-audio/PKGBUILD b/arch-packages/illogical-impulse-audio/PKGBUILD index 81054e0b..6cab7de5 100644 --- a/arch-packages/illogical-impulse-audio/PKGBUILD +++ b/arch-packages/illogical-impulse-audio/PKGBUILD @@ -5,6 +5,7 @@ pkgdesc='Illogical Impulse Audio Dependencies' arch=(any) license=(None) depends=( + cava pavucontrol-qt wireplumber libdbusmenu-gtk3