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:
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user