wfview/old-source/audioprocessingwidget.cpp
2026-05-23 18:21:27 +01:00

769 lines
32 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// AudioProcessingWidget — TX audio processing settings dialog
#include "audioprocessingwidget.h"
#include "logcategories.h"
#include <cmath>
// MBEQ band centre frequencies (Hz) — must match MbeqProcessor::bandFreqs
static const float kBandFreqs[TxAudioProcessor::EQ_BANDS] = {
50, 100, 200, 300, 400, 600, 800, 1000,
1200, 1600, 2000, 2500, 3200, 5000, 8000
};
static QString freqLabel(float hz)
{
if (hz >= 1000.0f) return QString::number(hz / 1000.0f, 'f', 1) + "K";
return QString::number(int(hz));
}
// ─────────────────────────────────────────────────────────────────────────────
AudioProcessingWidget::AudioProcessingWidget(QWidget* parent)
: QDialog(parent)
{
setWindowTitle(tr("TX Audio Processing"));
setWindowFlags(windowFlags() | Qt::Window);
buildUi();
connect(&m_specDiagTimer, &QTimer::timeout,
this, &AudioProcessingWidget::onSpecDiagTimer);
m_specDiagTimer.setInterval(1000);
m_specDiagTimer.start();
}
// ─── setPrefs / getPrefs ─────────────────────────────────────────────────────
void AudioProcessingWidget::setPrefs(const audioProcessingPrefs& p)
{
blockAll(true);
populateFromPrefs(p);
blockAll(false);
}
audioProcessingPrefs AudioProcessingWidget::getPrefs() const
{
audioProcessingPrefs p;
p.bypass = bypassCheck->isChecked();
p.eqEnabled = eqEnable->isChecked();
p.compEnabled = compEnable->isChecked();
p.eqFirst = (orderCombo->currentIndex() == 0); // 0=EQ→Comp, 1=Comp→EQ
for (int i = 0; i < EQ_BANDS; ++i)
p.eqBands[i] = eqSliders[i]->value() * 0.1f; // slider is ×10
p.compPeakLimit = compPeakLimit->value() * 0.1f;
p.compRelease = compRelease->value() * 0.01f;
p.compFastRatio = (100-compFastRatio->value()) * 0.01f;
p.compSlowRatio = (100-compSlowRatio->value()) * 0.01f;
p.inputGainDB = inputGain->value() * 0.1f;
p.outputGainDB = outputGain->value() * 0.1f;
p.sidetoneEnabled = sidetoneEnable->isChecked();
p.sidetoneLevel = sidetoneLevel->value() * 0.01f;
p.muteRx = muteRxCheck->isChecked();
p.spectrumEnabled = specEnable->isChecked();
p.spectrumFPS = m_spectrumFps;
p.gateEnabled = gateEnable->isChecked();
p.gateThreshold = static_cast<float>(gateThreshold->value());
p.gateAttack = static_cast<float>(gateAttack->value());
p.gateHold = static_cast<float>(gateHold->value());
p.gateDecay = static_cast<float>(gateDecay->value());
p.gateRange = static_cast<float>(gateRange->value());
p.gateLfCutoff = static_cast<float>(gateLfCutoff->value());
p.gateHfCutoff = static_cast<float>(gateHfCutoff->value());
return p;
}
// ─── Level meter updates ──────────────────────────────────────────────────────
void AudioProcessingWidget::updateInputLevel(float peak)
{
inputMeter->setLevel(static_cast<double>(peak) * 255.0);
}
void AudioProcessingWidget::updateOutputLevel(float peak)
{
outputMeter->setLevel(static_cast<double>(peak) * 255.0);
}
void AudioProcessingWidget::updateInputLevels(float rms, float peak)
{
inputMeter->setLevels(static_cast<double>(rms) * 255.0, static_cast<double>(peak) * 255.0);
}
void AudioProcessingWidget::updateOutputLevels(float rms, float peak)
{
outputMeter->setLevels(static_cast<double>(rms) * 255.0, static_cast<double>(peak) * 255.0);
}
void AudioProcessingWidget::updateGainReduction(float linear)
{
// Gain reduction meter: 0 dB = no reduction (1.0), shown on meterComp scale.
// Convert linear gain → raw 0-255 value for the meter.
// meterComp typically shows 020 dB reduction; map linearly.
const float dBgain = (linear > 0.0001f) ? 20.0f * std::log10(linear) : -40.0f;
// dBgain ≤ 0 for compression. Map [0, -20] dB → [0, 255]
const double val = std::min(255.0, std::max(0.0, -dBgain * 255.0 / 20.0));
grMeter->setLevel(val);
}
// ─── Any control changed ──────────────────────────────────────────────────────
void AudioProcessingWidget::onAnyControlChanged()
{
// Update value labels
for (int i = 0; i < EQ_BANDS; ++i) {
float db = eqSliders[i]->value() * 0.1f;
eqValues[i]->setText(QString::number(db, 'f', 1));
}
lblPeak->setText(QString::number(compPeakLimit->value() * 0.1f, 'f', 1) + " dB");
lblRelease->setText(QString::number(compRelease->value() * 0.01f, 'f', 2) + " s");
lblFast->setText(QString("%1:1").arg(1 + compFastRatio->value() * 99 / 100, 3));
lblSlow->setText(QString("%1:1").arg(1 + compSlowRatio->value() * 99 / 100, 3));
lblInputGain->setText(QString::number(inputGain->value() * 0.1f, 'f', 1) + " dB");
lblOutputGain->setText(QString::number(outputGain->value() * 0.1f, 'f', 1) + " dB");
lblSidetone->setText(QString::number(sidetoneLevel->value()) + "%");
lblGateThreshold->setText(QString::number(gateThreshold->value()) + " dB");
lblGateAttack->setText(QString::number(gateAttack->value()) + " ms");
lblGateHold->setText(QString::number(gateHold->value()) + " ms");
lblGateDecay->setText(QString::number(gateDecay->value()) + " ms");
lblGateRange->setText(QString::number(gateRange->value()) + " dB");
lblGateLfCutoff->setText(QString::number(gateLfCutoff->value()) + " Hz");
lblGateHfCutoff->setText(QString::number(gateHfCutoff->value()) + " Hz");
emit prefsChanged(getPrefs());
}
// ─── Bypass / Clear EQ slots ──────────────────────────────────────────────────
void AudioProcessingWidget::onBypassToggled(bool bypassed)
{
setProcessingControlsEnabled(!bypassed);
emit prefsChanged(getPrefs());
}
void AudioProcessingWidget::onClearEq()
{
blockAll(true);
for (int i = 0; i < EQ_BANDS; ++i) {
eqSliders[i]->setValue(0);
eqValues[i]->setText("0.0");
}
blockAll(false);
emit prefsChanged(getPrefs());
}
void AudioProcessingWidget::reorderDspGroups(bool eqFirst)
{
// Remove both widgets from the sub-layout, then re-add in the chosen order.
m_dspOrderLayout->removeWidget(eqGrp);
m_dspOrderLayout->removeWidget(compGrp);
if (eqFirst) {
m_dspOrderLayout->addWidget(eqGrp);
m_dspOrderLayout->addWidget(compGrp);
} else {
m_dspOrderLayout->addWidget(compGrp);
m_dspOrderLayout->addWidget(eqGrp);
}
}
void AudioProcessingWidget::onPluginOrderChanged(int index)
{
// index 0 = "EQ → Compressor" (EQ on top), 1 = "Compressor → EQ"
reorderDspGroups(index == 0);
emit prefsChanged(getPrefs());
}
void AudioProcessingWidget::onSpecEnableToggled(bool enabled)
{
specWidget->setVisible(enabled);
if (!enabled) {
specWidget->spectrumPrimary.clear();
specWidget->spectrumSecondary.clear();
}
adjustSize();
emit prefsChanged(getPrefs());
}
void AudioProcessingWidget::setConnected(bool connected)
{
m_radioConnected = connected;
if (!connected) {
specWidget->spectrumPrimary.clear();
specWidget->spectrumSecondary.clear();
m_audioSampleRate = 0.0f;
}
}
// ─── onSpectrumBins ───────────────────────────────────────────────────────────
// Receives pre-computed dBFS spectrum bins from TxAudioProcessor (runs FFT on
// the converter thread at TimeCriticalPriority). This slot only needs to
// update EQ band visibility on SR change and forward bins to SpectrumWidget.
void AudioProcessingWidget::onSpectrumBins(QVector<double> inBins,
QVector<double> outBins,
float rawSR)
{
if (!m_radioConnected) return;
if (!specEnable || !specEnable->isChecked()) return;
++m_batchCount;
if (rawSR != m_audioSampleRate) {
m_audioSampleRate = rawSR;
updateEqBandVisibility(rawSR);
// Effective sample rate after decimation to ~16 kHz.
const int K = qMax(1, static_cast<int>(std::round(rawSR / 16000.0f)));
specWidget->sampleRate = static_cast<double>(rawSR) / K;
specWidget->fftLength = TxAudioProcessor::SPEC_FFT_LEN;
}
specWidget->spectrumPrimary.assign(inBins.cbegin(), inBins.cend());
specWidget->spectrumSecondary.assign(outBins.cbegin(), outBins.cend());
}
void AudioProcessingWidget::updateEqBandVisibility(float sampleRate)
{
if (sampleRate <= 0.0f) return;
const float nyquist = sampleRate * 0.5f;
for (int i = 0; i < EQ_BANDS; ++i)
eqColumns[i]->setVisible(kBandFreqs[i] <= nyquist);
}
// ─── onSpecDiagTimer ──────────────────────────────────────────────────────────
// Fires every second. FFT now runs on the converter thread (TxAudioProcessor);
// we just log how many bin-sets per second reach this widget.
void AudioProcessingWidget::onSpecDiagTimer()
{
if (!specEnable || !specEnable->isChecked()) {
m_batchCount = 0;
return;
}
const bool audioArriving = (m_batchCount > 0);
const bool fpsMiss = audioArriving && (m_batchCount < m_spectrumFps * 8 / 10);
QString noDataReason;
if (!audioArriving) {
if (!m_radioConnected) noDataReason = "radio not connected";
else noDataReason = "no TX audio (not transmitting?)";
}
/*
qCDebug(logAudio) << "[SpectrumBins]"
<< m_batchCount << "bin-sets/s"
<< "(target" << m_spectrumFps << "fps)"
<< (fpsMiss ? "*** FPS NOT MET ***" : "")
<< (!noDataReason.isEmpty() ? "— " + noDataReason : "");
*/
m_batchCount = 0;
}
void AudioProcessingWidget::setProcessingControlsEnabled(bool enabled)
{
orderRow->setEnabled(enabled);
gateGrp->setEnabled(enabled);
gainGrp->setEnabled(enabled);
eqGrp->setEnabled(enabled);
compGrp->setEnabled(enabled);
sidetoneGrp->setEnabled(enabled);
}
// ─── buildUi ─────────────────────────────────────────────────────────────────
void AudioProcessingWidget::buildUi()
{
auto* mainLayout = new QVBoxLayout(this);
// ── Master bypass ────────────────────────────────────────────────────────
{
bypassCheck = new QCheckBox(tr("Master Bypass (disable all DSP)"));
QFont f = bypassCheck->font();
f.setBold(true);
bypassCheck->setFont(f);
mainLayout->addWidget(bypassCheck);
connect(bypassCheck, &QCheckBox::toggled,
this, &AudioProcessingWidget::onBypassToggled);
}
// ── Noise gate (runs first — before input gain) ──────────────────────────
{
gateGrp = new QGroupBox(tr("Noise Gate (pre-gain)"));
auto* grp = gateGrp;
auto* form = new QFormLayout(grp);
gateEnable = new QCheckBox(tr("Enable Noise Gate"));
gateEnable->setToolTip(tr("Gate attenuates audio below threshold before input gain. "
"Use to eliminate background noise between speech bursts."));
form->addRow(gateEnable);
auto makeGateSlider = [](int lo, int hi, int val) {
auto* s = new QSlider(Qt::Horizontal);
s->setRange(lo, hi);
s->setValue(val);
return s;
};
// Threshold: -70..0 dB
gateThreshold = makeGateSlider(-70, 0, -40);
lblGateThreshold = new QLabel("-40 dB");
{
auto* row = new QHBoxLayout;
row->addWidget(gateThreshold);
row->addWidget(lblGateThreshold);
form->addRow(tr("Threshold:"), row);
}
gateThreshold->setToolTip(tr("Signal level at which the gate opens. "
"Lower = gate opens for quieter signals."));
// Attack: 1..500 ms
gateAttack = makeGateSlider(1, 500, 10);
lblGateAttack = new QLabel("10 ms");
{
auto* row = new QHBoxLayout;
row->addWidget(gateAttack);
row->addWidget(lblGateAttack);
form->addRow(tr("Attack:"), row);
}
gateAttack->setToolTip(tr("How quickly the gate fully opens after threshold is crossed (ms)."));
// Hold: 2..1000 ms
gateHold = makeGateSlider(2, 1000, 100);
lblGateHold = new QLabel("100 ms");
{
auto* row = new QHBoxLayout;
row->addWidget(gateHold);
row->addWidget(lblGateHold);
form->addRow(tr("Hold:"), row);
}
gateHold->setToolTip(tr("Minimum time the gate stays open after signal drops below threshold (ms). "
"Prevents rapid chattering."));
// Decay: 2..2000 ms
gateDecay = makeGateSlider(2, 1000, 200);
lblGateDecay = new QLabel("200 ms");
{
auto* row = new QHBoxLayout;
row->addWidget(gateDecay);
row->addWidget(lblGateDecay);
form->addRow(tr("Decay:"), row);
}
gateDecay->setToolTip(tr("How quickly the gate closes after hold time expires (ms)."));
// Range: -90..0 dB
gateRange = makeGateSlider(-90, 0, -60);
lblGateRange = new QLabel("-90 dB");
{
auto* row = new QHBoxLayout;
row->addWidget(gateRange);
row->addWidget(lblGateRange);
form->addRow(tr("Range:"), row);
}
gateRange->setToolTip(tr("Attenuation applied when the gate is closed. "
"-90 dB = virtually silent; 0 dB = no gating effect."));
// LF key filter cutoff: 20..4000 Hz
gateLfCutoff = makeGateSlider(20, 500, 380);
lblGateLfCutoff = new QLabel("380 Hz");
{
auto* row = new QHBoxLayout;
row->addWidget(gateLfCutoff);
row->addWidget(lblGateLfCutoff);
form->addRow(tr("Key LF cut:"), row);
}
gateLfCutoff->setToolTip(tr("High-pass frequency of the gate key detector (Hz). "
"Raise to prevent low-frequency rumble from triggering the gate."));
// HF key filter cutoff: 200..20000 Hz
gateHfCutoff = makeGateSlider(200, 4000, 2700);
lblGateHfCutoff = new QLabel("8000 Hz");
{
auto* row = new QHBoxLayout;
row->addWidget(gateHfCutoff);
row->addWidget(lblGateHfCutoff);
form->addRow(tr("Key HF cut:"), row);
}
gateHfCutoff->setToolTip(tr("Low-pass frequency of the gate key detector (Hz). "
"Lower to prevent high-frequency noise from triggering the gate."));
mainLayout->addWidget(grp);
}
// ── Plugin order ────────────────────────────────────────────────────────
{
orderRow = new QWidget;
auto* row = new QHBoxLayout(orderRow);
row->setContentsMargins(0, 0, 0, 0);
row->addWidget(new QLabel(tr("Plugin order:")));
orderCombo = new QComboBox;
orderCombo->addItem(tr("EQ → Compressor"));
orderCombo->addItem(tr("Compressor → EQ"));
row->addWidget(orderCombo);
row->addStretch();
mainLayout->addWidget(orderRow);
}
// ── Gain controls ────────────────────────────────────────────────────────
{
gainGrp = new QGroupBox(tr("Gain"));
auto* grp = gainGrp;
auto* form = new QFormLayout(grp);
auto makeGainSlider = [&](QSlider*& sl, QLabel*& lbl) {
sl = new QSlider(Qt::Horizontal);
sl->setRange(-200, 200); // ×0.1 dB → -20..+20 dB
sl->setValue(0);
sl->setTickPosition(QSlider::TicksBelow);
sl->setTickInterval(50);
lbl = new QLabel("0.0 dB");
lbl->setMinimumWidth(60);
auto* row = new QHBoxLayout;
row->addWidget(sl);
row->addWidget(lbl);
return row;
};
form->addRow(tr("Input gain:"), makeGainSlider(inputGain, lblInputGain));
form->addRow(tr("Output gain:"), makeGainSlider(outputGain, lblOutputGain));
inputGain->setToolTip(tr("Tip: Increase to around +10dB to drive the compressor into harder compression."));
outputGain->setToolTip(tr("Tip: Use the output gain to makeup for lost amplitude from the compression stage."));
mainLayout->addWidget(grp);
}
// ── Compressor ──────────────────────────────────────────────────────────
{
compGrp = new QGroupBox(tr("Dyson Compressor"));
auto* grp = compGrp;
auto* form = new QFormLayout(grp);
compEnable = new QCheckBox(tr("Enable Compressor"));
form->addRow(compEnable);
auto makeSlider = [](int lo, int hi, int val) {
auto* s = new QSlider(Qt::Horizontal);
s->setRange(lo, hi);
s->setValue(val);
return s;
};
// Peak limit: -30..0 dB, stored ×10, so range -300..0
compPeakLimit = makeSlider(-300, 0, -100);
lblPeak = new QLabel("-10.0 dB");
{
auto* row = new QHBoxLayout;
row->addWidget(compPeakLimit);
row->addWidget(lblPeak);
form->addRow(tr("Peak limit:"), row);
}
// Release: 0.01..1.0 s → stored ×100, range 1..100
compRelease = makeSlider(1, 100, 10);
lblRelease = new QLabel("0.10 s");
{
auto* row = new QHBoxLayout;
row->addWidget(compRelease);
row->addWidget(lblRelease);
form->addRow(tr("Release time:"), row);
}
// Fast ratio: DSP 0.0=1:1 (no compression) … 1.0=100:1 (max), stored ×100
compFastRatio = makeSlider(0, 100, 50);
lblFast = new QLabel(" 50:1");
lblFast->setMinimumWidth(55);
{
auto* row = new QHBoxLayout;
row->addWidget(compFastRatio);
row->addWidget(lblFast);
form->addRow(tr("Fast ratio:"), row);
}
// Slow ratio: DSP 0.0=1:1 (no compression) … 1.0=100:1 (max), stored ×100
compSlowRatio = makeSlider(0, 100, 30);
lblSlow = new QLabel(" 30:1");
lblSlow->setMinimumWidth(55);
{
auto* row = new QHBoxLayout;
row->addWidget(compSlowRatio);
row->addWidget(lblSlow);
form->addRow(tr("Slow ratio:"), row);
}
}
// ── Multiband EQ ─────────────────────────────────────────────────────────
{
eqGrp = new QGroupBox(tr("Multiband EQ"));
auto* grp = eqGrp;
auto* vbox = new QVBoxLayout(grp);
auto* eqEnableRow = new QHBoxLayout;
eqEnable = new QCheckBox(tr("Enable EQ"));
clearEqBtn = new QPushButton(tr("Clear"));
clearEqBtn->setToolTip(tr("Reset all EQ bands to 0 dB"));
eqEnableRow->addWidget(eqEnable);
eqEnableRow->addWidget(clearEqBtn);
eqEnableRow->addStretch();
vbox->addLayout(eqEnableRow);
auto* sliderRow = new QHBoxLayout;
for (int i = 0; i < EQ_BANDS; ++i) {
// Wrap each band in a QWidget so it can be hidden when its
// centre frequency exceeds the audio Nyquist.
eqColumns[i] = new QWidget;
auto* col = new QVBoxLayout(eqColumns[i]);
col->setContentsMargins(1, 0, 1, 0);
eqSliders[i] = new QSlider(Qt::Vertical);
eqSliders[i]->setRange(-100, 100); // ×0.1 dB → -10..+10 dB
eqSliders[i]->setValue(0);
eqSliders[i]->setTickPosition(QSlider::TicksRight);
eqSliders[i]->setTickInterval(10); // 1.0 dB ticks
eqSliders[i]->setMinimumHeight(120);
eqValues[i] = new QLabel("0.0");
eqValues[i]->setAlignment(Qt::AlignHCenter);
auto* freqLbl = new QLabel(freqLabel(kBandFreqs[i]));
freqLbl->setAlignment(Qt::AlignHCenter);
col->addWidget(eqValues[i]);
col->addWidget(eqSliders[i]);
col->addWidget(freqLbl);
sliderRow->addWidget(eqColumns[i]);
connect(eqSliders[i], &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
}
vbox->addLayout(sliderRow);
}
// ── DSP order container: holds eqGrp + compGrp in combo-selected order ──
{
auto* dspContainer = new QWidget;
m_dspOrderLayout = new QVBoxLayout(dspContainer);
m_dspOrderLayout->setContentsMargins(0, 0, 0, 0);
// Default index 0 = "EQ → Compressor": EQ on top.
m_dspOrderLayout->addWidget(eqGrp);
m_dspOrderLayout->addWidget(compGrp);
mainLayout->addWidget(dspContainer);
}
// ── TX Spectrum ──────────────────────────────────────────────────────────
{
specGrp = new QGroupBox(tr("TX Spectrum"));
auto* grp = specGrp;
auto* vbox = new QVBoxLayout(grp);
specEnable = new QCheckBox(tr("Enable spectrum display"));
specEnable->setToolTip(tr("Show TX audio spectrum (input: green, output: orange). "
"Uses a 1024-point sliding DFT, 50 Hz 8 kHz."));
vbox->addWidget(specEnable);
specWidget = new SpectrumWidget;
specWidget->logBins = true;
specWidget->fftLength = TxAudioProcessor::SPEC_FFT_LEN;
specWidget->sampleRate = 48000.0;
specWidget->showSecondary = true;
specWidget->setMinimumHeight(120);
specWidget->setVisible(false); // hidden until checkbox is ticked
vbox->addWidget(specWidget);
mainLayout->addWidget(grp);
connect(specEnable, &QCheckBox::toggled,
this, &AudioProcessingWidget::onSpecEnableToggled);
}
// ── Monitoring (via SideTone) ────────────────────────────────────────────
{
sidetoneGrp = new QGroupBox(tr("Self-Monitor"));
auto* grp = sidetoneGrp;
auto* form = new QFormLayout(grp);
auto* sidetoneEnableRow = new QHBoxLayout;
sidetoneEnable = new QCheckBox(tr("Enable self-monitoring"));
sidetoneEnable->setToolTip(tr("Enables Talk Back"));
muteRxCheck = new QCheckBox(tr("Mute RX Audio"));
muteRxCheck->setToolTip(tr("Check to mute the receiver temporarily"));
sidetoneEnableRow->addWidget(sidetoneEnable);
sidetoneEnableRow->addWidget(muteRxCheck);
sidetoneEnableRow->addStretch();
form->addRow(sidetoneEnableRow);
sidetoneLevel = new QSlider(Qt::Horizontal);
sidetoneLevel->setRange(0, 100);
sidetoneLevel->setValue(50);
lblSidetone = new QLabel("50%");
{
auto* row = new QHBoxLayout;
row->addWidget(sidetoneLevel);
row->addWidget(lblSidetone);
form->addRow(tr("Monitor level:"), row);
}
mainLayout->addWidget(grp);
}
// ── Meters ──────────────────────────────────────────────────────────────
{
auto* grp = new QGroupBox(tr("Meters"));
auto* hbox = new QHBoxLayout(grp);
auto makeMeterBox = [&](const QString& label, meter*& m, meter_t type,
double lo, double hi, double red) {
auto* vbox = new QVBoxLayout;
vbox->addWidget(new QLabel(label));
m = new meter;
m->setMeterType(type);
m->setMeterExtremities(lo, hi, red);
m->setMinimumWidth(150);
m->setMinimumHeight(40);
m->enableCombo(false);
vbox->addWidget(m);
return vbox;
};
// meterAudio uses the log audiopot taper so scaleMin/Max are not used for
// bar drawing; only haveExtremities matters. Redline at 241/255 ≈ -0.9 dBfs.
// meterComp uses linear 0255 (our updateGainReduction maps 020 dB → 0255).
hbox->addLayout(makeMeterBox(tr("Input"), inputMeter, meterAudio, 0, 255, 241));
hbox->addLayout(makeMeterBox(tr("Output"), outputMeter, meterAudio, 0, 255, 241));
hbox->addLayout(makeMeterBox(tr("Gain Reduction"), grMeter, meterComp, 0, 100, 75));
grMeter->setCompReverse(true);
mainLayout->addWidget(grp);
}
// ── Wire remaining signals ───────────────────────────────────────────────
connect(clearEqBtn, &QPushButton::clicked, this, &AudioProcessingWidget::onClearEq);
connect(muteRxCheck, &QCheckBox::toggled, this, &AudioProcessingWidget::onAnyControlChanged);
connect(eqEnable, &QCheckBox::toggled, this, &AudioProcessingWidget::onAnyControlChanged);
connect(compEnable, &QCheckBox::toggled, this, &AudioProcessingWidget::onAnyControlChanged);
connect(orderCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &AudioProcessingWidget::onPluginOrderChanged);
connect(sidetoneEnable,&QCheckBox::toggled, this, &AudioProcessingWidget::onAnyControlChanged);
connect(sidetoneLevel, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(inputGain, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(outputGain, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(compPeakLimit, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(compRelease, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(compFastRatio, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(compSlowRatio, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateEnable, &QCheckBox::toggled,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateThreshold, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateAttack, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateHold, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateDecay, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateRange, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateLfCutoff, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
connect(gateHfCutoff, &QSlider::valueChanged,
this, &AudioProcessingWidget::onAnyControlChanged);
adjustSize();
}
// ─── Private helpers ─────────────────────────────────────────────────────────
void AudioProcessingWidget::blockAll(bool block)
{
bypassCheck->blockSignals(block);
for (int i = 0; i < EQ_BANDS; ++i) eqSliders[i]->blockSignals(block);
eqEnable->blockSignals(block);
compEnable->blockSignals(block);
compPeakLimit->blockSignals(block);
compRelease->blockSignals(block);
compFastRatio->blockSignals(block);
compSlowRatio->blockSignals(block);
inputGain->blockSignals(block);
outputGain->blockSignals(block);
orderCombo->blockSignals(block);
sidetoneEnable->blockSignals(block);
sidetoneLevel->blockSignals(block);
muteRxCheck->blockSignals(block);
specEnable->blockSignals(block);
gateEnable->blockSignals(block);
gateThreshold->blockSignals(block);
gateAttack->blockSignals(block);
gateHold->blockSignals(block);
gateDecay->blockSignals(block);
gateRange->blockSignals(block);
gateLfCutoff->blockSignals(block);
gateHfCutoff->blockSignals(block);
}
void AudioProcessingWidget::populateFromPrefs(const audioProcessingPrefs& p)
{
bypassCheck->setChecked(p.bypass);
setProcessingControlsEnabled(!p.bypass);
eqEnable->setChecked(p.eqEnabled);
compEnable->setChecked(p.compEnabled);
orderCombo->setCurrentIndex(p.eqFirst ? 0 : 1);
reorderDspGroups(p.eqFirst);
for (int i = 0; i < EQ_BANDS; ++i) {
eqSliders[i]->setValue(static_cast<int>(std::round(p.eqBands[i] * 10.0f)));
eqValues[i]->setText(QString::number(p.eqBands[i], 'f', 1));
}
compPeakLimit->setValue(static_cast<int>(std::round(p.compPeakLimit * 10.0f)));
compRelease->setValue(static_cast<int>(std::round(p.compRelease * 100.0f)));
compFastRatio->setValue(static_cast<int>(std::round((1-p.compFastRatio) * (100.0f))));
compSlowRatio->setValue(static_cast<int>(std::round((1-p.compSlowRatio) * (100.0f))));
inputGain->setValue(static_cast<int>(std::round(p.inputGainDB * 10.0f)));
outputGain->setValue(static_cast<int>(std::round(p.outputGainDB * 10.0f)));
sidetoneEnable->setChecked(p.sidetoneEnabled);
sidetoneLevel->setValue(static_cast<int>(std::round(p.sidetoneLevel * 100.0f)));
muteRxCheck->setChecked(false);
specEnable->setChecked(p.spectrumEnabled);
specWidget->setVisible(p.spectrumEnabled);
m_spectrumFps = qBound(1, p.spectrumFPS, 60);
specWidget->setFps(m_spectrumFps);
// Update labels
lblPeak->setText(QString::number(p.compPeakLimit, 'f', 1) + " dB");
lblRelease->setText(QString::number(p.compRelease, 'f', 2) + " s");
{ int s = qRound((1-p.compFastRatio) * 100.0f); lblFast->setText(QString("%1:1").arg(1 + s * 99 / 100, 3)); }
{ int s = qRound((1-p.compSlowRatio) * 100.0f); lblSlow->setText(QString("%1:1").arg(1 + s * 99 / 100, 3)); }
lblInputGain->setText(QString::number(p.inputGainDB, 'f', 1) + " dB");
lblOutputGain->setText(QString::number(p.outputGainDB, 'f', 1) + " dB");
lblSidetone->setText(QString::number(static_cast<int>(p.sidetoneLevel * 100.0f)) + "%");
gateEnable->setChecked(p.gateEnabled);
gateThreshold->setValue(static_cast<int>(std::round(p.gateThreshold)));
gateAttack->setValue(static_cast<int>(std::round(p.gateAttack)));
gateHold->setValue(static_cast<int>(std::round(p.gateHold)));
gateDecay->setValue(static_cast<int>(std::round(p.gateDecay)));
gateRange->setValue(static_cast<int>(std::round(p.gateRange)));
gateLfCutoff->setValue(static_cast<int>(std::round(p.gateLfCutoff)));
gateHfCutoff->setValue(static_cast<int>(std::round(p.gateHfCutoff)));
lblGateThreshold->setText(QString::number(static_cast<int>(p.gateThreshold)) + " dB");
lblGateAttack->setText(QString::number(static_cast<int>(p.gateAttack)) + " ms");
lblGateHold->setText(QString::number(static_cast<int>(p.gateHold)) + " ms");
lblGateDecay->setText(QString::number(static_cast<int>(p.gateDecay)) + " ms");
lblGateRange->setText(QString::number(static_cast<int>(p.gateRange)) + " dB");
lblGateLfCutoff->setText(QString::number(static_cast<int>(p.gateLfCutoff)) + " Hz");
lblGateHfCutoff->setText(QString::number(static_cast<int>(p.gateHfCutoff)) + " Hz");
}