Skip to content

feat(rade): unified TX pipeline with EOO frame transmission and callsign encoding#3221

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
NF0T:feat/rade-unified-pipeline
May 27, 2026
Merged

feat(rade): unified TX pipeline with EOO frame transmission and callsign encoding#3221
ten9876 merged 2 commits into
aethersdr:mainfrom
NF0T:feat/rade-unified-pipeline

Conversation

@NF0T

@NF0T NF0T commented May 27, 2026

Copy link
Copy Markdown
Collaborator

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

  • Local build passes (cmake --build build) — MinGW/Windows clean
  • Dtmax12_eoo crosses detection threshold (~0.45; was stuck at ~0.21)
  • rade_demod_wav -v 2 reports "End-of-over at modem frame N"
  • OTA: far-end FreeDV-GUI station decodes "EOO callsign: NF0T"
  • 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

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — EOO timing constants are
    static constexpr on RADEEngine (Principle V)
  • No new meter UI — MeterSmoother not applicable (Principle II)
  • docs/architecture/audio-pipeline.md updated

Related

🤖 Generated with Claude Code

@NF0T NF0T requested review from a team as code owners May 27, 2026 17:22
@NF0T NF0T self-assigned this May 27, 2026
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>
@NF0T NF0T force-pushed the feat/rade-unified-pipeline branch from 821fbc8 to ab21e8c Compare May 27, 2026 17:25
@aethersdr-agent

Copy link
Copy Markdown
Contributor

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 status

Of the four checks on ab21e8c:

  • build (Linux) — success
  • check-paths — success
  • analyze (cpp) (CodeQL) — still in progress at time of writing
  • check-windows — failed at the Build step

What failed

check-windows is failing on the pre-existing third_party/liquid-dsp linker error — not on anything in this PR. It's the same -lc/-lm regression you already documented and fixed in #3220:

target_link_libraries(${target} c m) at third_party/liquid-dsp/CMakeLists.txt:468 — MinGW ld reports cannot find -lc: No such file or directory.

Your "Related" note in the PR description anticipates this exactly:

Upstream issue #3049 (liquid-dsp Windows build) — stopgap in progress via PR #3220; that fix is not included here

Nothing in the RADE EOO pipeline — RADEEngine::setEooRequested(), rade_tx_eoo(), PttOffHook, the 254 ms transport timer, the rade_demod_wav target, or the RADE_WAV_TAP diagnostics — touches liquid-dsp. The Linux build proves the changes compile and link cleanly on a path that does build liquid-dsp.

So this is CI infrastructure (upstream), not your code

This 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 flagging

Your test plan unchecks "CI (Linux build — will run on PR open)" — I'd suggest checking it now: the Linux build job did pass cleanly, which is meaningful evidence that the EOO pipeline integrates correctly with the rest of the audio path on the platform that exercises liquid-dsp end-to-end.

Recommended next steps

  1. Wait for fix(liquid-dsp): guard -lc/-lm link flags against WIN32 platforms #3220 to merge (or ping a maintainer to expedite it — it's a one-line guard with a clean diff).
  2. Rebase feat/rade-eoo-callsign onto main once fix(liquid-dsp): guard -lc/-lm link flags against WIN32 platforms #3220 is in.
  3. Push the rebase; check-windows should pass.

No code changes needed in this PR. Really nice work on the three-layer PTT intercept and the Dtmax12 evidence — 0.21 → >0.45 is a textbook before/after measurement.

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>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Resampler::prewarm() runs in every Resampler constructor — not just the RADE m_up8to24. This silently consumes Latency zero 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=true default at the call sites that want it) so the change is opt-in per resampler.

  2. AudioEngine::onTxAudioReady PTT gate (AudioEngine.cpp:4639) applies to all TX modes (Opus voice, DAX, RADE), not just RADE. Same for sendModemTxAudio. The meter is correctly accumulated before the gate, so VU readings are unaffected — but pre-MOX txRawPcmReady() 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

  1. setTxCallsign() header comment claims thread-safe (RADEEngine.h:62) but the implementation calls rade_text_generate_tx_string() and rade_tx_set_eoo_bits() directly with no lock. If feedTxAudio() is concurrently running on the worker thread, that's a data race on the RADE context. In practice activateRADE() calls this before TX starts so it's fine, but please either drop the thread-safety claim or wrap the call in QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection).

  2. m_eooFinished becomes a sticky state — once set in feedTxAudio(), the very first line of the function returns early. It's only cleared by resetTx(). The PR description says Layer 3 (moxChanged(true)) calls resetTx() for the new over — please confirm that path always fires before the next feedTxAudio() call (e.g. fast double-tap PTT, CAT-initiated MOX cycles). If a TX edge ever races ahead of resetTx(), the engine silently drops mic audio for the entire over with no log line.

  3. 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 to qCCritical or adding telemetry — at minimum the warning should mention whether the dropped bytes were the EOO block vs. trailing voice.

Smaller stuff

  1. rade_demod_wav is built unconditionally whenever ENABLE_RADE + the source file exists. It's a diagnostic tool; consider tying it to option(BUILD_RADE_DIAG ...) so contributors and CI don't pay the build cost.

  2. writeWavFloat() doesn't check f.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

@ten9876 ten9876 merged commit 6c37e37 into aethersdr:main May 27, 2026
5 checks passed
@NF0T NF0T deleted the feat/rade-unified-pipeline branch May 27, 2026 19:20
@aethersdr-agent aethersdr-agent Bot mentioned this pull request Jun 3, 2026
2 tasks
aethersdr-agent Bot added a commit that referenced this pull request Jun 3, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
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.

2 participants