fix(rade): show mic level meter and enable gain slider during RADE mode#2292
fix(rade): show mic level meter and enable gain slider during RADE mode#2292ten9876 merged 2 commits intoten9876:mainfrom
Conversation
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]>
There was a problem hiding this comment.
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_radeModeandm_pcMicGainare bothstd::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 insideonTxAudioReady, same as the SSB path — no new race. - Queued
pcMicLevelChangedsignals delivered aftersetRadeMode(false)are correctly suppressed by the new!m_audio->isRadeMode()gate at MainWindow.cpp:3149 (whenmic_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:
- Snapshot
transmitModel().micLevel()inactivateRADEand restore it indeactivateRADEalongside thePcMicGainreset. - Suppress
m_model->setMicLevel(v)from the slider lambda whenm_radeActiveis 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
activateRADEthe order issetPcMicGain(...)→setRadeActive(true)→audioStartTx. ThesetRadeActive(true)call triggerssyncPhoneFromModel→ slidersetValue(pcGain)→valueChangedlambda emitsmicLevelChanged, which the MainWindow handler routes back intosetPcMicGainand rewritesPcMicGainto AppSettings. Idempotent and harmless, but a slightly noisy round-trip on every activation. - In
setRadeActive(false)(PhoneCwApplet.cpp), the explicitm_levelGauge->setValue(-150.0f); setPeakValue(-150.0f);is fine, butsyncPhoneFromModelis already called above — the gauge clear is the only bitsyncPhoneFromModeldoesn'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]>
|
Claude here on Jeremy's behalf — pushed the slider-gate fix as a follow-up commit ( The change: the slider's 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) |
ten9876
left a comment
There was a problem hiding this comment.
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!
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
PcMicGainvs. a separateRadeMicGainkey — 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::onTxAudioReadyat the int16→float32 conversion step, before the SSB client-side DSP chain runs. This meant:pcMicLevelChangedwas never emitted → P/CW gauge stayed blankm_pcMicGainwas never applied → Mic Level slider had no effectmet_in_rxguard inPhoneCwApplet::updateMeterswith no exemption for RADEChanges
src/core/AudioEngine.cppBefore the RADE early-return, apply
m_pcMicGainto the int16 buffer and run the same metering window accumulator used by the SSB path.pcMicLevelChangedis now emitted on every meter window during RADE capture. RADEEngine receives the post-gain float32 audio.src/gui/PhoneCwApplet.h/.cppAdd
setRadeActive(bool)slot. When active: refreshes the slider to displayPcMicGainfrom AppSettings (same as PC mic path); exempts the gauge from the!metInRx && !isTransmittingsuppression 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'smic_level.src/gui/MainWindow.cpppcMicLevelChangedVU ballistics handler: gate widened to pass through whenm_audio->isRadeMode()in addition tomic_selection=="PC".micLevelChangedhandler: also callssetPcMicGainand persistsPcMicGainwhen RADE is active (the radio's hardware mic input is unused in RADE TX).activateRADE: restoresPcMicGainfrom AppSettings and callssetRadeActive(true)on the applet beforeaudioStartTxso UI state is correct before any audio flows.deactivateRADE: callssetRadeActive(false)and resetssetPcMicGain(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:
m_pcMicPeak,m_pcMicSumSq,m_pcMicSampleCountare non-atomic but accessed exclusively on the audio thread viaonTxAudioReady; no race.isRadeMode()andsetPcMicGain()cross-thread calls usestd::atomicper the existing pattern in the codebase.setRadeMode(false)indeactivateRADE, anypcMicLevelChangedsignals still queued from the audio thread are suppressed by theisRadeMode()gate in the main-thread lambda before they reachupdateMeters.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 thesetRadeActivestate is isolated to the applet.Test plan
mic_levelfor hardware mic🤖 Generated with Claude Code