Skip to content

fix(rade): show mic level meter and enable gain slider during RADE mode#2292

Merged
ten9876 merged 2 commits intoten9876:mainfrom
NF0T:fix/rade-mic-level-meter
May 3, 2026
Merged

fix(rade): show mic level meter and enable gain slider during RADE mode#2292
ten9876 merged 2 commits intoten9876:mainfrom
NF0T:fix/rade-mic-level-meter

Conversation

@NF0T
Copy link
Copy Markdown
Contributor

@NF0T NF0T commented May 2, 2026

Summary

Closes #2283.

The fix follows the approach proposed in the aethersdr-agent triage comment on #2283. That comment traced the full root cause and outlined a 4-step fix across 3 files; this PR implements it as described. The one open design question from that triage — shared PcMicGain vs. a separate RadeMicGain key — is resolved here by sharing, per the agent's recommendation (RADE and PC SSB never run simultaneously on the same TX slice).

Filed as draft pending maintainer confirmation of that design decision.

Root cause

RADE TX takes an early return in AudioEngine::onTxAudioReady at the int16→float32 conversion step, before the SSB client-side DSP chain runs. This meant:

  • pcMicLevelChanged was never emitted → P/CW gauge stayed blank
  • m_pcMicGain was never applied → Mic Level slider had no effect
  • The gauge was suppressed during RX by the met_in_rx guard in PhoneCwApplet::updateMeters with no exemption for RADE

Changes

src/core/AudioEngine.cpp
Before the RADE early-return, apply m_pcMicGain to the int16 buffer and run the same metering window accumulator used by the SSB path. pcMicLevelChanged is now emitted on every meter window during RADE capture. RADEEngine receives the post-gain float32 audio.

