aethersdr / aethersdr/AetherSDR

CW weak-signal workflow: APF level slider is live while APF is off, plus a CW improvement roadmap from an operator field report

Open
#4,658 2 comments 0 reactions 1 assignee View on GitHub

@skerker is already working on this.

Since Aug 22, 2026.

bug CW enhancement external devices GUI maintainer-review
Dominant language
C++
Stars
221
Forks
117
Avg merge
2d 7h
Merged PRs (30d)
299

Description

Summary

An operator field report on weak-signal CW work with a FLEX-8400 surfaced one
concrete GUI bug and a cluster of CW workflow gaps. This issue records the
report, the code analysis behind it, and a proposed set of actions.

The bug: in CW mode the APF level slider is on screen and live whether or
not the APF filter is engaged. Dragging it sends apf_level to the radio, the
value label updates, and nothing reaches the audio. Every other DSP parameter
in the app is unreachable until its filter is on. APF should follow the same
rule.


The operator report

Andrew O'Brien posted a detailed writeup of digging weak CW out of the noise
with a FLEX-8400 and AetherSDR, after going back through FlexRadio's videos by
Tim Ellison and working through the app control by control. Recounted here in
full because the value is in where an experienced operator got stuck, not
just in the conclusions.

The method they arrived at:

  • Preamp first. Compare the RX meter reading in dB with no antenna
    connected, then with the antenna connected. Lower the preamp if the delta is
    greater than 8–10 dB, raise it if less. (Tim Ellison's habit; Mike Walker
    echoes it in other videos.)
  • Start with all NR off.
  • T-AGC is the single most important factor for reception, adjusted
    regularly to band conditions. Andrew adds: "I wonder if that old T-AGC line
    that I used to see in older versions of SMARTSDR has been removed? I don't
    see it in AetherSDR."
  • Pitch matters more than zero-beating. "I notice the pitch of the current
    CW signal I am tuned to. I would often just zero beat the signal but there is
    a bit more to it, and I have not quite figured out the 'loop' option in
    AetherSDR. A mouse hover over the 'PITCH' option seems to imply this is for
    setting decoder search range but I am wondering if it also impacts the LOOP
    option? For my old digital mode brain, the LOOP option seems to work a bit
    like the AFC option in software like Zakanaka, Fldigi, Multipsk or MixW. Not
    exactly, but thinking of it this way helps me."
  • Matching the received station's pitch (as reported by AetherSDR) to the
    operator's established CW pitch makes the signal pop out.
  • And then the part this issue exists for: "Then, the stupid part of my
    operations, I have to turn APF ON!! I was using the APF slider and saying it
    does not work but eventually clicked on APF in DSP between NB and NRL and the
    slider began to make a real difference."
  • NR helps. NR2 in the ADSP area helps. Both on at once "sometimes has made a
    big difference to weak CW with typical noise floor noise but certainly not
    always."

Their resulting checklist, verbatim:

  1. Establish band conditions with no antenna and note dB reading. Then with
    antenna connected do the same and then set Preamp slider as described above.
  2. Establish T-AGC settings with slider (under RX Controls applet in AetherSDR)
    as per usual FlexRadio's recommendations. For CW set AGC to "fast".
  3. With no NR engaged, tune to signal. Match pitch, use "auto tune" if needed.
    Use narrow CW filter, often 100 Hz. Matching pitch, narrow filter and steps
    1 and 2 above will much improve weak signal noisy copy.
  4. Under DSP click on APF, it will then be green in AetherSDR. Use the slider.
    Noise is significantly reduced as the slider moves left to right.
  5. Engage NR, if desired/needed.
  6. If really weak and down in the noise, try tweaking the pitch up and down a
    small amount. Sometimes that slight tweak may make a difference to your
    ears/brain. Enough to dig out a callsign and RST.
  7. If your brain tells you that ultra weak CW station is P5DX, don't believe
    it! 🙂
