// 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 0–20 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 0–255 (our updateGainReduction maps 0–20 dB → 0–255).
        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");
}
