Initial bass-manage Qt Quick / Kirigami control panel.
PipeWire LFE/center mix UI with live pw-cli controls and conf save/reload.
This commit is contained in:
365
qml/Main.qml
Normal file
365
qml/Main.qml
Normal file
@@ -0,0 +1,365 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
title: "Bass Manage 7.1"
|
||||
width: 580
|
||||
height: 520
|
||||
minimumWidth: 480
|
||||
minimumHeight: 420
|
||||
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||
|
||||
// Aggregated UI values (conf uses full map; UI links FL/FR & mains)
|
||||
property real fcFold: 0.707
|
||||
property real dry: 1.0
|
||||
property real lfeMains: 0.99
|
||||
property real lfeDisc: 0.99
|
||||
property real lfeDelay: 0.010
|
||||
property bool suppressApply: false
|
||||
|
||||
function valuesMap() {
|
||||
return {
|
||||
"fl_dry": dry,
|
||||
"fr_dry": dry,
|
||||
"fc_to_fl": fcFold,
|
||||
"fc_to_fr": fcFold,
|
||||
"lfe_fl": lfeMains,
|
||||
"lfe_fr": lfeMains,
|
||||
"lfe_fc": lfeMains,
|
||||
"lfe_lfe": lfeDisc,
|
||||
"lfe_rl": lfeMains,
|
||||
"lfe_rr": lfeMains,
|
||||
"lfe_sl": lfeMains,
|
||||
"lfe_sr": lfeMains,
|
||||
"lfe_delay": lfeDelay
|
||||
}
|
||||
}
|
||||
|
||||
function applyFromMap(m) {
|
||||
suppressApply = true
|
||||
fcFold = (Number(m["fc_to_fl"]) + Number(m["fc_to_fr"])) / 2
|
||||
dry = (Number(m["fl_dry"]) + Number(m["fr_dry"])) / 2
|
||||
lfeMains = (Number(m["lfe_fl"]) + Number(m["lfe_fr"]) + Number(m["lfe_fc"])
|
||||
+ Number(m["lfe_rl"]) + Number(m["lfe_rr"]) + Number(m["lfe_sl"])
|
||||
+ Number(m["lfe_sr"])) / 7
|
||||
lfeDisc = Number(m["lfe_lfe"])
|
||||
lfeDelay = Number(m["lfe_delay"])
|
||||
suppressApply = false
|
||||
}
|
||||
|
||||
function scheduleApply() {
|
||||
if (suppressApply)
|
||||
return
|
||||
applyTimer.restart()
|
||||
}
|
||||
|
||||
function linToDb(x) {
|
||||
if (x <= 1e-9)
|
||||
return -120
|
||||
return 20 * Math.log(x) / Math.LN10
|
||||
}
|
||||
|
||||
function dbToLin(db) {
|
||||
if (db <= -120)
|
||||
return 0
|
||||
return Math.pow(10, db / 20)
|
||||
}
|
||||
|
||||
function iconUrl(name) {
|
||||
return "file://" + Backend.iconDir + "/" + name + ".svg"
|
||||
}
|
||||
|
||||
component LucideIcon: Kirigami.Icon {
|
||||
property string name
|
||||
source: root.iconUrl(name)
|
||||
color: Kirigami.Theme.textColor
|
||||
isMask: true
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: applyTimer
|
||||
interval: 40
|
||||
onTriggered: Backend.applyLive(root.valuesMap())
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onErrorOccurred(title, message) {
|
||||
errorDialog.title = title
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const m = Backend.loadConf()
|
||||
if (Object.keys(m).length)
|
||||
applyFromMap(m)
|
||||
}
|
||||
|
||||
pageStack.initialPage: Kirigami.ScrollablePage {
|
||||
id: page
|
||||
title: "Bass Manage 7.1"
|
||||
|
||||
titleDelegate: Item {
|
||||
// Stretch across the header so actions sit on the right of the title
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: row.implicitHeight
|
||||
width: parent ? parent.width : row.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
LucideIcon {
|
||||
name: "audio-lines"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
Kirigami.Heading {
|
||||
text: "Bass Manage 7.1"
|
||||
level: 1
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Controls.ToolButton {
|
||||
onClicked: {
|
||||
if (Backend.saveConf(root.valuesMap()))
|
||||
savedDialog.open()
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
LucideIcon { name: "save" }
|
||||
Controls.Label { text: "Save" }
|
||||
}
|
||||
Accessible.name: "Save"
|
||||
Controls.ToolTip.text: "Save to conf"
|
||||
Controls.ToolTip.visible: hovered
|
||||
Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
onClicked: {
|
||||
const m = Backend.loadConf()
|
||||
if (Object.keys(m).length)
|
||||
root.applyFromMap(m)
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
LucideIcon { name: "refresh-cw" }
|
||||
Controls.Label { text: "Reload" }
|
||||
}
|
||||
Accessible.name: "Reload"
|
||||
Controls.ToolTip.text: "Reload from conf"
|
||||
Controls.ToolTip.visible: hovered
|
||||
Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
LucideIcon { name: "audio-lines" }
|
||||
Controls.Label {
|
||||
text: Backend.status
|
||||
opacity: 0.75
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
wideMode: true
|
||||
|
||||
Kirigami.Separator {
|
||||
Kirigami.FormData.isSection: true
|
||||
Kirigami.FormData.label: "Center → fronts (add)"
|
||||
}
|
||||
|
||||
SliderRow {
|
||||
Kirigami.FormData.label: "FC into FL/FR"
|
||||
iconName: "align-center-horizontal"
|
||||
from: 0; to: 1.5; stepSize: 0.01
|
||||
kind: "gain"
|
||||
value: root.fcFold
|
||||
onValueEdited: (v) => { root.fcFold = v; root.scheduleApply() }
|
||||
}
|
||||
SliderRow {
|
||||
Kirigami.FormData.label: "FL/FR dry"
|
||||
iconName: "volume-2"
|
||||
from: 0; to: 1.5; stepSize: 0.01
|
||||
kind: "gain"
|
||||
value: root.dry
|
||||
onValueEdited: (v) => { root.dry = v; root.scheduleApply() }
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Kirigami.FormData.isSection: true
|
||||
Kirigami.FormData.label: "LFE mix"
|
||||
}
|
||||
|
||||
SliderRow {
|
||||
Kirigami.FormData.label: "Mains → LFE"
|
||||
iconName: "sliders-horizontal"
|
||||
from: 0; to: 1.5; stepSize: 0.01
|
||||
kind: "gain"
|
||||
value: root.lfeMains
|
||||
onValueEdited: (v) => { root.lfeMains = v; root.scheduleApply() }
|
||||
}
|
||||
SliderRow {
|
||||
Kirigami.FormData.label: "Discrete LFE"
|
||||
iconName: "speaker"
|
||||
from: 0; to: 1.5; stepSize: 0.01
|
||||
kind: "gain"
|
||||
value: root.lfeDisc
|
||||
onValueEdited: (v) => { root.lfeDisc = v; root.scheduleApply() }
|
||||
}
|
||||
SliderRow {
|
||||
Kirigami.FormData.label: "LFE delay"
|
||||
iconName: "timer"
|
||||
from: 0; to: 0.05; stepSize: 0.001
|
||||
kind: "delay"
|
||||
value: root.lfeDelay
|
||||
onValueEdited: (v) => { root.lfeDelay = v; root.scheduleApply() }
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
opacity: 0.7
|
||||
text: "Type a value and press Enter (gains: 0.707 or -3dB; delay: ms). Reload resets from conf. Save writes conf."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SliderRow: RowLayout {
|
||||
id: row
|
||||
property real from
|
||||
property real to
|
||||
property real stepSize
|
||||
property real value
|
||||
property string kind: "gain"
|
||||
property string iconName
|
||||
signal valueEdited(real v)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: root.iconUrl(row.iconName)
|
||||
color: Kirigami.Theme.textColor
|
||||
isMask: true
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
Controls.Slider {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
from: row.from
|
||||
to: row.to
|
||||
stepSize: row.stepSize
|
||||
value: row.value
|
||||
onMoved: row.valueEdited(value)
|
||||
}
|
||||
|
||||
Controls.TextField {
|
||||
id: field
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
|
||||
horizontalAlignment: Text.AlignRight
|
||||
|
||||
function formatValue(v) {
|
||||
return row.kind === "delay"
|
||||
? Number(v * 1000).toFixed(2).replace(/\.?0+$/, "")
|
||||
: Number(v).toPrecision(4)
|
||||
}
|
||||
|
||||
Component.onCompleted: text = formatValue(row.value)
|
||||
|
||||
Connections {
|
||||
target: row
|
||||
function onValueChanged() {
|
||||
if (!field.activeFocus)
|
||||
field.text = field.formatValue(row.value)
|
||||
}
|
||||
}
|
||||
|
||||
function commit() {
|
||||
let s = text.trim().toLowerCase().replace(",", ".")
|
||||
let parsed = NaN
|
||||
if (row.kind === "delay") {
|
||||
if (s.endsWith("ms"))
|
||||
parsed = parseFloat(s.slice(0, -2)) / 1000
|
||||
else if (s.endsWith("s"))
|
||||
parsed = parseFloat(s.slice(0, -1))
|
||||
else
|
||||
parsed = parseFloat(s) / 1000
|
||||
} else {
|
||||
if (s.endsWith("db"))
|
||||
parsed = root.dbToLin(parseFloat(s.slice(0, -2)))
|
||||
else if (s.startsWith("+") || (s.startsWith("-") && s.indexOf("e") < 0))
|
||||
parsed = root.dbToLin(parseFloat(s))
|
||||
else
|
||||
parsed = parseFloat(s)
|
||||
}
|
||||
if (isNaN(parsed)) {
|
||||
text = formatValue(row.value)
|
||||
return
|
||||
}
|
||||
parsed = Math.min(row.to, Math.max(row.from, parsed))
|
||||
row.valueEdited(parsed)
|
||||
}
|
||||
|
||||
onEditingFinished: commit()
|
||||
Keys.onReturnPressed: commit()
|
||||
Keys.onEnterPressed: commit()
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 5
|
||||
text: row.kind === "delay"
|
||||
? "ms"
|
||||
: "(" + (root.linToDb(row.value) >= 0 ? "+" : "")
|
||||
+ root.linToDb(row.value).toFixed(1) + " dB)"
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Dialog {
|
||||
id: errorDialog
|
||||
property alias text: errorLabel.text
|
||||
modal: true
|
||||
standardButtons: Controls.Dialog.Ok
|
||||
anchors.centerIn: parent
|
||||
Controls.Label {
|
||||
id: errorLabel
|
||||
wrapMode: Text.WordWrap
|
||||
width: errorDialog.availableWidth
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Dialog {
|
||||
id: savedDialog
|
||||
title: "Saved"
|
||||
modal: true
|
||||
standardButtons: Controls.Dialog.Ok
|
||||
anchors.centerIn: parent
|
||||
Controls.Label {
|
||||
wrapMode: Text.WordWrap
|
||||
width: savedDialog.availableWidth
|
||||
text: "Wrote " + Backend.confPath + "\nLive params applied."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user