Skip to content

Fix RADE RX not decoding on Windows: create dax_rx stream in activateRADE()#1820

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-rx-windows-no-dax-stream
Apr 21, 2026
Merged

Fix RADE RX not decoding on Windows: create dax_rx stream in activateRADE()#1820
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/rade-rx-windows-no-dax-stream

Conversation

@NF0T

@NF0T NF0T commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1819

Problem

RADE mode activates successfully on Windows (log shows "RADE mode activated") but produces no decoded audio. The sync indicator never appears and RADEEngine::feedRxAudio is never called.

Root cause: PanadapterStream::daxAudioReady only fires when the radio is sending dax_rx VITA-49 packets, which only start after the client sends stream create type=dax_rx dax_channel=N. That command lives inside startDax(), which is compiled out on non-Mac/non-PipeWire platforms (#if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE)). On Windows, the stream is never created, so feedRxAudio never gets called.

RADE TX is unaffected — it uses the remote_audio_tx Opus path with dax=0 mic routing, which is platform-independent.

Fix

When RADE activates on a non-bridge platform, send stream create type=dax_rx for the RADE slice's DAX channel directly and register the returned stream ID with PanadapterStream via a one-shot statusReceived connection.

Follows the same borrow-vs-create pattern as TciServer:

  • If a stream already exists for the channel (created by TCI), reuse it — no duplicate that would deliver double audio to RADEEngine
  • m_radeDaxStreamId is only set when we created the stream, so deactivateRADE() knows whether to send stream remove
  • On deactivate, skip stream remove if TCI has active clients (TCI may have borrowed our stream; removing it would silently kill TCI audio)

Test Plan

  • Verified on Windows 11 / MinGW 13.1.0 / FLEX-8400 firmware 4.1.5
  • Log shows RADE registered dax_rx ch 1 stream 0x400000b on activation
  • SNR indicator appears in VFO widget on RADE signal acquisition
  • Clean audio decode confirmed end-to-end
  • Linux without PipeWire (same code path, untested — would need a non-PipeWire Linux build)

Notes

🤖 Generated with Claude Code

…RADE()

On platforms without a DAX audio bridge (Windows, Linux without PipeWire),
startDax() is not compiled in so stream create type=dax_rx is never sent
to the radio. RADEEngine::feedRxAudio() was consequently never called
because PanadapterStream::daxAudioReady only fires when the radio is
actively sending dax_rx VITA-49 packets.

Fix: when RADE activates on a non-bridge platform, send stream create
type=dax_rx for the RADE slice's DAX channel directly. Register the
returned stream ID with PanadapterStream on the statusReceived response.
On deactivate, remove the stream only if TCI has no active clients
(TCI may have borrowed our stream via the same daxStreamIdForChannel
reuse pattern it already uses with DaxBridge streams).

If a dax_rx stream for this channel already exists (created by TCI),
reuse it rather than creating a duplicate that would deliver double audio
to RADEEngine.

RADE TX is unaffected — it uses the remote_audio_tx Opus path with
dax=0 mic routing, which is platform-independent.

Fixes: RADE mode shows "activated" but produces no decoded audio on
Windows or Linux builds without HAVE_PIPEWIRE.

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.

Good catch @NF0T — this is a real bug and the fix approach is solid. The root cause analysis is correct: startDax() is compiled out on Windows/non-PipeWire Linux, so the dax_rx VITA-49 stream never gets created and feedRxAudio is never called.

The implementation follows the existing TciServer borrow-vs-create pattern well, and the files changed are tightly scoped to the stated fix.

A few observations:

1. Race between stream create response and RADE deactivation

If the user toggles RADE off very quickly after activation, deactivateRADE() runs disconnect(m_radeDaxStreamConn) — good. But the stream create command is already in-flight. The radio will respond with a status message containing the new stream ID, and since the one-shot connection is now disconnected, m_radeDaxStreamId stays 0. The stream is created on the radio but never removed by us.

This is a narrow window and unlikely to hit in practice, but worth noting. A possible mitigation: in deactivateRADE(), if m_radeDaxStreamId == 0 and the connection was still live (not yet fired), you could set a flag to send stream remove when the response arrives, or just accept the leak as a known limitation documented in the TODO.

2. m_tciServer access without #ifdef HAVE_TCI guard

The deactivation path does m_tciServer && m_tciServer->clientCount() > 0. If TCI support is compiled out, m_tciServer would still be nullptr (it's initialised to nullptr in the header), so the null check saves you. This works, but it relies on the member always being declared — if m_tciServer were ever put behind #ifdef HAVE_TCI in the header, this would break. A comment noting the assumption would be defensive.

3. The daxCh range check (>= 1 && <= 4) is correct — FlexRadio DAX channels are 1–4, matching the existing codebase conventions.

4. Clean scoping — the { } block inside the #if keeps radeSlice and daxCh from leaking into the surrounding scope. Nice.

Overall this looks correct and well-reasoned. The PR description is exemplary — clear root cause, explanation of the TCI interaction, and honest test coverage gaps noted. Thank you for the contribution, and for tracking down a platform-specific issue that's hard to discover without Windows hardware.


Reviewed by AetherClaude

@NF0T

NF0T commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review @aethersdr-agent.

On the race window: Acknowledged — this is a real edge case. The window is the radio's round-trip latency for the stream create response (typically 20–50ms on a LAN), so it requires the user to toggle RADE off almost instantly after toggling it on. We've documented it in the TODO comment (// TODO: replace with proper ref-counting). Adding an in-flight flag to catch the late response would work but adds complexity for a very narrow edge case. Accepting it as a known limitation for now; a proper fix belongs in the broader stream lifecycle refactor tracked in #1821.

On m_tciServer without #ifdef HAVE_TCI: TciServer is unconditionally compiled in — there is no HAVE_TCI guard in CMakeLists.txt. m_tciServer is always a declared member initialized to nullptr. The null check is therefore always valid and not fragile in the current build system. I'll add a brief comment to make that assumption explicit so it doesn't silently break if TCI is ever made optional.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Reviewed the diff. This is a clean fix — the root cause analysis is correct (no dax_rx VITA-49 packets without stream create, and startDax() is compiled out on Windows), and the solution is well-scoped.

A few notes after reading through:

The one-shot connection pattern is solid. disconnect(m_radeDaxStreamConn) being placed outside the if (m_radeDaxStreamId) block in deactivateRADE() is the right call — it handles the race case where deactivate fires before the radio responds, preventing the lambda from writing to m_radeDaxStreamId after RADE has already torn down. The orphaned stream on the radio side is the documented race window you mentioned; agreed that's acceptable for now.

On m_tciServer without a guard: Your explanation that TciServer is unconditionally compiled makes sense. A one-line comment above the null check like // TciServer is always compiled in — no HAVE_TCI guard exists would make the assumption grep-able for anyone who later considers making it optional.

One thing to watch: If daxChannel() returns 0 (no DAX channel assigned), the warning log fires but RADE still activates with no RX path. The user gets working TX but silent RX with no UI indication beyond the log. This isn't a blocker for this PR — it's an existing limitation of the RADE activation flow — but it might warrant a follow-up to surface a user-visible warning or auto-assign a DAX channel on RADE activation.

LGTM overall. The borrow-vs-create pattern with TCI and the cleanup guard on isConnected() before sending stream remove are both good defensive choices.

@ten9876

ten9876 commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

Hey Ryan, thanks for all of the contributions you're making. We have a discord space where all of the AetherSDR users are discussing roadmap plans and ideas. We'd love for you to join in the conversation.

https://discord.gg/wBxDWbGe

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified all helper APIs exist (daxStreamIdForChannel, registerDaxStream, clientCount). Correct platform guard matches startDax() gating. TCI-safe cleanup logic is sound. Tested on real Windows 11 + FLEX-8400 hardware. LGTM.

@ten9876 ten9876 merged commit 250a0a7 into aethersdr:main Apr 21, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Apr 22, 2026
)

Cuts a community-contribution-heavy point release. Eight community PRs
landed plus three AetherClaude crash fixes. Highlights:

Community:
- Fix RADE RX not decoding on Windows (#1820, NF0T)
- Clamp stale DAX RX backlog on macOS — fixes +1.5s FT8 DT bias (#1822, jensenpat)
- Fix TCI DAX resampler crosstalk between slices (#1815, Chaosuk97)
- Fix RX applet pan slider with NR active (#1799, Chaosuk97)
- Fix TCI RX gain default 1.0 → 0.5 to match applet (#1811, NF0T)
- Fix HAVE_SERIALPORT guard (#1812, NF0T)
- Seamless ADIF logbook auto-reload (#1801, Chaosuk97)
- RAC Canada band-plan corrections (#1817, VE3NEM)

AetherClaude crash fixes:
- Applet reorder with floating containers (#1745#1746)
- Lazy-build RadioSetupDialog tabs, dodges Wayland FFmpeg/VDPAU crash (#1776#1777)
- PanAdapter float-freeze — show-after-reset + direct reparent (#1668#1669)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NF0T NF0T deleted the fix/rade-rx-windows-no-dax-stream branch April 22, 2026 14:03
@aethersdr-agent aethersdr-agent Bot mentioned this pull request May 8, 2026
2 tasks
ten9876 added a commit that referenced this pull request May 10, 2026
* Fix DAX IQ silently broken on Windows / non-PipeWire Linux

DAX IQ has never delivered samples on Windows or non-PipeWire Linux:
neither the GUI's DaxIqApplet level meter nor any TCI IQ client receives
any data, even though stream creation succeeds and the radio sends UDP
packets.

Same class of bug as #1820 (RADE RX on Windows):

  startDax() is gated by #if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE)
  and contains the DAX IQ stream-status registration handler. On
  platforms without an audio bridge it's compiled out entirely. As a
  result, when iq_start arrives or the user toggles a DAX IQ channel
  'On' in the applet:

    - 'stream create type=dax_iq daxiq_channel=N' is sent OK
    - The radio creates the stream and sends VITA-49 IQ packets
    - PanadapterStream receives them but never registers them to a
      channel (registerIqStream() lives inside startDax())
    - PanadapterStream::iqDataReady never fires
    - DaxIqModel::feedRawIqPacket never runs → GUI meter silent
    - TciServer::onIqDataReady never runs → TCI clients get nothing

Mirror the DAX IQ wiring (status handler + registration + level meter
+ enable/disable hooks) outside the audio-bridge gate, conditional on
the inverse so we don't double-connect on Mac/PipeWire builds where
startDax() still does its own wiring lazily when DAX audio is toggled.

Verified on Windows (Flex 6700) by capturing 30s of frames into a
TCI IQ client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dax): restore qCDebug(lcDax) lines in Windows/Linux IQ wiring

Mirror the three diagnostic logs the original startDax() block has —
removed, ignoring-non-owner, and per-status — so the new ctor-time
path on Windows / non-PipeWire Linux produces the same trail when
debugging future stream-routing issues.  These were exactly the lines
that diagnosed this bug in the first place; keeping them on the new
path matches the pattern the next debugger will expect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Nigel Fenton <nigelfenton@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.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.

RADE RX does not decode on Windows — dax_rx stream never created without DAX bridge

2 participants