From 7fa87125b59305fad00b3545c52cdb3c8c93983c Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 24 Nov 2025 11:37:24 -0500 Subject: [PATCH] audio: optimize visualizations --- .../DankBar/Widgets/AudioVisualization.qml | 47 +++++--- quickshell/Modules/DankBar/Widgets/Media.qml | 51 ++++++-- quickshell/Services/CavaService.qml | 20 ++-- quickshell/Widgets/DankAlbumArt.qml | 111 ++++++++---------- 4 files changed, 131 insertions(+), 98 deletions(-) diff --git a/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml b/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml index 2f2d0b33..52ace330 100644 --- a/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml +++ b/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml @@ -26,6 +26,7 @@ Item { readonly property real maxBarHeight: Theme.iconSize - 2 readonly property real minBarHeight: 3 readonly property real heightRange: maxBarHeight - minBarHeight + property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight] Timer { id: fallbackTimer @@ -38,6 +39,34 @@ Item { } } + Connections { + target: CavaService + function onValuesChanged() { + if (!root.isPlaying) { + root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight]; + return; + } + + const newHeights = []; + for (let i = 0; i < 6; i++) { + if (CavaService.values.length <= i) { + newHeights.push(root.minBarHeight); + continue; + } + + const rawLevel = CavaService.values[i]; + if (rawLevel <= 0) { + newHeights.push(root.minBarHeight); + } else if (rawLevel >= 100) { + newHeights.push(root.maxBarHeight); + } else { + newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange); + } + } + root.barHeights = newHeights; + } + } + Row { anchors.centerIn: parent spacing: 1.5 @@ -46,27 +75,17 @@ Item { model: 6 Rectangle { - readonly property real targetHeight: { - if (!root.isPlaying || CavaService.values.length <= index) - return root.minBarHeight; - - const rawLevel = CavaService.values[index]; - const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel); - const scaledLevel = Math.sqrt(clampedLevel * 0.01); - return root.minBarHeight + scaledLevel * root.heightRange; - } - width: 2 - height: targetHeight + height: root.barHeights[index] radius: 1.5 color: Theme.primary anchors.verticalCenter: parent.verticalCenter Behavior on height { + enabled: root.isPlaying && !CavaService.cavaAvailable NumberAnimation { - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standardDecel + duration: 100 + easing.type: Easing.Linear } } } diff --git a/quickshell/Modules/DankBar/Widgets/Media.qml b/quickshell/Modules/DankBar/Widgets/Media.qml index 617cc240..3e26d739 100644 --- a/quickshell/Modules/DankBar/Widgets/Media.qml +++ b/quickshell/Modules/DankBar/Widgets/Media.qml @@ -101,9 +101,24 @@ BasePill { anchors.centerIn: parent spacing: Theme.spacingXS - AudioVisualization { + Item { + width: 20 + height: 20 anchors.horizontalCenter: parent.horizontalCenter + AudioVisualization { + anchors.fill: parent + visible: CavaService.cavaAvailable + } + + DankIcon { + anchors.fill: parent + name: "music_note" + size: 20 + color: Theme.primary + visible: !CavaService.cavaAvailable + } + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor @@ -165,28 +180,38 @@ BasePill { id: mediaInfo spacing: Theme.spacingXS - AudioVisualization { + Item { + width: 20 + height: 20 anchors.verticalCenter: parent.verticalCenter + + AudioVisualization { + anchors.fill: parent + visible: CavaService.cavaAvailable + } + + DankIcon { + anchors.fill: parent + name: "music_note" + size: 20 + color: Theme.primary + visible: !CavaService.cavaAvailable + } } Rectangle { id: textContainer + readonly property string cachedIdentity: activePlayer ? (activePlayer.identity || "") : "" + readonly property string lowerIdentity: cachedIdentity.toLowerCase() + readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari") + property string displayText: { if (!activePlayer || !activePlayer.trackTitle) { return ""; } - let identity = activePlayer.identity || ""; - let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari"); - let title = ""; - let subtitle = ""; - if (isWebMedia && activePlayer.trackTitle) { - title = activePlayer.trackTitle; - subtitle = activePlayer.trackArtist || identity; - } else { - title = activePlayer.trackTitle || "Unknown Track"; - subtitle = activePlayer.trackArtist || ""; - } + const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track"); + const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || ""); return subtitle.length > 0 ? title + " • " + subtitle : title; } diff --git a/quickshell/Services/CavaService.qml b/quickshell/Services/CavaService.qml index e2cf1615..e9815aa5 100644 --- a/quickshell/Services/CavaService.qml +++ b/quickshell/Services/CavaService.qml @@ -1,5 +1,4 @@ pragma Singleton - pragma ComponentBehavior: Bound import QtQuick @@ -19,12 +18,12 @@ Singleton { command: ["which", "cava"] running: false onExited: exitCode => { - root.cavaAvailable = exitCode === 0 + root.cavaAvailable = exitCode === 0; } } Component.onCompleted: { - cavaCheck.running = true + cavaCheck.running = true; } Process { @@ -35,21 +34,18 @@ Singleton { onRunningChanged: { if (!running) { - root.values = Array(6).fill(0) + root.values = Array(6).fill(0); } } stdout: SplitParser { splitMarker: "\n" onRead: data => { - if (root.refCount > 0 && data.trim()) { - let points = data.split(";").map(p => { - return parseInt(p.trim(), 10) - }).filter(p => { - return !isNaN(p) - }) - if (points.length >= 6) { - root.values = points.slice(0, 6) + if (root.refCount > 0 && data.length > 0) { + const parts = data.split(";"); + if (parts.length >= 6) { + const points = [parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10), parseInt(parts[3], 10), parseInt(parts[4], 10), parseInt(parts[5], 10)]; + root.values = points; } } } diff --git a/quickshell/Widgets/DankAlbumArt.qml b/quickshell/Widgets/DankAlbumArt.qml index 9a29caa2..d5f8d109 100644 --- a/quickshell/Widgets/DankAlbumArt.qml +++ b/quickshell/Widgets/DankAlbumArt.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import QtQuick.Shapes import Quickshell.Services.Mpris import qs.Common @@ -18,7 +17,7 @@ Item { onArtUrlChanged: { if (artUrl && albumArt.status !== Image.Error) { - lastValidArtUrl = artUrl + lastValidArtUrl = artUrl; } } @@ -36,7 +35,7 @@ Item { width: parent.width * 1.1 height: parent.height * 1.1 anchors.centerIn: parent - visible: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation + visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation asynchronous: false antialiasing: true preferredRendererType: Shape.CurveRenderer @@ -50,19 +49,21 @@ Item { property var audioLevels: { if (!CavaService.cavaAvailable || CavaService.values.length === 0) { - return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6] + return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]; } - return CavaService.values + return CavaService.values; } property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6] property var cubics: [] - onAudioLevelsChanged: updatePath() - - FrameAnimation { - running: morphingBlob.visible - onTriggered: morphingBlob.updatePath() + Connections { + target: CavaService + function onValuesChanged() { + if (morphingBlob.visible) { + morphingBlob.updatePath(); + } + } } Component { @@ -71,69 +72,61 @@ Item { } Component.onCompleted: { - shapePath.pathElements.push(Qt.createQmlObject( - 'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath - )) + shapePath.pathElements.push(Qt.createQmlObject('import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath)); for (let i = 0; i < segments; i++) { - const seg = cubicSegment.createObject(shapePath) - shapePath.pathElements.push(seg) - cubics.push(seg) + const seg = cubicSegment.createObject(shapePath); + shapePath.pathElements.push(seg); + cubics.push(seg); } - updatePath() - } - - function expSmooth(prev, next, alpha) { - return prev + alpha * (next - prev) + updatePath(); } function updatePath() { - if (cubics.length === 0) return + if (cubics.length === 0) + return; - for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) { - smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.35) + const alpha = 0.35; + const minLen = Math.min(smoothedLevels.length, audioLevels.length); + for (let i = 0; i < minLen; i++) { + smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]); } - const points = [] + const angleStep = 2 * Math.PI / segments; + const tension3 = 0.16666667; + const startMove = shapePath.pathElements[0]; + + const points = new Array(segments); for (let i = 0; i < segments; i++) { - const angle = (i / segments) * 2 * Math.PI - const audioIndex = i % Math.min(smoothedLevels.length, 10) - - const rawLevel = smoothedLevels[audioIndex] || 0 - const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100 - const normalizedLevel = scaledLevel / 100 - const audioLevel = Math.max(0.15, normalizedLevel) * 0.5 - - const radius = baseRadius * (1.0 + audioLevel) - const x = centerX + Math.cos(angle) * radius - const y = centerY + Math.sin(angle) * radius - points.push({x: x, y: y}) + const angle = i * angleStep; + const audioIndex = i % 10; + const rawLevel = smoothedLevels[audioIndex] || 0; + const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel); + const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5; + const radius = baseRadius * (1.0 + audioLevel); + points[i] = { + x: centerX + Math.cos(angle) * radius, + y: centerY + Math.sin(angle) * radius + }; } - const startMove = shapePath.pathElements[0] - startMove.x = points[0].x - startMove.y = points[0].y + startMove.x = points[0].x; + startMove.y = points[0].y; - const tension = 0.5 for (let i = 0; i < segments; i++) { - const p0 = points[(i - 1 + segments) % segments] - const p1 = points[i] - const p2 = points[(i + 1) % segments] - const p3 = points[(i + 2) % segments] + const p0 = points[(i + segments - 1) % segments]; + const p1 = points[i]; + const p2 = points[(i + 1) % segments]; + const p3 = points[(i + 2) % segments]; - const c1x = p1.x + (p2.x - p0.x) * tension / 3 - const c1y = p1.y + (p2.y - p0.y) * tension / 3 - const c2x = p2.x - (p3.x - p1.x) * tension / 3 - const c2y = p2.y - (p3.y - p1.y) * tension / 3 - - const seg = cubics[i] - seg.control1X = c1x - seg.control1Y = c1y - seg.control2X = c2x - seg.control2Y = c2y - seg.x = p2.x - seg.y = p2.y + const seg = cubics[i]; + seg.control1X = p1.x + (p2.x - p0.x) * tension3; + seg.control1Y = p1.y + (p2.y - p0.y) * tension3; + seg.control2X = p2.x - (p3.x - p1.x) * tension3; + seg.control2Y = p2.y - (p3.y - p1.y) * tension3; + seg.x = p2.x; + seg.y = p2.y; } } @@ -161,8 +154,8 @@ Item { onImageSourceChanged: { if (imageSource && imageStatus !== Image.Error) { - lastValidArtUrl = imageSource + lastValidArtUrl = imageSource; } } } -} \ No newline at end of file +}