Comment thread
  • Dan Trainor: "You have nicely and correctly (imo) described the 'secret'
    to maximizing S/R, especially for CW weak signals. From Pre-amp all the way
    to speaker. You should bottle this and sell it — these words are that
    valuable! And also the pre-amp / pan-adapter noise floor bug (reversed) you
    found in AetherSDR is a really good catch."
  • Paul Ecker: "Really helpful information that will help us all… I think
    making progress in noise reduction in the CW mode is necessary."
  • Andrew, follow-up: reports that an AI assistant reviewed their noise-floor
    observations and concluded the displayed floor "is jumping up by exactly
    8 dB when you engage the -8 dB attenuator"
    , diagnosing a "Software Gain
    Miscalculation"
    in which AetherSDR "is applying the offset backward."
  • Andrew, second follow-up: after comparing against SmartSDR for macOS,
    reports SmartSDR reading +8 dB preamp → -118 dBm, 0 dB preamp → -112 dBm,
    -8 dB attenuator → -125 dBm, versus AetherSDR dropping to -120 dBm when gain
    was added and climbing to -107 dBm when attenuation was added. The assistant's
    verdict: AetherSDR has "an inverted multiplier tracking the FlexRadio API
    gain states"
    and operators should "rely on SmartSDR for Mac OS until the
    AetherSDR rendering wrapper is patched."

What they actually hit

1. The APF level slider is live while APF is off — this is the bug

m_apfContainer is built in a plain scope block at
VfoWidget.cpp:2024-2056,
starts hidden, and after that its visibility is set in exactly two places:

Both key off the mode. Neither reads apfOn(). So in CW mode with the DSP
tab open, the slider is mounted whether or not the filter is engaged, and its
handler at VfoWidget.cpp:2049-2052
unconditionally calls setApfLevel()slice set N apf_level=…. The radio
accepts the value and stores it against a filter that isn't in circuit.

This is inconsistent with the rest of the DSP row: every other DSP parameter
lives in the right-click DspParamPopup, which you can only meaningfully
summon on an engaged filter. APF is the only DSP with a permanently-mounted
inline level slider in the DSP tab — and it's the exception in the one mode
where the payoff is highest.

Expected: the APF slider behaves like every other DSP parameter control —
not reachable until APF is on.

Fix: gate on both conditions, setVisible(isCw && m_slice->apfOn()), and
add the same call to the apfChanged handler already wired at
VfoWidget.cpp:4500 so the slider
appears and disappears with the button.

2. "LOOP" is our CW autotune, and nothing says so

The Autotune: Once / Loop row lives at
VfoWidget.cpp:5444-5493.
SmartSDR+ radios get Once/Loop; other radios get a client-side "Zero Beat"
button instead. Andrew's read — that Loop behaves like AFC in Fldigi — is
correct, and neither button carries a tooltip saying so.

3. Two unrelated controls are both called "pitch"

The one they hovered is the decoder search range at
PanadapterApplet.cpp:211-217
("Decoder pitch search range (Hz)"), which feeds ggmorse. The radio's actual CW
pitch is TransmitModel::cwPitch(), set in the P/CW applet. They are already
coupled internally — CwDecoder::setKnownParameters()
locks the decoder to the operator's pitch ±150 Hz — but the UI presents them as
two independent controls with no cross-reference, so assuming one drives the
other is entirely reasonable.

4. The T-AGC line genuinely doesn't exist here

SmartSDR draws an AGC-T threshold line on the panadapter. AetherSDR does not.
We do have a full AGC-T calibration engine
(AgcTCalibrator.h,
docs/agc-t-calibration-design.md)
— reachable only by right-clicking the AGC-T slider
(RxApplet.cpp:1007-1023).
One of our better CW features is behind an undiscoverable gesture, which is why
a skilled operator wrote a seven-step manual procedure instead of using it.

5. The noise-floor claim — symptom real, stated cause wrong

The "inverted multiplier tracking the FlexRadio API gain states" diagnosis is
not what the code does.
AetherSDR applies no gain offset to panadapter data at
all; preamp is stored as inert internal state that deliberately emits nothing
(PanadapterModel.h:138-143).
There is no multiplier to invert.

The symptom is still plausible, for a different reason. Two things collide on a
gain change:

  • SpectrumWidget::setRfGain() calls reacquireNoiseFloorLock()
    (SpectrumWidget.h:369-375).
    With auto noise floor on, a gain change re-baselines and re-levels the
    display: the trace stays pinned at the same screen position while the dBm
    axis slides under it. That alone reads as "the noise floor jumped."
  • The FLEX panadapter wire format is Y-pixel positions relative to the
    client-set min_dbm/max_dbm
    (PanadapterStream.h:79-83)
    — the dBm decode is entirely client-side. During the re-level handshake,
    in-flight frames decode against a scale that has already been superseded. The
    re-baseline smoothing is also asymmetric (upward shifts must persist ~16
    frames, downward adopt in 2), so the settled result depends on which
    direction the gain moved.

