feat(rade): unified TX pipeline with EOO frame transmission and callsign encoding#3221
Conversation
Implements a unified RADE TX pipeline that correctly transmits End-of-Over (EOO) frames with an LDPC-encoded operator callsign, matching the behaviour of FreeDV-GUI through the same DAX TX path. Five root-cause fixes (RC1-RC3, RC5) resolve why EOO frames were not decodable at the far end: - RC1: send final partial VITA-49 packet on setTransmitting(false) - RC2: drop zero-padding of partial voice accumulator at PTT release - RC3/RC5: emit EOO frame + FIR flush + silence as one atomic block - RC4 (output burst cadence) deferred — RC1-RC3/RC5 sufficient OTA Three-layer PTT intercept (PttOffHook + eooFinished timer + moxChanged fallback) holds TX open for the full EOO playout before set_mox=0. Callsign pre-encoded via rade_text_generate_tx_string() / rade_tx_set_eoo_bits() and embedded in every EOO frame. EOO timing: kEooFrameMs(144) + kEooSilenceTailMs(60) + kEooTransportMarginMs(50) = 254 ms total. Diagnostic RADE_WAV_TAP taps (A-F, off by default) and rade_demod_wav build target included for offline EOO frame analysis. OTA-confirmed against FreeDV-GUI: Dtmax12_eoo crosses ~0.45 threshold (was stuck at ~0.21), callsign "NF0T" decoded at far end. Principle XI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
821fbc8 to
ab21e8c
Compare
|
Hi @NF0T — thanks for the careful, evidence-backed write-up here. The OTA-confirmed EOO callsign decode against FreeDV-GUI is exactly the kind of demonstration Principle XI is asking for. CI statusOf the four checks on
What failed
Your "Related" note in the PR description anticipates this exactly:
Nothing in the RADE EOO pipeline — So this is CI infrastructure (upstream), not your codeThis is a pre-existing Windows toolchain issue in vendored third-party code, not a defect in your PR. The fix path is already in motion:
One small thing worth flaggingYour test plan unchecks "CI (Linux build — will run on PR open)" — I'd suggest checking it now: the Linux Recommended next steps
No code changes needed in this PR. Really nice work on the three-layer PTT intercept and the Dtmax12 evidence — 73 — AetherClaude 🤖 I'm AetherClaude, an automated CI triage assistant. If this analysis looks wrong, ping a human maintainer and we'll sort it out. 🤖 aethersdr-agent · cost: $5.3653 · model: claude-opus-4-7 |
rade_demod_wav.c includes getopt.h which is POSIX-only and absent from MSVC's SDK, causing a fatal C1083 on the CI Windows build. The tool is a Linux/macOS offline analysis utility; gate it with AND NOT WIN32. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @NF0T — really nice write-up, and the evidence trail (Dtmax12 numbers, rade_demod_wav -v 2 confirmation, OTA decode against FreeDV-GUI) is exactly what makes a fix like this reviewable. The three-layer PTT intercept story is well documented in both the code and audio-pipeline.md.
A few things I'd like a maintainer to look at — none are blockers, but a couple have blast-radius beyond the stated RADE scope:
Scope: changes that touch non-RADE paths
-
Resampler::prewarm()runs in everyResamplerconstructor — not just the RADEm_up8to24. This silently consumesLatencyzero samples for RX, TX, BNR, every Resampler in the app. The justification (eliminate startup transient) is reasonable, but this is a global audio-pipeline behavior change. Worth a quick sanity-check on the RX waterfall / voice paths to confirm no one was relying on the old startup behavior. Consider gating it on a constructor flag (prewarm=truedefault at the call sites that want it) so the change is opt-in per resampler. -
AudioEngine::onTxAudioReadyPTT gate (AudioEngine.cpp:4639) applies to all TX modes (Opus voice, DAX, RADE), not just RADE. Same forsendModemTxAudio. The meter is correctly accumulated before the gate, so VU readings are unaffected — but pre-MOXtxRawPcmReady()emissions to non-RADE consumers now stop. Likely fine, but please confirm Opus voice TX and DAX low-latency TX don't have any "warm up the encoder before MOX" assumption.
Possible issues
-
setTxCallsign()header comment claims thread-safe (RADEEngine.h:62) but the implementation callsrade_text_generate_tx_string()andrade_tx_set_eoo_bits()directly with no lock. IffeedTxAudio()is concurrently running on the worker thread, that's a data race on the RADE context. In practiceactivateRADE()calls this before TX starts so it's fine, but please either drop the thread-safety claim or wrap the call inQMetaObject::invokeMethod(this, ..., Qt::QueuedConnection). -
m_eooFinishedbecomes a sticky state — once set infeedTxAudio(), the very first line of the function returns early. It's only cleared byresetTx(). The PR description says Layer 3 (moxChanged(true)) callsresetTx()for the new over — please confirm that path always fires before the nextfeedTxAudio()call (e.g. fast double-tap PTT, CAT-initiated MOX cycles). If a TX edge ever races ahead ofresetTx(), the engine silently drops mic audio for the entire over with no log line. -
sendModemTxAudio"EOO race?" warning is silent data loss. If this fires for the EOO frame itself, the far-end won't decode the callsign and the user won't know why. Consider promoting toqCCriticalor adding telemetry — at minimum the warning should mention whether the dropped bytes were the EOO block vs. trailing voice.
Smaller stuff
-
rade_demod_wavis built unconditionally wheneverENABLE_RADE+ the source file exists. It's a diagnostic tool; consider tying it tooption(BUILD_RADE_DIAG ...)so contributors and CI don't pay the build cost. -
writeWavFloat()doesn't checkf.write()return values — fine for a debug-only path, just noting.
Code is solid overall, root-causes well-identified, and the per-tap WAV diagnostic infrastructure (off by default) is a nice debugging artifact. Looking forward to the maintainer's call on the three-layer intercept and the cross-cutting Resampler / AudioEngine changes.
73 from a reviewer who appreciated the "Dtmax12 ~0.21 → ~0.45" measurement up front.
🤖 aethersdr-agent · cost: $6.8807 · model: claude-opus-4-7
…x() (#3369). Principle I. The MIDI Toggle dispatch reads the getter, flips it, and calls the setter. For tx.mox and global.txButton the getter regressed to isMox() in #3221 (commit 6c37e37), silently reverting the fix from #2858 (407ae4c). Per FlexLib (Principle I), the radio never publishes `mox=` in `transmit status` messages, so m_mox stays false forever. The toggle then always reads 0, always flips to 1, always calls setTransmit(true) — stuck-on TX, mouse-only recovery. isTransmitting() reflects the optimistic state set inside TransmitModel::setMox() the moment setTransmit() fires, so the toggle correctly inverts on the next press. Matches the FlexControl handler already fixed by #2858. Fixes #3369. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ign encoding (aethersdr#3221) ## Summary Implements a unified RADE TX pipeline that correctly transmits End-of-Over (EOO) frames with an LDPC-encoded operator callsign, matching the behaviour of FreeDV-GUI through the same DAX TX path. Five root-cause fixes were required; EOO+callsign transmission is now OTA-confirmed against a FreeDV-GUI station (see Test plan). ## Constitution principle honored **Principle VIII — Evidence Over Assertion**: the EOO fix claim is backed by measured `Dtmax12_eoo` values (previously stuck at ~0.21, now reliably crossing the ~0.45 detection threshold), `rade_demod_wav -v 2` output reporting frame-level EOO detection, and OTA callsign decode ("NF0T") at the far end. The behavior change is demonstrated independently of the implementing agent's description. **Principle XI — Fixes Are Demonstrated**: the fix was demonstrated OTA against a FreeDV-GUI station before this PR was opened; the test criteria listed below were all met and are reproducible. **Principle X — Claims Are Atomic And Mortal**: branch rebased against current `main` (including the 109-commit upstream batch from 2026-05-27) before this PR was opened; no stale-snapshot overwrites. ## Changes ### Core pipeline (`src/core/`) - `RADEEngine`: three-layer PTT intercept — `PttOffHook` holds TX open while `setEooRequested(true)` → `rade_tx_eoo()` + callsign encoding → FIR flush → 60 ms silence tail → `eooFinished` signal → 254 ms transport timer → `set_mox=0` - `RADEEngine::setTxCallsign()` pre-encodes callsign via `rade_text_generate_tx_string()` / `rade_tx_set_eoo_bits()` - Named timing constants on the class: `kEooFrameMs` (144) + `kEooSilenceTailMs` (60) + `kEooTransportMarginMs` (50) = 254 ms total - `Resampler`: removed dead `flush()` stub; added `prewarm()` to stabilise FIR state before first audio block ### GUI (`src/gui/`) - `MainWindow`: three-layer PTT intercept wired up; `PttOffHook` intercepts MOX button, serial/MIDI, and CAT PTT paths; `kEooPlaybackMs` composed from named constants; DAX stream registration uses `qCDebug(lcRade)`; VFO signals explicitly disconnected on `deactivateRADE()` ### Diagnostics (`#ifdef RADE_WAV_TAP`, off by default) - Six TAP points (A–F) write per-PTT WAV files for offline analysis - `rade_demod_wav` build target (wrapped in `EXISTS` guard) for verifying EOO frame detectability with `rade_demod_wav -v 2` ### Docs - `docs/architecture/audio-pipeline.md`: EOO transmission and three-layer PTT intercept documented; TX flowchart and path table updated > **Architecture note:** the three-layer PTT intercept touches > `MainWindow.cpp` (maintainer-only CODEOWNERS tier). This is an > architectural change — flagging for maintainer review per CONTRIBUTING.md. ## Test plan - [x] Local build passes (`cmake --build build`) — MinGW/Windows clean - [x] `Dtmax12_eoo` crosses detection threshold (~0.45; was stuck at ~0.21) - [x] `rade_demod_wav -v 2` reports "End-of-over at modem frame N" - [x] OTA: far-end FreeDV-GUI station decodes "EOO callsign: NF0T" - [x] No regression in voice RX/TX quality - [ ] CI (Linux build — will run on PR open) > **Note:** FreeDV-GUI only decodes the EOO callsign when Reporting is > enabled (Tools → Options). `textPtr_` is null when reporting is off; > the EOO pilot is still detected but the callsign is silently skipped. > RC4 (output burst cadence) deferred — RC1–RC3/RC5 proved sufficient OTA. ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) - [x] No new flat-key `AppSettings` calls — EOO timing constants are `static constexpr` on `RADEEngine` (Principle V) - [x] No new meter UI — `MeterSmoother` not applicable (Principle II) - [x] `docs/architecture/audio-pipeline.md` updated ## Related - Upstream issue aethersdr#3049 (liquid-dsp Windows build) — stopgap in progress via PR aethersdr#3220; that fix is not included here 🤖 Generated with [Claude Code](https://claude.ai/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Implements a unified RADE TX pipeline that correctly transmits
End-of-Over (EOO) frames with an LDPC-encoded operator callsign,
matching the behaviour of FreeDV-GUI through the same DAX TX path.
Five root-cause fixes were required; EOO+callsign transmission is now
OTA-confirmed against a FreeDV-GUI station (see Test plan).
Constitution principle honored
Principle VIII — Evidence Over Assertion: the EOO fix claim is backed
by measured
Dtmax12_eoovalues (previously stuck at ~0.21, now reliablycrossing the ~0.45 detection threshold),
rade_demod_wav -v 2outputreporting frame-level EOO detection, and OTA callsign decode ("NF0T")
at the far end. The behavior change is demonstrated independently of the
implementing agent's description.
Principle XI — Fixes Are Demonstrated: the fix was demonstrated OTA
against a FreeDV-GUI station before this PR was opened; the test criteria
listed below were all met and are reproducible.
Principle X — Claims Are Atomic And Mortal: branch rebased against
current
main(including the 109-commit upstream batch from 2026-05-27)before this PR was opened; no stale-snapshot overwrites.
Changes
Core pipeline (
src/core/)RADEEngine: three-layer PTT intercept —PttOffHookholds TX openwhile
setEooRequested(true)→rade_tx_eoo()+ callsign encoding→ FIR flush → 60 ms silence tail →
eooFinishedsignal → 254 mstransport timer →
set_mox=0RADEEngine::setTxCallsign()pre-encodes callsign viarade_text_generate_tx_string()/rade_tx_set_eoo_bits()kEooFrameMs(144) +kEooSilenceTailMs(60) +kEooTransportMarginMs(50) = 254 ms totalResampler: removed deadflush()stub; addedprewarm()tostabilise FIR state before first audio block
GUI (
src/gui/)MainWindow: three-layer PTT intercept wired up;PttOffHookintercepts MOX button, serial/MIDI, and CAT PTT paths;
kEooPlaybackMscomposed from named constants; DAX stream registration uses
qCDebug(lcRade); VFO signals explicitly disconnected ondeactivateRADE()Diagnostics (
#ifdef RADE_WAV_TAP, off by default)rade_demod_wavbuild target (wrapped inEXISTSguard) forverifying EOO frame detectability with
rade_demod_wav -v 2Docs
docs/architecture/audio-pipeline.md: EOO transmission andthree-layer PTT intercept documented; TX flowchart and path table updated
Test plan
cmake --build build) — MinGW/Windows cleanDtmax12_eoocrosses detection threshold (~0.45; was stuck at ~0.21)rade_demod_wav -v 2reports "End-of-over at modem frame N"Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — EOO timing constants arestatic constexpronRADEEngine(Principle V)MeterSmoothernot applicable (Principle II)docs/architecture/audio-pipeline.mdupdatedRelated
progress via PR fix(liquid-dsp): guard -lc/-lm link flags against WIN32 platforms #3220; that fix is not included here
🤖 Generated with Claude Code