Conversation
Adds a client-side CW sidetone generator that fires within ~25 ms of a key event, independent of the radio's DAX-fed sidetone (which has 30–100 ms of round-trip latency, brutal at 25+ WPM). Architecture: * src/core/CwSidetoneGenerator — DSP core. State machine (idle/ramp-up/sustain/ramp-down), atomic params for live UI updates, raised-cosine envelope at 5 ms default for click-free attack/release, continuous-phase sine. 10 unit tests covering enable/disable, pitch via zero-crossings, volume scaling, envelope shape, ramp transitions, and reset. * AudioEngine owns a dedicated QAudioSink for sidetone, separate from the RX sink so RX keeps its 200 ms jitter cushion while sidetone runs on a 50 ms buffer fed by a 2 ms PreciseTimer. The dedicated sink + push mode were chosen after a pull-mode attempt flapped Idle/Active in 85 ms cycles on Linux/Pulse, producing audible chop. Diagnostic logs from that flapping run pinpointed the root cause — removed before commit so steady-state operation is logging-free. * RadioModel emits cwKeyDownChanged on every transition through sendCwKey/sendCwPaddle. Every existing key source funnels through those: serial CTS/DSR (FlexControl), MIDI Gate (MidiControlManager), TCI straight-key, CWX, HID encoder. One signal hooks all of them. * src/core/CwxLocalKeyer — Morse keyer that translates CWX text + WPM into a sequence of dit/dah/gap timing events. CwxModel emits transmissionRequested(text, wpm) from send/sendChar/sendMacro and transmissionCancelled from erase/clearBuffer; the keyer drives the same sidetone generator so CWX transmissions get audible feedback matching what the radio is sending. Both sides run at the same WPM so they stay in sync within ±1 element on typical hardware. * PhoneCwApplet UI gains a "Local STn" toggle, volume slider, "Follow" toggle (radio's cw_pitch), and manual pitch slider (disabled when follow is on). Persisted via AppSettings keys CwLocalSidetoneEnabled, CwLocalSidetoneVolume, CwLocalSidetonePitchFollow, CwLocalSidetonePitchHz. * TransmitModel::phoneStateChanged drives pitch-follow updates so the sidetone tracks any radio-side cw_pitch change. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three protocol-correctness fixes on the netCW UDP keying path, mirrored from FlexLib reference (Radio.cs:8890-8965, NetCWStream.cs). None of these solved the underlying issue that the radio doesn't transition to TRANSMIT on netCW commands, but they bring the wire format closer to what FlexLib actually ships and clear three subtle deviations. 1. sendCwKey/sendCwPaddle now bracket each `cw key` with a `cw ptt 1` on press and `cw ptt 0` on release, matching FlexLib's CWPTT+CWKey pair. Without explicit PTT the radio queues key events with break_in=0 instead of transmitting. 2. buildNetCwPacket no longer pads the payload to a 4-byte boundary. FlexLib's AddTXData sends `tx_data.Length` payload bytes exactly (so a 57-byte command yields an 85-byte datagram, not 88). The radio uses datagram length to delimit the command string; trailing zero bytes can confuse the parser. 3. sendNetCwCommand now builds four independent packets for the redundant 0/5/10/15 ms sends so each datagram has a unique VITA-49 packet_count. FlexLib increments packet_count after every ToBytesTX() call, so four redundant copies arrive with counts N, N+1, N+2, N+3. Reusing one buffer for all four makes the stream layer drop them as duplicates. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
M_PI is a POSIX extension and not defined by MSVC <cmath> without _USE_MATH_DEFINES. Use an explicit constexpr kPi instead so the sidetone generator builds on Windows CI. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2 tasks
This was referenced Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a client-side CW sidetone generator that fires within ~25 ms of a
key event, independent of the radio's DAX-fed sidetone (which has
30–100 ms of round-trip latency, brutal at 25+ WPM).
Architecture:
src/core/CwSidetoneGenerator — DSP core. State machine
(idle/ramp-up/sustain/ramp-down), atomic params for live UI updates,
raised-cosine envelope at 5 ms default for click-free attack/release,
continuous-phase sine. 10 unit tests covering enable/disable, pitch
via zero-crossings, volume scaling, envelope shape, ramp transitions,
and reset.
AudioEngine owns a dedicated QAudioSink for sidetone, separate from
the RX sink so RX keeps its 200 ms jitter cushion while sidetone
runs on a 50 ms buffer fed by a 2 ms PreciseTimer. The dedicated
sink + push mode were chosen after a pull-mode attempt flapped
Idle/Active in 85 ms cycles on Linux/Pulse, producing audible chop.
Diagnostic logs from that flapping run pinpointed the root cause —
removed before commit so steady-state operation is logging-free.
RadioModel emits cwKeyDownChanged on every transition through
sendCwKey/sendCwPaddle. Every existing key source funnels through
those: serial CTS/DSR (FlexControl), MIDI Gate (MidiControlManager),
TCI straight-key, CWX, HID encoder. One signal hooks all of them.
src/core/CwxLocalKeyer — Morse keyer that translates CWX text +
WPM into a sequence of dit/dah/gap timing events. CwxModel emits
transmissionRequested(text, wpm) from send/sendChar/sendMacro and
transmissionCancelled from erase/clearBuffer; the keyer drives the
same sidetone generator so CWX transmissions get audible feedback
matching what the radio is sending. Both sides run at the same
WPM so they stay in sync within ±1 element on typical hardware.
PhoneCwApplet UI gains a "Local STn" toggle, volume slider, "Follow"
toggle (radio's cw_pitch), and manual pitch slider (disabled when
follow is on). Persisted via AppSettings keys CwLocalSidetoneEnabled,
CwLocalSidetoneVolume, CwLocalSidetonePitchFollow, CwLocalSidetonePitchHz.
TransmitModel::phoneStateChanged drives pitch-follow updates so the
sidetone tracks any radio-side cw_pitch change.
Co-Authored-By: Claude Opus 4.7 (1M context) [email protected]