This is a hypothesis, not a confirmed diagnosis — it has not been tested on
hardware.
The clean test: disable auto noise floor, pin min_dbm/max_dbm
manually, repeat the preamp sweep. A stable reading points at the auto-level
handshake. That test should happen before anyone accepts either explanation.


Proposed actions

Fix now
  1. APF slider gating (the bug above). Small, self-contained, and it turns
    "APF doesn't work" into "APF is great" for anyone who hits it the way Andrew
    did.
  2. Tooltips and names for the CW autotune row. "Autotune: Once / Loop" →
    wording that says Loop is continuous zero-beat tracking (AFC-like). Same for
    the "Zero Beat" variant on non-SmartSDR+ radios.
  3. Distinguish the two "pitch" controls. Rename the CW decode panel's
    control to something like "Decode search", and surface that it already
    follows the radio's CW pitch.
  4. Surface AGC-T calibration. Right-click-only is not discoverable. Add it
    to the RX Controls applet menu.
Build next
  1. CW tuning indicator — the data already exists. The decoder measures the
    received tone and we render it as dead text in a stats label
    (PanadapterApplet.cpp:777).
    Turn measured_pitch − target_pitch into a centering bar under the filter,
    highlighted when within ±10 Hz. This is Andrew's step 6 done for them, and
    it closes #1494 and #3486.
  2. Adaptive filter for CW. AdaptiveFilterEngine
    is hard-gated to USB/LSB. CW is a far easier target than SSB — one tone, at
    a pitch we already know — so occupied-bandwidth measurement collapses to "is
    there anything else inside 500 Hz." Auto-narrow 500 → 250 → 100 Hz, glide
    the edges, respect an operator floor. Reuses the existing per-frame pipeline
    and glide/dwell machinery.
  3. A CW noise-reduction profile. SpectralNR
    is WDSP emnr — Gaussian/Gamma speech estimators. That is the wrong
    statistical prior for a single keyed carrier, which is a good explanation for
    why Andrew finds NR + NR2 together helping "sometimes but certainly not
    always." A CW branch wants tone-tracking narrowband subtraction or a coherent
    enhancer locked to the pitch, preserving the keying envelope rather than
    gating on a speech model. Related: #632, #4357.
  4. AGC-T threshold line on the panadapter, draggable to set. This is the
    affordance Andrew misses by name; we already track the noise floor and know
    agc_threshold. Natural UI for #1162.
  5. Ship the checklist as a feature. A "Weak CW" one-click setup: AGC fast,
    AGC-T from the calibrator, 100–250 Hz filter (both already in our presets at
    RxApplet.cpp:249-252), APF
    on at a sane level, CW NR profile, decoder locked to radio pitch. When a
    skilled operator writes a seven-step procedure and other operators reply
    "you should bottle this and sell it," that procedure is a missing feature.
  6. Front-end check wizard. Automate the antenna-off/antenna-on delta
    measurement: measure both, show the delta, apply the 8–10 dB rule,
    recommend a preamp setting per band, remember it. Mechanical, fast, and it
    turns folklore into a button.
Investigate
  1. The preamp/noise-floor behaviour, per the test described in §5 above.
    Worth resolving publicly either way, since the "rely on SmartSDR until
    AetherSDR is patched" conclusion is circulating with a stated cause that
    does not match the code.

Related issues

  • #1494 — CW pitch centering indicator within filter bandwidth (K3-style)
  • #3486 — Magic-eye tuning indicator for CW/RTTY
  • #1756 — Asymmetric CW filter shape control
  • #1162 — Auto-control for panadapter noise level and auto AGC threshold
  • #632 — NR2 scratchy/artifact audio
  • #4357 — Noise-profile capture and reference-based noise reduction (NRP)
  • #333 — DSP presets and better panadapter
  • #736 — RF gain per-antenna instead of global, breaking dBm comparison
  • #2310 — Spectrum amplitude scale drag causes noise floor/signal level shift
  • #3186 — FFT Auto stops adjusting the noise floor after repeated profile changes

Environment

Reported against a FLEX-8400 on macOS, with a SmartSDR for macOS comparison.
The APF gating defect is platform-independent and reproducible on any radio in
CW mode.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.