Skip to content

Fix KiwiSDR CW BFO offset so a tuned carrier produces an audible tone (#4423) - #1

Closed
wa2n-code wants to merge 2 commits into
mainfrom
fix/4423-kiwisdr-cw-bfo-offset
Closed

Fix KiwiSDR CW BFO offset so a tuned carrier produces an audible tone (#4423)#1
wa2n-code wants to merge 2 commits into
mainfrom
fix/4423-kiwisdr-cw-bfo-offset

Conversation

@wa2n-code

Copy link
Copy Markdown
Owner

Summary

  • Fixes CW tuned frequency offset with Flex antenna does not match KiwiSDR offset for audible tone aethersdr/AetherSDR#4423: on a KiwiSDR virtual antenna in CW mode, a carrier centered under the tuned-frequency pip produced no audible tone (or, after the first commit below, a DC-thump/heterodyne instead of a clean tone).
  • KiwiSDR's SET ... freq= is the BFO/mixdown point, not a dial frequency — low_cut/high_cut are an audio passband applied relative to it. Sending freq=carrier puts the carrier at 0 Hz, which either falls outside the passband (no tone) or lands as an inaudible DC thump (tone, but not a tone).

Why two commits

This shipped in two steps because the first fix's assumption was disproved by a real session log attached to the issue, and I want that story visible rather than squashed away:

  1. Offset KiwiSDR CW sound-stream BFO by the passband center — an automated triage comment on the issue had already root-caused the "BFO stays at the carrier" problem and proposed shifting freq by the CW passband's center (assuming Flex reports the passband already offset toward the pitch, e.g. 400..800). I implemented that. It compiled and looked reasonable, but testing on real hardware showed no change in behavior — still no tone, and audible "DC shifting" while tuning.

  2. Use the radio's actual CW pitch for KiwiSDR BFO offset, not passband center — pulling the log files attached to the issue showed the actual commands AetherSDR was sending: SET mod=cw low_cut=-400 high_cut=400 freq=.... Flex's real CW passband is symmetric about the carrier, not offset — the sidetone pitch is a DSP shift Flex applies after that filter, not something baked into the reported passband. With a symmetric passband, (low+high)/2 is always 0, so commit 1's shift was a no-op — the carrier kept landing at 0 Hz, which is exactly the "DC thump" that was reported back.

    The fix: use the radio's actual configured CW pitch (TransmitModel::cwPitch(), the same value driving the Flex sidetone/monitor tone) instead of deriving anything from the passband. Move the Kiwi BFO down by the pitch (carrier demodulates to +pitch Hz) and slide low_cut/high_cut up by the same amount (so that +pitch Hz frequency is still inside the passband instead of getting filtered out). This reproduces, in software, the shift the Flex's own onboard DSP does natively — which is also why Flex antennas never needed this fix in the first place.

    Also plumbed cwPitch from MainWindowKiwiSdrManagerKiwiSdrClient::setTrackedSlice(), and wired a live re-send when the operator changes CW pitch mid-session (MainWindow_Wiring.cpp), so an already-active KiwiSDR CW session doesn't go stale if the pitch is adjusted after switching to the Kiwi.

Test plan

  • Added/updated formatSoundTuneCommand unit coverage in tests/kiwi_sdr_protocol_test.cpp (CW with real symmetric passband + pitch, CW with pitch disabled, USB/LSB unaffected).
  • Verified against the real SET mod=cw low_cut=-400 high_cut=400 freq=... commands captured in the issue's attached session log.
  • Manually verified on real hardware (Flex 6600 + KiwiSDR): tuned-frequency pip stays centered on the signal, CW carrier now produces an audible tone matching the configured CW Pitch.

🤖 Generated with Claude Code

wa2n-code and others added 2 commits July 24, 2026 16:24
…4423)

KiwiSDR's SET freq= is the BFO/mixdown point, not a dial frequency;
low_cut/high_cut are applied as an audio passband relative to it. Our
CW passband is already centered on the sidetone pitch above (or, for
CWL, below) the carrier, so sending freq=carrier put the carrier at
0 Hz, outside the passband, and the tone was filtered out even though
the tuned-frequency pip overlaid the signal.

Extract SET mod=/low_cut=/high_cut=/freq= construction into
KiwiSdrProtocol::formatSoundTuneCommand() so the CW BFO math is unit
testable, and shift freq by the passband center for mode=="cw".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…center (aethersdr#4423)

A real session log (attached to the issue) showed Flex reports the CW
passband symmetric about the carrier (low_cut=-400 high_cut=400) — the
sidetone pitch is a DSP shift Flex applies AFTER that filter, not baked
into the reported passband. With a symmetric passband the previous fix's
"shift by passband center" computed a zero offset, so nothing changed:
the carrier still demodulated to 0 Hz (heard as a DC thump while tuning,
not a tone), matching what was reported after the first fix landed.

Shift the whole receive chain by the radio's actual CW pitch instead:
move the Kiwi BFO down by the pitch so the carrier demodulates to
+cwPitchHz, and slide low_cut/high_cut up by the same amount so that
frequency is still inside the passband. Plumb TransmitModel::cwPitch()
from MainWindow down through KiwiSdrManager and into
KiwiSdrClient::setTrackedSlice(), and re-send tracked-slice state when
the pitch changes live so an active KiwiSDR CW session doesn't go stale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@wa2n-code

Copy link
Copy Markdown
Owner Author

Closing — reopening against the upstream aethersdr/AetherSDR repo instead of my own fork.

@wa2n-code wa2n-code closed this Jul 24, 2026
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.

CW tuned frequency offset with Flex antenna does not match KiwiSDR offset for audible tone

1 participant