src/gui/PhoneCwApplet.h/.cpp
Add setRadeActive(bool) slot. When active: refreshes the slider to display PcMicGain from AppSettings (same as PC mic path); exempts the gauge from the !metInRx && !isTransmitting suppression so the level shows during RX (matching the "Level Meter During Receive" behaviour requested in #2283). On deactivation: clears the gauge and restores the slider to the radio's mic_level.

src/gui/MainWindow.cpp

  • pcMicLevelChanged VU ballistics handler: gate widened to pass through when m_audio->isRadeMode() in addition to mic_selection=="PC".
  • Mic level slider micLevelChanged handler: also calls setPcMicGain and persists PcMicGain when RADE is active (the radio's hardware mic input is unused in RADE TX).
  • activateRADE: restores PcMicGain from AppSettings and calls setRadeActive(true) on the applet before audioStartTx so UI state is correct before any audio flows.
  • deactivateRADE: calls setRadeActive(false) and resets setPcMicGain(100) for hardware mic paths so no RADE-adjusted gain leaks into subsequent SSB sessions.

Code audit notes

Pre-submission audit performed on thread safety, coding style, and regression risk:

  • Thread safetym_pcMicPeak, m_pcMicSumSq, m_pcMicSampleCount are non-atomic but accessed exclusively on the audio thread via onTxAudioReady; no race. isRadeMode() and setPcMicGain() cross-thread calls use std::atomic per the existing pattern in the codebase.
  • Queued signal drain — after setRadeMode(false) in deactivateRADE, any pcMicLevelChanged signals still queued from the audio thread are suppressed by the isRadeMode() gate in the main-thread lambda before they reach updateMeters.
  • Style — braces added to all new control flow per project guidelines; a misleading comment ("float→int16 path") corrected to "int16 gain path".
  • No new compiler warnings — verified against the full build; all existing warnings are pre-existing upstream issues.

Risk

Additive only on the RADE TX path. All SSB/CW/DIG behaviour is unchanged — the new metering and gain code is inside the if (m_radeMode) block and the setRadeActive state is isolated to the applet.

Test plan

  • RADE active, hardware mic: P/CW gauge shows mic level during RX (no TX needed)
  • RADE active: Mic Level slider adjusts level visually on gauge
  • RADE active: Mic Level slider value persists across RADE deactivate/reactivate
  • RADE deactivated: gauge clears; slider reverts to radio's mic_level for hardware mic
  • PC mic (non-RADE): gauge and slider behaviour unchanged
  • SSB/USB/LSB mode (non-RADE): no regression in metering or gain

🤖 Generated with Claude Code

RADE TX bypasses the SSB mic-level path entirely (early-return at the
float32 conversion step), so the P/CW gauge was always blank and the
mic level slider had no effect during RADE sessions.

AudioEngine: apply PcMicGain and emit pcMicLevelChanged before the RADE
early-return so the post-gain level is metered and fed to RADEEngine.

MainWindow: widen the pcMicLevelChanged→gauge gate to pass through when
RADE is active (previously gated on mic_selection=="PC" only); widen the
mic level slider handler to call setPcMicGain and persist PcMicGain when
RADE is active regardless of mic_selection (radio mic input is unused in
RADE TX). Restore PcMicGain on RADE activation; reset to 100 on
deactivation for hardware mic paths.

PhoneCwApplet: add setRadeActive(bool) slot called from activateRADE /
deactivateRADE; exempt RADE from the met_in_rx suppression gate so the
gauge shows during RX (matches SmartSDR "Level Meter During Receive"
behaviour); refresh slider to show PcMicGain in RADE mode. All changes
are additive on the RADE path — SSB/CW behaviour is unchanged.

Fixes ten9876#2283

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@NF0T NF0T requested review from jensenpat and ten9876 as code owners May 2, 2026 20:38
Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful work, @NF0T — the linked triage write-up made this very easy to follow, and the change mirrors the SSB metering accumulator exactly so the new RADE path stays consistent with the rest of onTxAudioReady. Sharing PcMicGain is the right call given RADE and PC SSB don't run on the same TX slice simultaneously.

Audit notes confirm what I checked:

  • m_radeMode and m_pcMicGain are both std::atomic (AudioEngine.h:464-465), so the cross-thread reads from the GUI lambda are safe.
  • The new accumulator state (m_pcMicPeak, m_pcMicSumSq, m_pcMicSampleCount) lives only on the audio thread inside onTxAudioReady, same as the SSB path — no new race.
  • Queued pcMicLevelChanged signals delivered after setRadeMode(false) are correctly suppressed by the new !m_audio->isRadeMode() gate at MainWindow.cpp:3149 (when mic_selection != "PC").

One substantive concern worth thinking through:

Slider movements during RADE still mutate the radio's hardware mic_level. The slider's valueChanged handler in PhoneCwApplet.cpp:203-208 unconditionally calls m_model->setMicLevel(v), which sends transmit set miclevel=N to the radio (TransmitModel.cpp:488-493). So if a user is in RADE with a hardware mic selected (say MIC at level 80), drops the slider to 40 to tame their RADE input, then deactivates RADE, their pre-RADE hardware level is gone — the radio is now at 40. The new setPcMicGain(100) reset in deactivateRADE only restores the client gain, not the radio's hardware setting that the slider clobbered along the way.

This is technically a pre-existing artifact of the PC-mic slider path (same code already calls setMicLevel for PC, where the radio just ignores it), but RADE-with-hardware-mic is the first case where it has visible consequences. Two options:

  1. Snapshot transmitModel().micLevel() in activateRADE and restore it in deactivateRADE alongside the PcMicGain reset.
  2. Suppress m_model->setMicLevel(v) from the slider lambda when m_radeActive is true — symmetric with how PC mode is already client-authoritative.

(2) feels cleaner and matches the PR's stated invariant that "the radio's mic input is unused in RADE TX." Either fix is fine; just worth not shipping the silent overwrite.

Two minor observations, not blocking:

  • In activateRADE the order is setPcMicGain(...)setRadeActive(true)audioStartTx. The setRadeActive(true) call triggers syncPhoneFromModel → slider setValue(pcGain)valueChanged lambda emits micLevelChanged, which the MainWindow handler routes back into setPcMicGain and rewrites PcMicGain to AppSettings. Idempotent and harmless, but a slightly noisy round-trip on every activation.
  • In setRadeActive(false) (PhoneCwApplet.cpp), the explicit m_levelGauge->setValue(-150.0f); setPeakValue(-150.0f); is fine, but syncPhoneFromModel is already called above — the gauge clear is the only bit syncPhoneFromModel doesn't cover, which the comment could note. Trivial.

Otherwise this looks good: scope is tight, the metering math is identical to SSB, and the behaviour matches what #2283 asked for.

The mic-level slider's valueChanged lambda unconditionally calls
m_model->setMicLevel(v), which sends transmit set miclevel=N to the
radio.  For PC mic the radio ignores it, so it's a no-op in practice.
For RADE-with-hardware-mic it silently clobbers the user's hardware
mic_level — they activate RADE, drag the slider to tame their RADE
input, and on deactivation the radio is left at the dragged-to value
instead of their pre-RADE setting.

Gate on !m_radeActive so the slider stays purely client-side
(PcMicGain) during RADE, matching the PR's stated invariant that
"the radio's mic input is unused in RADE TX."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@ten9876
Copy link
Copy Markdown
Owner

ten9876 commented May 3, 2026

Claude here on Jeremy's behalf — pushed the slider-gate fix as a follow-up commit (7483f77) so the PR is ready to merge as-is.

The change: the slider's valueChanged lambda now skips m_model->setMicLevel(v) when m_radeActive is true, so during RADE the slider stays purely client-side (PcMicGain) without sending transmit set miclevel=N to the radio. PC mode is unaffected (radio ignores mic_level for PC anyway). One file, +7/−1.

Otherwise the PR was clean — RADE accumulator mirrors the SSB path exactly, atomics on the cross-thread reads, queued-signal drain handled correctly, scope tight. Thanks for the careful triage write-up; it made the review trivial. 73, Jeremy KK7GWY & Claude (AI dev partner)

Copy link
Copy Markdown
Owner

@ten9876 ten9876 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the slider clobber bug on current main and traced the fix path. NF0T's RADE accumulator mirrors the SSB path exactly; thread safety, queued-signal drain, and convention checks all clean. Pushed the slider-gate follow-up (7483f77) to round it out. Thanks for the careful triage write-up @NF0T!

@ten9876 ten9876 merged commit 245e8cd into ten9876:main May 3, 2026
5 checks passed
@NF0T NF0T deleted the fix/rade-mic-level-meter branch May 3, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FreeDV Outgoing Mic Audio Level Controling

2 participants