366 lines
12 KiB
QML
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."
|
|
}
|
|
}
|
|
}
|