1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

audio: optimize visualizations

This commit is contained in:
bbedward
2025-11-24 11:37:24 -05:00
parent f618df46d8
commit 7fa87125b5
4 changed files with 131 additions and 98 deletions

View File

@@ -26,6 +26,7 @@ Item {
readonly property real maxBarHeight: Theme.iconSize - 2 readonly property real maxBarHeight: Theme.iconSize - 2
readonly property real minBarHeight: 3 readonly property real minBarHeight: 3
readonly property real heightRange: maxBarHeight - minBarHeight readonly property real heightRange: maxBarHeight - minBarHeight
property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
Timer { Timer {
id: fallbackTimer 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 { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 1.5 spacing: 1.5
@@ -46,27 +75,17 @@ Item {
model: 6 model: 6
Rectangle { 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 width: 2
height: targetHeight height: root.barHeights[index]
radius: 1.5 radius: 1.5
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Behavior on height { Behavior on height {
enabled: root.isPlaying && !CavaService.cavaAvailable
NumberAnimation { NumberAnimation {
duration: Anims.durShort duration: 100
easing.type: Easing.BezierSpline easing.type: Easing.Linear
easing.bezierCurve: Anims.standardDecel
} }
} }
} }

View File

@@ -101,9 +101,24 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
AudioVisualization { Item {
width: 20
height: 20
anchors.horizontalCenter: parent.horizontalCenter 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 { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@@ -165,28 +180,38 @@ BasePill {
id: mediaInfo id: mediaInfo
spacing: Theme.spacingXS spacing: Theme.spacingXS
AudioVisualization { Item {
width: 20
height: 20
anchors.verticalCenter: parent.verticalCenter 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 { Rectangle {
id: textContainer 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: { property string displayText: {
if (!activePlayer || !activePlayer.trackTitle) { if (!activePlayer || !activePlayer.trackTitle) {
return ""; return "";
} }
let identity = activePlayer.identity || ""; const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari"); const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
let title = "";
let subtitle = "";
if (isWebMedia && activePlayer.trackTitle) {
title = activePlayer.trackTitle;
subtitle = activePlayer.trackArtist || identity;
} else {
title = activePlayer.trackTitle || "Unknown Track";
subtitle = activePlayer.trackArtist || "";
}
return subtitle.length > 0 ? title + " • " + subtitle : title; return subtitle.length > 0 ? title + " • " + subtitle : title;
} }

View File

@@ -1,5 +1,4 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
@@ -19,12 +18,12 @@ Singleton {
command: ["which", "cava"] command: ["which", "cava"]
running: false running: false
onExited: exitCode => { onExited: exitCode => {
root.cavaAvailable = exitCode === 0 root.cavaAvailable = exitCode === 0;
} }
} }
Component.onCompleted: { Component.onCompleted: {
cavaCheck.running = true cavaCheck.running = true;
} }
Process { Process {
@@ -35,21 +34,18 @@ Singleton {
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
root.values = Array(6).fill(0) root.values = Array(6).fill(0);
} }
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: data => {
if (root.refCount > 0 && data.trim()) { if (root.refCount > 0 && data.length > 0) {
let points = data.split(";").map(p => { const parts = data.split(";");
return parseInt(p.trim(), 10) if (parts.length >= 6) {
}).filter(p => { 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)];
return !isNaN(p) root.values = points;
})
if (points.length >= 6) {
root.values = points.slice(0, 6)
} }
} }
} }

View File

@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import QtQuick.Shapes import QtQuick.Shapes
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common import qs.Common
@@ -18,7 +17,7 @@ Item {
onArtUrlChanged: { onArtUrlChanged: {
if (artUrl && albumArt.status !== Image.Error) { if (artUrl && albumArt.status !== Image.Error) {
lastValidArtUrl = artUrl lastValidArtUrl = artUrl;
} }
} }
@@ -36,7 +35,7 @@ Item {
width: parent.width * 1.1 width: parent.width * 1.1
height: parent.height * 1.1 height: parent.height * 1.1
anchors.centerIn: parent anchors.centerIn: parent
visible: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
asynchronous: false asynchronous: false
antialiasing: true antialiasing: true
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
@@ -50,19 +49,21 @@ Item {
property var audioLevels: { property var audioLevels: {
if (!CavaService.cavaAvailable || CavaService.values.length === 0) { 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 smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
property var cubics: [] property var cubics: []
onAudioLevelsChanged: updatePath() Connections {
target: CavaService
FrameAnimation { function onValuesChanged() {
running: morphingBlob.visible if (morphingBlob.visible) {
onTriggered: morphingBlob.updatePath() morphingBlob.updatePath();
}
}
} }
Component { Component {
@@ -71,69 +72,61 @@ Item {
} }
Component.onCompleted: { Component.onCompleted: {
shapePath.pathElements.push(Qt.createQmlObject( shapePath.pathElements.push(Qt.createQmlObject('import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath));
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
))
for (let i = 0; i < segments; i++) { for (let i = 0; i < segments; i++) {
const seg = cubicSegment.createObject(shapePath) const seg = cubicSegment.createObject(shapePath);
shapePath.pathElements.push(seg) shapePath.pathElements.push(seg);
cubics.push(seg) cubics.push(seg);
} }
updatePath() updatePath();
}
function expSmooth(prev, next, alpha) {
return prev + alpha * (next - prev)
} }
function 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++) { const alpha = 0.35;
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 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++) { for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI const angle = i * angleStep;
const audioIndex = i % Math.min(smoothedLevels.length, 10) const audioIndex = i % 10;
const rawLevel = smoothedLevels[audioIndex] || 0;
const rawLevel = smoothedLevels[audioIndex] || 0 const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100 const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
const normalizedLevel = scaledLevel / 100 const radius = baseRadius * (1.0 + audioLevel);
const audioLevel = Math.max(0.15, normalizedLevel) * 0.5 points[i] = {
x: centerX + Math.cos(angle) * radius,
const radius = baseRadius * (1.0 + audioLevel) y: centerY + Math.sin(angle) * radius
const x = centerX + Math.cos(angle) * radius };
const y = centerY + Math.sin(angle) * radius
points.push({x: x, y: y})
} }
const startMove = shapePath.pathElements[0] startMove.x = points[0].x;
startMove.x = points[0].x startMove.y = points[0].y;
startMove.y = points[0].y
const tension = 0.5
for (let i = 0; i < segments; i++) { for (let i = 0; i < segments; i++) {
const p0 = points[(i - 1 + segments) % segments] const p0 = points[(i + segments - 1) % segments];
const p1 = points[i] const p1 = points[i];
const p2 = points[(i + 1) % segments] const p2 = points[(i + 1) % segments];
const p3 = points[(i + 2) % segments] const p3 = points[(i + 2) % segments];
const c1x = p1.x + (p2.x - p0.x) * tension / 3 const seg = cubics[i];
const c1y = p1.y + (p2.y - p0.y) * tension / 3 seg.control1X = p1.x + (p2.x - p0.x) * tension3;
const c2x = p2.x - (p3.x - p1.x) * tension / 3 seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
const c2y = p2.y - (p3.y - p1.y) * tension / 3 seg.control2X = p2.x - (p3.x - p1.x) * tension3;
seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
const seg = cubics[i] seg.x = p2.x;
seg.control1X = c1x seg.y = p2.y;
seg.control1Y = c1y
seg.control2X = c2x
seg.control2Y = c2y
seg.x = p2.x
seg.y = p2.y
} }
} }
@@ -161,8 +154,8 @@ Item {
onImageSourceChanged: { onImageSourceChanged: {
if (imageSource && imageStatus !== Image.Error) { if (imageSource && imageStatus !== Image.Error) {
lastValidArtUrl = imageSource lastValidArtUrl = imageSource;
} }
} }
} }
} }