Files
bass-manage/qml/Main.qml
litruv 0310f8177f Initial bass-manage Qt Quick / Kirigami control panel.
PipeWire LFE/center mix UI with live pw-cli controls and conf save/reload.
2026-08-27 04:30:14 +10:00

366 lines
12 KiB
QML

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."
}
}
}