earbang protection

This commit is contained in:
end-4
2025-06-07 19:43:05 +02:00
parent c1a4670de2
commit 57327ab0fe
4 changed files with 114 additions and 20 deletions
@@ -12,6 +12,14 @@ Singleton {
property int fakeScreenRounding: 1 // 0: None | 1: Always | 2: When not fullscreen property int fakeScreenRounding: 1 // 0: None | 1: Always | 2: When not fullscreen
} }
property QtObject audio: QtObject { // Values in %
property QtObject protection: QtObject { // Prevent sudden bangs
property bool enable: true
property real maxAllowedIncrease: 10
property real maxAllowed: 90 // Realistically should already provide some protection when it's 99...
}
}
property QtObject apps: QtObject { property QtObject apps: QtObject {
property string bluetooth: "better-control --bluetooth" property string bluetooth: "better-control --bluetooth"
property string imageViewer: "loupe" property string imageViewer: "loupe"
@@ -73,7 +73,7 @@ Scope {
item: osdValuesWrapper item: osdValuesWrapper
} }
implicitWidth: Appearance.sizes.osdWidth implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight implicitHeight: columnLayout.implicitHeight
visible: osdLoader.active visible: osdLoader.active
@@ -12,6 +12,7 @@ import Quickshell.Hyprland
Scope { Scope {
id: root id: root
property bool showOsdValues: false property bool showOsdValues: false
property string protectionMessage: ""
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
function triggerOsd() { function triggerOsd() {
@@ -25,7 +26,8 @@ Scope {
repeat: false repeat: false
running: false running: false
onTriggered: { onTriggered: {
showOsdValues = false root.showOsdValues = false
root.protectionMessage = ""
} }
} }
@@ -36,7 +38,7 @@ Scope {
} }
} }
Connections { Connections { // Listen to volume changes
target: Audio.sink?.audio ?? null target: Audio.sink?.audio ?? null
function onVolumeChanged() { function onVolumeChanged() {
if (!Audio.ready) return if (!Audio.ready) return
@@ -48,6 +50,14 @@ Scope {
} }
} }
Connections { // Listen to protection triggers
target: Audio
function onSinkProtectionTriggered(reason) {
root.protectionMessage = reason;
root.triggerOsd()
}
}
Loader { Loader {
id: osdLoader id: osdLoader
active: showOsdValues active: showOsdValues
@@ -75,7 +85,7 @@ Scope {
item: osdValuesWrapper item: osdValuesWrapper
} }
implicitWidth: Appearance.sizes.osdWidth implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight implicitHeight: columnLayout.implicitHeight
visible: osdLoader.active visible: osdLoader.active
@@ -85,8 +95,8 @@ Scope {
Item { Item {
id: osdValuesWrapper id: osdValuesWrapper
// Extra space for shadow // Extra space for shadow
implicitHeight: osdValues.implicitHeight + Appearance.sizes.elevationMargin * 2 implicitHeight: contentColumnLayout.implicitHeight + Appearance.sizes.elevationMargin * 2
implicitWidth: osdValues.implicitWidth implicitWidth: contentColumnLayout.implicitWidth
clip: true clip: true
MouseArea { MouseArea {
@@ -95,20 +105,63 @@ Scope {
onEntered: root.showOsdValues = false onEntered: root.showOsdValues = false
} }
Behavior on implicitHeight { ColumnLayout {
NumberAnimation { id: contentColumnLayout
duration: Appearance.animation.menuDecel.duration anchors {
easing.type: Appearance.animation.menuDecel.type top: parent.top
left: parent.left
right: parent.right
leftMargin: Appearance.sizes.elevationMargin
rightMargin: Appearance.sizes.elevationMargin
} }
} spacing: 0
OsdValueIndicator { OsdValueIndicator {
id: osdValues id: osdValues
anchors.fill: parent Layout.fillWidth: true
anchors.margins: Appearance.sizes.elevationMargin value: Audio.sink?.audio.volume ?? 0
value: Audio.sink?.audio.volume ?? 0 icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up" name: qsTr("Volume")
name: qsTr("Volume") }
Item {
id: protectionMessageWrapper
implicitHeight: protectionMessageBackground.implicitHeight
implicitWidth: protectionMessageBackground.implicitWidth
Layout.alignment: Qt.AlignHCenter
opacity: root.protectionMessage !== "" ? 1 : 0
StyledRectangularShadow {
target: protectionMessageBackground
}
Rectangle {
id: protectionMessageBackground
anchors.centerIn: parent
color: Appearance.m3colors.m3error
property real padding: 10
implicitHeight: protectionMessageRowLayout.implicitHeight + padding * 2
implicitWidth: protectionMessageRowLayout.implicitWidth + padding * 2
radius: Appearance.rounding.normal
RowLayout {
id: protectionMessageRowLayout
anchors.centerIn: parent
MaterialSymbol {
id: protectionMessageIcon
text: "dangerous"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onError
}
StyledText {
id: protectionMessageTextWidget
horizontalAlignment: Text.AlignHCenter
color: Appearance.m3colors.m3onError
wrapMode: Text.Wrap
text: root.protectionMessage
}
}
}
}
} }
} }
} }
+35 -2
View File
@@ -1,3 +1,4 @@
import "root:/modules/common"
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
@@ -11,11 +12,43 @@ Singleton {
id: root id: root
property bool ready: Pipewire.defaultAudioSink?.ready ?? false property bool ready: Pipewire.defaultAudioSink?.ready ?? false
property var sink: Pipewire.defaultAudioSink property PwNode sink: Pipewire.defaultAudioSink
property var source: Pipewire.defaultAudioSource property PwNode source: Pipewire.defaultAudioSource
signal sinkProtectionTriggered(string reason);
PwObjectTracker { PwObjectTracker {
objects: [sink, source] objects: [sink, source]
Component.onCompleted: {
sink.audio.volume = sink.audio.volume; // Trigger initial volume change
}
}
Connections { // Protection against sudden volume changes
target: sink?.audio ?? null
property bool lastReady: false
property real lastVolume: 0
function onVolumeChanged() {
if (!ConfigOptions.audio.protection.enable) return;
if (!lastReady) {
lastVolume = sink.audio.volume;
lastReady = true;
return;
}
const newVolume = sink.audio.volume;
const maxAllowedIncrease = ConfigOptions.audio.protection.maxAllowedIncrease / 100;
const maxAllowed = ConfigOptions.audio.protection.maxAllowed / 100;
if (newVolume - lastVolume > maxAllowedIncrease) {
sink.audio.volume = lastVolume;
root.sinkProtectionTriggered("Illegal increment");
} else if (newVolume > maxAllowed) {
sink.audio.volume = lastVolume;
root.sinkProtectionTriggered("Exceeded max allowed");
}
lastVolume = sink.audio.volume;
}
} }
} }