wfview/qml/AudioProcessing.qml
2026-06-01 18:00:14 -07:00

811 lines
36 KiB
QML

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import WFVIEW 1.0
ApplicationWindow {
id: win
transientParent: null
title: mode === "rx" ? qsTr("RX Audio Processing") : qsTr("TX Audio Processing")
color: palette.window
width: mode === "rx" ? 860 : 635
height: mode === "rx" ? 660 : 980
minimumWidth: mode === "rx" ? 760 : 620
minimumHeight: 520
readonly property bool _sysTheme: MainController.settings.options["Interface.UseSystemTheme"] === true
palette {
window: _sysTheme ? undefined : MainController.settings.options["Color.Window"]
windowText: _sysTheme ? undefined : MainController.settings.options["Color.WindowText"]
base: _sysTheme ? undefined : MainController.settings.options["Color.Base"]
alternateBase: _sysTheme ? undefined : MainController.settings.options["Color.AlternateBase"]
text: _sysTheme ? undefined : MainController.settings.options["Color.MainText"]
button: _sysTheme ? undefined : MainController.settings.options["Color.Button"]
buttonText: _sysTheme ? undefined : MainController.settings.options["Color.ButtonText"]
brightText: _sysTheme ? undefined : MainController.settings.options["Color.BrightText"]
highlight: _sysTheme ? undefined : MainController.settings.options["Color.Highlight"]
highlightedText: _sysTheme ? undefined : MainController.settings.options["Color.HighlightedText"]
mid: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
dark: _sysTheme ? undefined : MainController.settings.options["Color.Dark"]
light: _sysTheme ? undefined : MainController.settings.options["Color.Light"]
placeholderText: _sysTheme ? undefined : MainController.settings.options["Color.PlaceholderText"]
disabled {
window: _sysTheme ? undefined : MainController.settings.options["Color.Window"]
windowText: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
base: _sysTheme ? undefined : MainController.settings.options["Color.Base"]
alternateBase: _sysTheme ? undefined : MainController.settings.options["Color.AlternateBase"]
text: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
button: _sysTheme ? undefined : MainController.settings.options["Color.Button"]
buttonText: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
brightText: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
highlight: _sysTheme ? undefined : MainController.settings.options["Color.Dark"]
highlightedText: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
mid: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
dark: _sysTheme ? undefined : MainController.settings.options["Color.Dark"]
light: _sysTheme ? undefined : MainController.settings.options["Color.Light"]
placeholderText: _sysTheme ? undefined : MainController.settings.options["Color.Mid"]
}
}
property string mode: "tx"
readonly property bool txMode: mode === "tx"
readonly property bool rxMode: mode === "rx"
property var controller
property var txInBins: []
property var txOutBins: []
property var rxInBins: []
property var rxOutBins: []
property real txInputLevel: 0
property real txOutputLevel: 0
property real txGainReduction: 0
property real rxInputLevel: 0
property real rxOutputLevel: 0
property int txBlocks: 0
property int rxBlocks: 0
property bool txSpectrumEnabled: false
property bool rxSpectrumEnabled: false
// Latest TX processing sample rate, reported via onTxAudioSpectrumChanged.
// Used to hide Multiband EQ bands that sit at or above the Nyquist limit.
property real txSampleRate: 48000
function applyDisabledPalette() {
try {
palette.disabled.window = MainController.settings.options["Color.Window"]
palette.disabled.windowText = Qt.darker(MainController.settings.options["Color.WindowText"], 2.5)
palette.disabled.base = Qt.darker(MainController.settings.options["Color.Base"], 1.1)
palette.disabled.alternateBase = Qt.darker(MainController.settings.options["Color.AlternateBase"], 1.1)
palette.disabled.text = Qt.darker(MainController.settings.options["Color.MainText"], 2.5)
palette.disabled.button = Qt.darker(MainController.settings.options["Color.Button"], 1.3)
palette.disabled.buttonText = Qt.darker(MainController.settings.options["Color.ButtonText"], 2.5)
palette.disabled.brightText = Qt.darker(MainController.settings.options["Color.BrightText"], 2.5)
palette.disabled.highlight = Qt.darker(MainController.settings.options["Color.Highlight"], 1.5)
palette.disabled.highlightedText = Qt.darker(MainController.settings.options["Color.MainText"], 2.5)
palette.disabled.mid = MainController.settings.options["Color.Mid"]
palette.disabled.dark = MainController.settings.options["Color.Dark"]
palette.disabled.light = Qt.darker(MainController.settings.options["Color.Light"], 1.2)
palette.disabled.placeholderText = Qt.darker(MainController.settings.options["Color.PlaceholderText"], 1.5)
} catch (e) {
// Qt 5 does not expose palette disabled groups to QML.
}
}
Component.onCompleted: applyDisabledPalette()
Connections {
target: MainController.settings
function onColChanged(items) {
win.applyDisabledPalette()
}
}
// Multiband EQ band centre frequencies — must stay in sync with the
// MbeqProcessor::bandFreqs table in src/audio/plugins/mbeq.h. The index of
// each entry is the backend EqBand index (AudioProc.Tx.EqBandN).
readonly property var eqBands: [
{ freq: 50, label: "50" },
{ freq: 100, label: "100" },
{ freq: 200, label: "200" },
{ freq: 300, label: "300" },
{ freq: 400, label: "400" },
{ freq: 600, label: "600" },
{ freq: 800, label: "800" },
{ freq: 1000, label: "1.0k" },
{ freq: 1200, label: "1.2k" },
{ freq: 1600, label: "1.6k" },
{ freq: 2000, label: "2.0k" },
{ freq: 2500, label: "2.5k" },
{ freq: 3200, label: "3.2k" },
{ freq: 5000, label: "5.0k" },
{ freq: 8000, label: "8.0k" }
]
// Bands below the Nyquist frequency, each carrying its original backend
// index so the correct AudioProc.Tx.EqBandN setting is driven.
function visibleEqBands() {
var nyquist = win.txSampleRate / 2
var out = []
for (var i = 0; i < win.eqBands.length; ++i)
if (win.eqBands[i].freq < nyquist)
out.push({ idx: i, label: win.eqBands[i].label })
return out
}
function opt(key, fallback) {
if (!controller || !controller.options || controller.options[key] === undefined)
return fallback
return controller.options[key]
}
function boolValue(value, fallback) {
if (typeof value === "boolean")
return value
if (typeof value === "number")
return value !== 0
if (typeof value === "string") {
const s = value.trim().toLowerCase()
if (s === "true" || s === "1" || s === "yes" || s === "on")
return true
if (s === "false" || s === "0" || s === "no" || s === "off" || s === "")
return false
}
return fallback === undefined ? false : boolValue(fallback, false)
}
function optBool(key, fallback) {
return boolValue(opt(key, fallback), fallback)
}
function setOpt(key, value) {
if (!controller)
return
controller.setOption(key, value)
MainController.applyAudioProcessingSettings()
}
onVisibleChanged: {
if (visible) {
MainController.updateApplicationPalette()
applyDisabledPalette()
}
}
function comboIndex(combo, value) {
var sv = String(value)
for (var i = 0; i < combo.count; ++i) {
var item = combo.model.get ? combo.model.get(i) : combo.model[i]
if (item && String(item.value) === sv)
return i
}
return -1
}
function canvasColor(c, alpha) {
if (c === undefined || c === null)
return "rgba(255,255,255," + alpha + ")"
return "rgba("
+ Math.round(c.r * 255) + ","
+ Math.round(c.g * 255) + ","
+ Math.round(c.b * 255) + ","
+ (alpha === undefined ? c.a : alpha) + ")"
}
component SwitchRow: CheckBox {
id: switchRow
property string key: ""
// Keyed switches reflect/persist a setting. Keyless switches are plain
// runtime checkboxes (default unchecked) — the caller handles onToggled.
Binding on checked {
when: switchRow.key !== ""
value: win.optBool(switchRow.key, false)
}
onToggled: if (switchRow.key !== "") win.setOpt(switchRow.key, checked)
indicator: Rectangle {
implicitWidth: 16
implicitHeight: 16
x: switchRow.leftPadding
y: switchRow.topPadding + (switchRow.availableHeight - height) / 2
radius: 2
color: switchRow.enabled
? (switchRow.checked ? switchRow.palette.highlight : switchRow.palette.base)
: switchRow.palette.mid
border.width: 1
border.color: switchRow.enabled ? switchRow.palette.text : switchRow.palette.mid
Label {
anchors.centerIn: parent
text: qsTr("X")
visible: switchRow.checked
color: switchRow.palette.highlightedText
font.bold: true
font.pixelSize: 11
}
}
contentItem: Label {
text: switchRow.text
font: switchRow.font
color: switchRow.enabled ? switchRow.palette.text : switchRow.palette.mid
verticalAlignment: Text.AlignVCenter
leftPadding: switchRow.indicator.width + switchRow.spacing
}
}
component SliderRow: RowLayout {
id: row
property string key: ""
property string label: ""
property real from: 0
property real to: 100
property real step: 1
property int decimals: 0
property string suffix: ""
property int maxSliderWidth: 0
property real displayScale: 1
spacing: 8
Layout.preferredHeight: 28
Label { text: row.label; Layout.preferredWidth: 118; elide: Text.ElideRight }
Slider {
id: slider
Layout.fillWidth: true
Layout.maximumWidth: row.maxSliderWidth > 0 ? row.maxSliderWidth : Number.POSITIVE_INFINITY
Layout.preferredHeight: 22
from: row.from
to: row.to
stepSize: row.step
value: Number(win.opt(row.key, row.from))
onMoved: win.setOpt(row.key, value)
}
Label {
Layout.preferredWidth: 70
horizontalAlignment: Text.AlignRight
text: Number(slider.value * row.displayScale).toFixed(row.decimals) + row.suffix
}
}
component ComboRow: RowLayout {
property string key: ""
property string label: ""
property var values: []
spacing: 8
Label { text: parent.label; Layout.preferredWidth: 118 }
ComboBox {
id: combo
Layout.preferredWidth: 210
textRole: "text"
valueRole: "value"
model: values
currentIndex: win.comboIndex(combo, win.opt(key, 0))
onActivated: win.setOpt(key, currentValue)
}
Item { Layout.fillWidth: true }
}
component VerticalEq: ColumnLayout {
id: eq
property string key: ""
property string label: ""
property real from: -9
property real to: 9
Label {
text: Number(slider.value).toFixed(1)
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 36
}
Slider {
id: slider
orientation: Qt.Vertical
Layout.preferredHeight: 104
Layout.preferredWidth: 28
Layout.alignment: Qt.AlignHCenter
from: eq.from
to: eq.to
stepSize: 0.5
value: Number(win.opt(eq.key, 0))
onMoved: win.setOpt(eq.key, value)
}
Label {
text: eq.label
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 44
font.pixelSize: 11
}
}
component LevelMeter: ColumnLayout {
property string label: ""
property real value: 0
spacing: 3
Label { text: parent.label; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
ProgressBar {
Layout.fillWidth: true
from: 0
to: 100
value: Math.max(0, Math.min(100, parent.value * 100))
}
}
component SpectrumBox: CollapsibleSection {
id: box
property var inputBins: []
property var outputBins: []
property string primaryColor: "#3fbf7f"
property string secondaryColor: "#ff9f43"
property string enableKey: ""
property string inhibitKey: ""
property string fpsKey: ""
property int blocksProcessed: 0
property bool processorEnabled: false
readonly property bool spectrumEnabled: enableKey === "" || win.optBool(enableKey, false)
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
RowLayout {
Layout.fillWidth: true
SwitchRow { text: qsTr("Enable spectrum"); key: box.enableKey }
SwitchRow { text: box.inhibitKey.indexOf(".Tx.") >= 0 ? qsTr("Inhibit during receive") : qsTr("Inhibit during TX"); key: box.inhibitKey }
Label { text: qsTr("FPS") }
SpinBox {
from: 1
to: 60
value: Number(win.opt(box.fpsKey, 10))
onValueModified: win.setOpt(box.fpsKey, value)
Layout.preferredWidth: 72
}
Item { Layout.fillWidth: true }
}
Canvas {
id: canvas
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 170
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var minDb = -90
var maxDb = 0
var left = 42
var right = 8
var top = 8
var bottom = 24
var plotX = left
var plotY = top
var plotW = Math.max(1, width - left - right)
var plotH = Math.max(1, height - top - bottom)
var bgColor = win.canvasColor(MainController.settings.options["Color.PlotBackground"], 1)
var gridColor = win.canvasColor(MainController.settings.options["Color.Grid"], 1)
var textColor = win.canvasColor(win.palette.text, 1)
var subtleTextColor = win.canvasColor(win.palette.text, 0.75)
ctx.fillStyle = bgColor
ctx.fillRect(0, 0, width, height)
drawGrid()
function dbToY(db) {
return plotY + plotH - ((db - minDb) / (maxDb - minDb)) * plotH
}
function freqToX(freq) {
var logMin = Math.log(50) / Math.LN10
var logMax = Math.log(8000) / Math.LN10
var logFreq = Math.log(Math.max(50, freq)) / Math.LN10
return plotX + ((logFreq - logMin) / (logMax - logMin)) * plotW
}
function freqLabel(freq) {
if (freq >= 1000)
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + "k"
return String(freq)
}
function drawGrid() {
ctx.save()
ctx.font = "10px sans-serif"
ctx.textBaseline = "middle"
ctx.lineWidth = 1
ctx.strokeStyle = gridColor
ctx.fillStyle = subtleTextColor
var dbStep = 6
while (plotH * dbStep / (maxDb - minDb) < 14 && dbStep < (maxDb - minDb))
dbStep += 6
ctx.setLineDash([3, 3])
for (var db = minDb; db <= maxDb; db += dbStep) {
var y = dbToY(db)
ctx.beginPath()
ctx.moveTo(plotX, y)
ctx.lineTo(plotX + plotW, y)
ctx.stroke()
ctx.textAlign = "right"
ctx.fillText(String(db), plotX - 5, y)
}
var freqs = [50, 100, 200, 400, 800, 1600, 3200, 6400, 8000]
ctx.textAlign = "center"
for (var f = 0; f < freqs.length; ++f) {
var x = freqToX(freqs[f])
ctx.beginPath()
ctx.moveTo(x, plotY)
ctx.lineTo(x, plotY + plotH)
ctx.stroke()
ctx.fillText(freqLabel(freqs[f]), x, plotY + plotH + 13)
}
ctx.setLineDash([])
ctx.strokeStyle = textColor
ctx.beginPath()
ctx.rect(plotX, plotY, plotW, plotH)
ctx.stroke()
ctx.textAlign = "left"
ctx.textBaseline = "top"
ctx.fillText("dBFS", 4, plotY)
ctx.restore()
}
if (!box.spectrumEnabled) {
ctx.fillStyle = textColor
ctx.textAlign = "center"
ctx.fillText(qsTr("Enable spectrum to view audio FFT data"), plotX + plotW / 2, plotY + plotH / 2)
return
}
if ((!box.inputBins || box.inputBins.length < 2) && (!box.outputBins || box.outputBins.length < 2)) {
ctx.fillStyle = textColor
ctx.textAlign = "center"
var status = qsTr("Waiting for audio spectrum data")
status += qsTr(" | blocks: ") + box.blocksProcessed
status += qsTr(" | processor spectrum: ") + (box.processorEnabled ? qsTr("on") : qsTr("off"))
ctx.fillText(status, plotX + plotW / 2, plotY + plotH / 2)
return
}
draw(box.inputBins, box.primaryColor)
draw(box.outputBins, box.secondaryColor)
function draw(bins, color) {
if (!bins || bins.length < 2) return
ctx.strokeStyle = color
ctx.lineWidth = 1.5
ctx.beginPath()
for (var n = 0; n < bins.length; ++n) {
var db = Math.max(minDb, Math.min(maxDb, Number(bins[n])))
var x = plotX + n * plotW / (bins.length - 1)
var y = dbToY(db)
if (n === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
}
}
}
onInputBinsChanged: canvas.requestPaint()
onOutputBinsChanged: canvas.requestPaint()
onSpectrumEnabledChanged: canvas.requestPaint()
}
Connections {
target: MainController
function onTxAudioSpectrumChanged(input, output, sampleRate) {
win.txInBins = input
win.txOutBins = output
if (sampleRate > 0)
win.txSampleRate = sampleRate
}
function onRxAudioSpectrumChanged(input, output, sampleRate) {
win.rxInBins = input
win.rxOutBins = output
}
function onTxAudioMetersChanged(input, output, gainReduction) {
win.txInputLevel = input
win.txOutputLevel = output
win.txGainReduction = gainReduction / 100.0
}
function onRxAudioMetersChanged(input, output) {
win.rxInputLevel = input
win.rxOutputLevel = output
}
function onAudioProcessingBlocksChanged(txBlocks, rxBlocks) {
win.txBlocks = txBlocks
win.rxBlocks = rxBlocks
}
function onAudioProcessingSpectrumStateChanged(txEnabled, rxEnabled) {
win.txSpectrumEnabled = txEnabled
win.rxSpectrumEnabled = rxEnabled
}
}
ScrollView {
id: scrollView
anchors.fill: parent
anchors.margins: 7
clip: true
contentWidth: availableWidth
Loader {
width: scrollView.availableWidth
sourceComponent: win.txMode ? txPage : rxPage
}
}
Component {
id: txPage
ColumnLayout {
width: parent ? parent.width : 620
spacing: 16
SwitchRow { text: qsTr("Master Bypass (disable all DSP)"); key: "AudioProc.Tx.Bypass"; font.bold: true }
CollapsibleSection {
title: qsTr("Noise Gate (pre-gain)")
expanded: false
ColumnLayout {
anchors.fill: parent
RowLayout {
SwitchRow { text: qsTr("Enable Noise Gate"); key: "AudioProc.Tx.GateEnabled" }
Item { Layout.fillWidth: true }
}
SliderRow { label: qsTr("Threshold"); key: "AudioProc.Tx.GateThreshold"; from: -70; to: 0; step: 1; suffix: " dB" }
SliderRow { label: qsTr("Attack"); key: "AudioProc.Tx.GateAttack"; from: 1; to: 500; step: 1; suffix: " ms" }
SliderRow { label: qsTr("Hold"); key: "AudioProc.Tx.GateHold"; from: 2; to: 1000; step: 2; suffix: " ms" }
SliderRow { label: qsTr("Decay"); key: "AudioProc.Tx.GateDecay"; from: 2; to: 1000; step: 2; suffix: " ms" }
SliderRow { label: qsTr("Range"); key: "AudioProc.Tx.GateRange"; from: -90; to: 0; step: 1; suffix: " dB" }
SliderRow { label: qsTr("LF cutoff"); key: "AudioProc.Tx.GateLfCutoff"; from: 20; to: 500; step: 5; suffix: " Hz" }
SliderRow { label: qsTr("HF cutoff"); key: "AudioProc.Tx.GateHfCutoff"; from: 200; to: 4000; step: 10; suffix: " Hz" }
}
}
ComboRow {
key: "AudioProc.Tx.EqFirst"
label: qsTr("Plugin order")
values: [{ text: qsTr("EQ -> Compressor"), value: true }, { text: qsTr("Compressor -> EQ"), value: false }]
}
GroupBox {
title: qsTr("Gain")
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
SliderRow { label: qsTr("Input gain"); key: "AudioProc.Tx.InputGainDB"; from: -20; to: 20; step: 0.5; decimals: 1; suffix: " dB"; maxSliderWidth: 86 }
SliderRow { label: qsTr("Output gain"); key: "AudioProc.Tx.OutputGainDB"; from: -20; to: 20; step: 0.5; decimals: 1; suffix: " dB"; maxSliderWidth: 86 }
}
}
CollapsibleSection {
title: qsTr("Multiband EQ")
expanded: false
ColumnLayout {
anchors.fill: parent
RowLayout {
SwitchRow { text: qsTr("Enable EQ"); key: "AudioProc.Tx.EqEnabled" }
Button {
text: qsTr("Clear")
onClicked: {
for (var i = 0; i < win.eqBands.length; ++i)
win.setOpt("AudioProc.Tx.EqBand" + i, 0)
}
}
}
RowLayout {
Layout.fillWidth: true
Repeater {
model: win.visibleEqBands()
VerticalEq { key: "AudioProc.Tx.EqBand" + modelData.idx; label: modelData.label }
}
}
}
}
CollapsibleSection {
title: qsTr("Dyson Compressor")
ColumnLayout {
anchors.fill: parent
SwitchRow { text: qsTr("Enable Compressor"); key: "AudioProc.Tx.CompEnabled" }
SliderRow { label: qsTr("Peak limit"); key: "AudioProc.Tx.CompPeakLimit"; from: -30; to: 0; step: 0.5; decimals: 1; suffix: " dB"; maxSliderWidth: 86 }
SliderRow { label: qsTr("Release time"); key: "AudioProc.Tx.CompRelease"; from: 0.01; to: 1; step: 0.01; decimals: 2; suffix: " s"; maxSliderWidth: 86 }
SliderRow { label: qsTr("Fast ratio"); key: "AudioProc.Tx.CompFastRatio"; from: 0; to: 1; step: 0.01; decimals: 2; maxSliderWidth: 86 }
SliderRow { label: qsTr("Slow ratio"); key: "AudioProc.Tx.CompSlowRatio"; from: 0; to: 1; step: 0.01; decimals: 2; maxSliderWidth: 86 }
}
}
SpectrumBox {
title: qsTr("TX Spectrum")
enableKey: "AudioProc.Tx.SpectrumEnabled"
inhibitKey: "AudioProc.Tx.SpecInhibitDuringRx"
fpsKey: "AudioProc.Tx.SpectrumFPS"
blocksProcessed: win.txBlocks
processorEnabled: win.txSpectrumEnabled
inputBins: win.txInBins
outputBins: win.txOutBins
}
GroupBox {
title: qsTr("Self-Monitor")
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
RowLayout {
SwitchRow { text: qsTr("Enable self-monitoring"); key: "AudioProc.Tx.SidetoneEnabled" }
SwitchRow {
id: muteRxSwitch
text: qsTr("Mute RX Audio")
enabled: win.optBool("AudioProc.Tx.SidetoneEnabled", false)
onToggled: MainController.setRxMuted(checked)
// Clear the mute if self-monitoring is turned off, so RX
// audio can't be left muted by a now-disabled control.
onEnabledChanged: if (!enabled && checked) { checked = false; MainController.setRxMuted(false) }
}
}
SliderRow { label: qsTr("Monitor level"); key: "AudioProc.Tx.SidetoneLevel"; from: 0; to: 1; step: 0.01; decimals: 0; suffix: "%"; maxSliderWidth: 86; displayScale: 100 }
}
}
GroupBox {
title: qsTr("Meters")
Layout.fillWidth: true
RowLayout {
anchors.fill: parent
LevelMeter { label: qsTr("Input"); value: win.txInputLevel }
LevelMeter { label: qsTr("Output"); value: win.txOutputLevel }
LevelMeter { label: qsTr("Gain Reduction"); value: win.txGainReduction }
}
}
}
}
Component {
id: rxPage
ColumnLayout {
width: parent ? parent.width : 840
spacing: 14
readonly property int nrMode: Number(win.opt("AudioProc.Rx.NrMode", 0))
RowLayout {
Layout.fillWidth: true
SwitchRow { text: qsTr("Master Bypass (pass audio unchanged)"); key: "AudioProc.Rx.Bypass"; font.bold: true }
Item { Layout.fillWidth: true }
ComboBox {
id: rxChannelCombo
Layout.preferredWidth: 420
textRole: "text"
valueRole: "value"
model: [{ text: qsTr("Auto"), value: 0 }, { text: qsTr("Left"), value: 1 }, { text: qsTr("Right"), value: 2 }, { text: qsTr("Mono"), value: 3 }]
currentIndex: win.comboIndex(rxChannelCombo, win.opt("AudioProc.Rx.ChannelSelect", 0))
onActivated: win.setOpt("AudioProc.Rx.ChannelSelect", currentValue)
}
}
GroupBox {
title: qsTr("Noise Reduction Algorithm")
Layout.fillWidth: true
RowLayout {
anchors.fill: parent
RadioButton { text: qsTr("None"); checked: nrMode === 0; onClicked: win.setOpt("AudioProc.Rx.NrMode", 0) }
RadioButton { text: qsTr("Speex"); checked: nrMode === 1; onClicked: win.setOpt("AudioProc.Rx.NrMode", 1) }
RadioButton { text: qsTr("ANR"); checked: nrMode === 2; onClicked: win.setOpt("AudioProc.Rx.NrMode", 2) }
Item { Layout.fillWidth: true }
}
}
Label {
visible: nrMode === 0
text: qsTr("No noise reduction selected.\nAudio passes through to the equalizer and output gain.")
font.italic: true
}
CollapsibleSection {
visible: nrMode === 1
title: qsTr("Speex DSP Noise Reduction")
ColumnLayout {
anchors.fill: parent
SliderRow { label: qsTr("Suppression"); key: "AudioProc.Rx.SpeexSuppression"; from: -70; to: -1; step: 1; suffix: " dB"; maxSliderWidth: 260 }
SliderRow { label: qsTr("Bark bands"); key: "AudioProc.Rx.SpeexBandsPreset"; from: 0; to: 8; step: 1; maxSliderWidth: 260 }
ComboRow { label: qsTr("Frame size"); key: "AudioProc.Rx.SpeexFrameMs"; values: [{ text: qsTr("10 ms frames"), value: 10 }, { text: qsTr("20 ms frames"), value: 20 }] }
SwitchRow { text: qsTr("Enable automatic gain control (AGC)"); key: "AudioProc.Rx.SpeexAgc" }
SliderRow { label: qsTr("AGC level"); key: "AudioProc.Rx.SpeexAgcLevel"; from: 500; to: 32000; step: 100; maxSliderWidth: 260 }
SliderRow { label: qsTr("AGC max gain"); key: "AudioProc.Rx.SpeexAgcMaxGain"; from: 0; to: 60; step: 1; suffix: " dB"; maxSliderWidth: 260 }
SwitchRow { text: qsTr("Enable voice activity detection (VAD)"); key: "AudioProc.Rx.SpeexVad" }
SliderRow { label: qsTr("VAD start"); key: "AudioProc.Rx.SpeexVadProbStart"; from: 0; to: 100; step: 1; suffix: " %"; maxSliderWidth: 260 }
SliderRow { label: qsTr("VAD continue"); key: "AudioProc.Rx.SpeexVadProbCont"; from: 0; to: 100; step: 1; suffix: " %"; maxSliderWidth: 260 }
SliderRow { label: qsTr("SNR decay"); key: "AudioProc.Rx.SpeexSnrDecay"; from: 0; to: 0.95; step: 0.01; decimals: 2; maxSliderWidth: 260 }
SliderRow { label: qsTr("Noise update"); key: "AudioProc.Rx.SpeexNoiseUpdateRate"; from: 0.01; to: 0.5; step: 0.01; decimals: 2; maxSliderWidth: 260 }
SliderRow { label: qsTr("Prior base"); key: "AudioProc.Rx.SpeexPriorBase"; from: 0.05; to: 0.5; step: 0.01; decimals: 2; maxSliderWidth: 260 }
}
}
CollapsibleSection {
visible: nrMode === 2
title: qsTr("ANR Noise Reduction")
ColumnLayout {
anchors.fill: parent
SliderRow { label: qsTr("Noise reduction"); key: "AudioProc.Rx.AnrNoiseReductionDb"; from: 0; to: 48; step: 0.5; decimals: 1; suffix: " dB"; maxSliderWidth: 260 }
SliderRow { label: qsTr("Sensitivity"); key: "AudioProc.Rx.AnrSensitivity"; from: 0; to: 10; step: 0.1; decimals: 1; maxSliderWidth: 260 }
SliderRow { label: qsTr("Smoothing"); key: "AudioProc.Rx.AnrFreqSmoothing"; from: 0; to: 6; step: 1; maxSliderWidth: 260 }
RowLayout {
Button { text: qsTr("Collect Noise Sample"); onClicked: MainController.startAnrNoiseProfile() }
Button { text: qsTr("Stop Collecting"); onClicked: MainController.stopAnrNoiseProfile() }
}
}
}
CollapsibleSection {
title: qsTr("Receive Equalizer")
ColumnLayout {
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
SwitchRow { text: qsTr("Enable EQ"); key: "AudioProc.Rx.EqEnabled" }
Button {
text: qsTr("Clear")
onClicked: {
for (var i = 0; i < 4; ++i)
win.setOpt("AudioProc.Rx.EqGain" + i, 0)
}
}
}
RowLayout {
Layout.fillWidth: true
Repeater {
model: [
{ name: qsTr("Low Shelf"), freq: "100 Hz" },
{ name: qsTr("Low Mid"), freq: "800 Hz" },
{ name: qsTr("High Mid"), freq: "2.00 kHz" },
{ name: qsTr("High Shelf"), freq: "3.50 kHz" }
]
ColumnLayout {
Layout.fillWidth: true
Label { text: modelData.name; font.bold: true; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
Label { text: Number(win.opt("AudioProc.Rx.EqGain" + index, 0)).toFixed(1) + " dB"; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
Slider {
orientation: Qt.Vertical
from: -9
to: 9
stepSize: 0.5
value: Number(win.opt("AudioProc.Rx.EqGain" + index, 0))
Layout.preferredHeight: 104
Layout.alignment: Qt.AlignHCenter
onMoved: win.setOpt("AudioProc.Rx.EqGain" + index, value)
}
Slider {
from: 20
to: 8000
stepSize: 10
value: Number(win.opt("AudioProc.Rx.EqFreq" + index, 0))
Layout.preferredWidth: 28
Layout.alignment: Qt.AlignHCenter
onMoved: win.setOpt("AudioProc.Rx.EqFreq" + index, value)
}
Label { text: modelData.freq; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
}
}
}
}
}
GroupBox {
title: qsTr("Output Gain")
Layout.fillWidth: true
SliderRow { label: qsTr("Output gain"); key: "AudioProc.Rx.OutputGainDB"; from: -6; to: 20; step: 0.5; decimals: 1; suffix: " dB"; maxSliderWidth: 86 }
}
SpectrumBox {
title: qsTr("RX Spectrum")
enableKey: "AudioProc.Rx.SpectrumEnabled"
inhibitKey: "AudioProc.Rx.SpecInhibitDuringTx"
fpsKey: "AudioProc.Rx.SpectrumFPS"
blocksProcessed: win.rxBlocks
processorEnabled: win.rxSpectrumEnabled
inputBins: win.rxInBins
outputBins: win.rxOutBins
}
}
}
}