Skip to content

Commit 5d4dc34

Browse files
on8stclauderfoust
authored
Show and correctly clear the ALC Gain meter (#5636). Principle XI.
Show the Phone-panel ALC Gain meter only when the radio defines it. Clear stale painted readings immediately, register the rendered surface, and notify meter withdrawal after routing cleanup. Add mutation-checked lifecycle and registry coverage. Operator rfoust authorized the visual-design override and squash merge. Co-authored-by: on8st <258096273+on8st@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Robbie Foust <rfoust@gmail.com>
1 parent 06e45cf commit 5d4dc34

11 files changed

Lines changed: 327 additions & 31 deletions

docs/CERTIFICATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ need:
974974
|---|---|
975975
| `TX:FWDPWR` / `TX:REFPWR` defined but never fed | two power meters that can never move; publish with a documented scale or stop defining them |
976976
| `SliceModel::setRfGain` has no runtime path | LNA gain is connect-parameters only; the preamp control does nothing after connect |
977-
| ~~`TX:ALC` computed and discarded~~ | **Producer fixed; operator-visible half remains #5636** (not yet verified on a real radio) — `Hl2TxDsp::alcGain` now feeds a meter of its own, `TX:ALCGAIN` (dB), through `MeterModel`, radiocert and the automation bridge; no GUI surface renders it yet. It was NOT a repoint of `TX:ALC`: that key is a post-ALC LEVEL consumed by `MeterModel::swAlc()`, both ALC gauges, the certification table and the bridge, and changing what a published key means breaks all of them silently. The two are now separate quantities, as they are in WDSP (`TXA_ALC_PK` and `TXA_ALC_GAIN`) |
977+
| ~~`TX:ALC` computed and discarded~~ | **Producer and Phone-panel display implemented (#5506, #5636)** (not yet verified on a real radio) — `Hl2TxDsp::alcGain` now feeds a meter of its own, `TX:ALCGAIN` (dB), through `MeterModel`, radiocert and the automation bridge; the Phone panel renders ALC Gain when the meter is defined, with immediate clearing on invalidation. It was NOT a repoint of `TX:ALC`: that key is a post-ALC LEVEL consumed by `MeterModel::swAlc()`, both ALC gauges, the certification table and the bridge, and changing what a published key means breaks all of them silently. The two are now separate quantities, as they are in WDSP (`TXA_ALC_PK` and `TXA_ALC_GAIN`) |
978978
| Tune power not separable from RF power | TUNE keys at full drive on a fresh connect |
979979
| `RTTY` unmapped | silently demodulated as USB; conventionally lower-sideband on HF, so it wants a decision rather than a default |
980980
| Sideband stage saturates | even at 5 % drive into a dummy load a few inches away; needs inline attenuation or a second receiver |

src/core/MeterSurfaces.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ inline constexpr std::array<MeterSurface, 10> kMeterSurfaces{{
102102
// nothing — it reports the ALC's success, not their input level." This is
103103
// the number that answers "is the ALC holding, and by how much". This PR
104104
// publishes that value through MeterModel and the diagnostic surfaces;
105-
// the operator-facing gauge is deliberately a separate change (#5636).
105+
// the Phone panel renders it when the radio declares the meter (#5636).
106106
//
107107
// dB ONLY, deliberately, where TX:ALC accepts dBFS or Percent. That set has
108108
// two members because an Icom reports its ALC level as a percentage of its
@@ -112,7 +112,7 @@ inline constexpr std::array<MeterSurface, 10> kMeterSurfaces{{
112112
// same line — `TXA_ALC_PK` and `TXA_ALC_GAIN` are separate entries in
113113
// `txaMeterType` in `third_party/wdsp/upstream/TXA.h`.
114114
{"TX:ALCGAIN", "dB", "MeterModel::alcGainChanged / alcGainDb()",
115-
"No GUI surface (producer-only; proposed Phone panel gauge is #5636)", false},
115+
"Phone applet ALC Gain gauge (when the meter is defined)", true},
116116

117117
{"TX:COMPPEAK", "dB", "MeterModel::compressionChanged",
118118
"Phone/CW applet Compression gauge", true},

src/gui/MainWindow.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7551,6 +7551,15 @@ void MainWindow::applyCapabilitiesToUi(bool connected, const RadioCapabilities&
75517551
cwCaps.cwPitchMinHz, cwCaps.cwPitchMaxHz, cwCaps.cwPitchStepHz);
75527552
m_appletPanel->phoneCwApplet()->setHasAudioPeakingFilter(
75537553
m_radioModel.hasAudioPeakingFilter());
7554+
// The ALC Gain gauge, on the meter EXISTING rather than on a
7555+
// capability flag: only the HL2 publishes TX:ALCGAIN, and the Phone
7556+
// panel is shared. `connected &&` rather than the permissive
7557+
// `!connected ||` used above — a disconnected panel must be the panel
7558+
// that shipped before this gauge, and the disconnect edge is what
7559+
// takes the row back down after an HL2 session so the next Flex
7560+
// connect does not inherit it.
7561+
m_appletPanel->phoneCwApplet()->setHasAlcGainMeter(
7562+
connected && m_radioModel.meterModel().hasAlcGainMeter());
75547563
}
75557564

75567565
// ── The 8-band graphic EQ ───────────────────────────────────────────────

src/gui/MainWindow_DspApplets.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,55 @@ void MainWindow::wireDspApplets()
266266
m_appletPanel->phoneCwApplet()->resetAlc();
267267
}
268268
});
269+
// The ALC's applied GAIN, beside the ALC's output level above. Gated the
270+
// same way and additionally on the model having a SAMPLE: unity gain and
271+
// "nothing has said what the ALC is doing" are the same 0 dB on the face,
272+
// so without hasAlcGainValue() a cleared meter would render as a confident
273+
// "the ALC is holding at unity" — the fabricated-reading failure §1.8
274+
// describes, where a dead meter and a real reading of nothing look alike.
275+
// WHETHER THE ROW EXISTS IS A DEFINITION QUESTION, and it is answered on
276+
// the definition signals — never on the arrival of a value.
277+
//
278+
// It was answered inside the alcGainChanged handler below, and that is a
279+
// value signal: submitTxAudio() returns early on !m_keyed, so alcGain never
280+
// fires outside TX. The gauge was therefore absent during exactly the
281+
// pre-transmission mic-gain setup it exists to inform, inserted itself into
282+
// the panel mid-over, and in CW-only operation never appeared at all. The
283+
// regime the meter earns its place in is the quiet-mic one — the ALC hits
284+
// its makeup ceiling near -41.4 dBFS, below the Level gauge's -40 floor, so
285+
// both existing gauges are pinned there and only this one can show the
286+
// difference. Gating on a value hid it precisely there.
287+
//
288+
// applyCapabilitiesToUi() alone cannot cover it either: Hl2Backend calls
289+
// defineMeters() from inside its own connected handler, AFTER RadioModel
290+
// has published capabilities, so at the moment the panel is configured the
291+
// meter does not exist yet. These three are the model's own account of
292+
// which meters exist -- defined, removed, and the disconnect that drops
293+
// them all (clear() emits only metersCleared, never a meterRemoved per
294+
// index). Idempotent: setHasAlcGainMeter() early-returns on no change.
295+
{
296+
auto syncAlcGainRow = [this] {
297+
m_appletPanel->phoneCwApplet()->setHasAlcGainMeter(
298+
m_radioModel.meterModel().hasAlcGainMeter());
299+
};
300+
connect(&m_radioModel.meterModel(), &MeterModel::meterDefinitionChanged,
301+
this, [syncAlcGainRow](int) { syncAlcGainRow(); });
302+
connect(&m_radioModel.meterModel(), &MeterModel::meterRemoved,
303+
this, [syncAlcGainRow](int) { syncAlcGainRow(); });
304+
connect(&m_radioModel.meterModel(), &MeterModel::metersCleared,
305+
this, syncAlcGainRow);
306+
}
307+
connect(&m_radioModel.meterModel(), &MeterModel::alcGainChanged,
308+
this, [this](float gainDb) {
309+
// READING ONLY. Presence is settled above, on the definition signals.
310+
const bool live = m_radioModel.isRadioTransmitting()
311+
&& m_radioModel.meterModel().hasAlcGainValue();
312+
if (live) {
313+
m_appletPanel->phoneCwApplet()->updateAlcGain(gainDb);
314+
} else {
315+
m_appletPanel->phoneCwApplet()->resetAlcGain();
316+
}
317+
});
269318
// Client-side PC mic metering — radio CODEC meters only see hardware mics.
270319
// Apply VU-style ballistics: fast attack, slow decay (~20 dB/sec).
271320
{

src/gui/MainWindow_Session.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,10 @@ void MainWindow::wireRadioModel()
13731373
if (!tx) {
13741374
m_appletPanel->phoneCwApplet()->updateCompression(0.0f);
13751375
m_appletPanel->phoneCwApplet()->resetAlc();
1376+
// Same reason as resetAlc: the last gain the ALC applied describes
1377+
// a transmission that has ended, and left on the face it reads as
1378+
// the gain being applied now.
1379+
m_appletPanel->phoneCwApplet()->resetAlcGain();
13761380
}
13771381
if (tx) {
13781382
AetherSDR::ThemeManager::instance().applyStyleSheet(m_txIndicator, "QLabel { color: white; background: {{color.accent.danger}}; font-weight: bold; "

src/gui/PhoneCwApplet.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929

3030
namespace AetherSDR {
3131

32+
// The ALC Gain gauge's face, named because two places need the SAME numbers
33+
// and one of them is "no reading".
34+
//
35+
// Not a display preference: the top is the HL2 modulator's makeup ceiling
36+
// (Hl2TxDsp::Config::alcMaxGainDb), so a reading pressed against it means the
37+
// ALC has run out of gain rather than that the face has run out of scale.
38+
// -20 covers the reductions this chain produces.
39+
static constexpr float kAlcGainGaugeMinDb = -20.0f;
40+
static constexpr float kAlcGainGaugeMaxDb = 40.0f;
41+
3242
// ── Triangle button (same as RxApplet) ──────────────────────────────────────
3343

3444
class CwTriBtn : public QPushButton {
@@ -267,6 +277,51 @@ void PhoneCwApplet::buildPhonePanel()
267277
m_compGauge->setHoverValuePopupEnabled(true);
268278
vbox->addWidget(m_compGauge);
269279

280+
// ── ALC Gain gauge (dB: -20 to +40) ──────────────────────────────────
281+
// Beside Compression because they answer the same kind of question — how
282+
// much is the chain changing my audio — where the ALC gauge below answers
283+
// where the audio ended up. The two are easily confused and the difference
284+
// is the whole reason this one exists: a post-ALC level meter sits pinned
285+
// near its target by construction, so an operator whose microphone is 30 dB
286+
// too quiet sees an ALC gauge that looks perfect.
287+
//
288+
// The range is the modulator's, not a preference: +40 dB is the HL2
289+
// modulator's makeup ceiling (Hl2TxDsp::Config::alcMaxGainDb), so a reading
290+
// at the top means the ALC has run out of gain rather than that the face
291+
// has run out of scale. -20 covers the reductions this chain produces.
292+
//
293+
// The colour breaks are the modulator's too, not taste. The face's top is
294+
// alcMaxGainDb, so yellow at +20 is "half the makeup is spent" and red at
295+
// +30 is "three quarters of it is, and the last 10 dB is all that stands
296+
// between this microphone and an ALC that cannot reach its target" --
297+
// which is a setup fault to fix at the gain control, not in software. The
298+
// reduction half is deliberately uncoloured: the ALC taking level away is
299+
// it working, at any depth this chain produces.
300+
m_alcGainGauge = new HGauge(kAlcGainGaugeMinDb, kAlcGainGaugeMaxDb, 30.0f,
301+
"ALC Gain", "dB",
302+
{{-20, "-20dB"}, {-10, "-10"}, {0, "0"}, {10, "+10"}, {20, "+20"},
303+
{30, "+30"}, {40, "+40"}}, nullptr, 20.0f);
304+
m_alcGainGauge->setObjectName(QStringLiteral("phoneAlcGainGauge"));
305+
// The floor, not 0: see resetAlcGain(). A gauge built hidden must not be
306+
// holding a third-full bar for the moment it is revealed.
307+
m_alcGainGauge->setValueImmediate(kAlcGainGaugeMinDb);
308+
m_alcGainGauge->setAccessibleName("ALC gain gauge");
309+
m_alcGainGauge->setAccessibleDescription(
310+
"Gain the transmit ALC is applying, in dB; 0 is unity");
311+
m_alcGainGauge->setHoverValueFormatter([](float v) {
312+
// Signed, unlike Compression's face below, because both directions are
313+
// real here: the ALC both adds makeup and takes level away.
314+
return QStringLiteral("%1%2 dB")
315+
.arg(v > 0.0f ? QStringLiteral("+") : QString())
316+
.arg(QString::number(v, 'f', 1));
317+
});
318+
m_alcGainGauge->setHoverValuePopupEnabled(true);
319+
// Built hidden. The Phone panel is shared with Flex, Icom and the sim,
320+
// none of which publish an ALCGAIN meter, and a row they cannot drive is
321+
// a change to their panel. setHasAlcGainMeter() is what reveals it.
322+
m_alcGainGauge->setVisible(m_hasAlcGainMeter);
323+
vbox->addWidget(m_alcGainGauge);
324+
270325
// ── ALC gauge (post-SW-ALC SSB-peak, dBFS) ──────────────────────────
271326
// Mirrored in m_cwPanel; both gauges read from MeterModel::alcValueChanged
272327
// so SSB operators watching mic gain see the same indicator CW
@@ -1358,6 +1413,54 @@ void PhoneCwApplet::updateCompression(float compPeak)
13581413
m_compGauge->setValue(-compressionDb);
13591414
}
13601415

1416+
void PhoneCwApplet::updateAlcGain(float gainDb)
1417+
{
1418+
if (!m_alcGainGauge) {
1419+
return;
1420+
}
1421+
// No clamp here: HGauge clamps to its own range, and clamping twice would
1422+
// hide the case worth seeing — a gain pressed against the modulator's
1423+
// ceiling, which reads as "the ALC has nothing left" rather than as a
1424+
// meter at the end of its travel.
1425+
m_alcGainGauge->setValue(gainDb);
1426+
}
1427+
1428+
void PhoneCwApplet::resetAlcGain()
1429+
{
1430+
if (!m_alcGainGauge) {
1431+
return;
1432+
}
1433+
// THE FACE FLOOR, NOT ZERO, and the distinction is the whole point of
1434+
// hasAlcGainValue(). This face runs -20..+40, so HGauge renders 0 dB as a
1435+
// bar one third full -- and 0 dB is also a real reading, "the ALC is
1436+
// holding at unity". Driving to 0 on unkey, on disconnect and as the first
1437+
// thing shown after a TX-slice change therefore painted a confident
1438+
// measurement in the one state where nothing has been measured. resetAlc()
1439+
// beside it drives to its own floor for the same reason; an empty bar is
1440+
// the only rendering of "no reading" this widget has.
1441+
m_alcGainGauge->setValueImmediate(kAlcGainGaugeMinDb);
1442+
m_alcGainGauge->clearPeak();
1443+
}
1444+
1445+
void PhoneCwApplet::setHasAlcGainMeter(bool has)
1446+
{
1447+
if (m_hasAlcGainMeter == has) {
1448+
return;
1449+
}
1450+
m_hasAlcGainMeter = has;
1451+
if (!m_alcGainGauge) {
1452+
return;
1453+
}
1454+
// Discard the departing radio's last gain before hiding, for the same
1455+
// reason setMicLevelMeterState() resets the Level gauge at the lifecycle
1456+
// boundary: a reading kept behind a hidden widget comes back as the next
1457+
// radio's when the gauge is shown again.
1458+
if (!has) {
1459+
resetAlcGain();
1460+
}
1461+
m_alcGainGauge->setVisible(has);
1462+
}
1463+
13611464
void PhoneCwApplet::setAlcMeterUnit(const QString& unit)
13621465
{
13631466
if (unit == m_alcMeterUnit) {

src/gui/PhoneCwApplet.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ class PhoneCwApplet : public QWidget {
6161
// on a Flex unplug.
6262
void setHasAudioPeakingFilter(bool has);
6363

64+
// Does THIS session's radio publish an ALCGAIN meter? Gates the ALC Gain
65+
// gauge, which is otherwise a face that can never move: the HL2 is the
66+
// only family in the tree that publishes the meter, and the shared Phone
67+
// panel is also Flex's, Icom's and the sim's. Pushed from
68+
// MainWindow::applyCapabilitiesToUi off MeterModel::hasAlcGainMeter(),
69+
// and again on definition changes, because meters are DEFINED after
70+
// capabilities are published on the connect edge.
71+
//
72+
// NOT permissive while disconnected, unlike the APF row above: this gauge
73+
// is new, and "no radio attached" must render as the panel that shipped
74+
// before it rather than as a gauge nothing can drive.
75+
void setHasAlcGainMeter(bool has);
76+
6477
signals:
6578
void micLevelChanged(int level); // slider value 0-100
6679

@@ -79,6 +92,17 @@ public slots:
7992
float micPeak, float compPeak);
8093
void updateCompression(float compPeak);
8194

95+
// The gain the transmitter's ALC is applying, in dB (0 = unity). A
96+
// different reading from updateAlc() below, not a second scaling of it:
97+
// that one is the post-ALC LEVEL, which sits near the ALC's target however
98+
// the operator has set their gain, and this is how hard the stage is
99+
// working to put it there — the half that answers "is the ALC holding, and
100+
// by how much" when a transmission goes out quiet.
101+
void updateAlcGain(float gainDb);
102+
// Clear immediately to the face floor on unkey, disconnect or invalidation.
103+
// A missing reading must not animate through apparently measured gains.
104+
void resetAlcGain();
105+
82106
// Notify the applet when RADE mode activates/deactivates so the mic level
83107
// slider and meter behave correctly (client-side gain + RX metering).
84108
void setRadeActive(bool on);
@@ -119,6 +143,16 @@ public slots:
119143
MicMeterSessionState m_micLevelMeterSession{MicMeterSessionState::Disconnected};
120144
bool m_micLevelMeterAvailable{true};
121145
HGauge* m_compGauge{nullptr};
146+
// Phone panel only, beside Compression — the two gauges that report what
147+
// the transmit chain is DOING to the operator's audio, as opposed to the
148+
// Level and ALC gauges above and below them, which report levels. Not
149+
// mirrored onto the CW panel the way the ALC gauge is: the remedy this
150+
// meter points at is the mic slider, which is a Phone control.
151+
HGauge* m_alcGainGauge{nullptr};
152+
// Hidden until a radio says it publishes the meter — see
153+
// setHasAlcGainMeter(). False by default so the gauge is built hidden and
154+
// a family that never publishes ALCGAIN gets the panel it had before.
155+
bool m_hasAlcGainMeter{false};
122156

123157
QComboBox* m_micProfileCombo{nullptr};
124158

src/models/MeterModel.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ void MeterModel::removeMeter(int index)
248248
m_defs.remove(index);
249249
m_values.remove(index);
250250
m_valueUpdatedMs.remove(index);
251-
emit meterRemoved(index);
252251

253252
const auto matchesIndex = [index](QMap<int, int>::iterator entry) {
254253
return entry.value() == index;
@@ -329,6 +328,9 @@ void MeterModel::removeMeter(int index)
329328
clearCompressionState();
330329
logCompressionSummary("meter-removed", true);
331330
}
331+
// Presence subscribers query the routing maps synchronously. Notify only
332+
// after the withdrawn meter has been removed from every index map.
333+
emit meterRemoved(index);
332334
}
333335

334336
float MeterModel::convertRaw(const MeterDef& def, qint16 raw) const

0 commit comments

Comments
 (0)