Skip to content

Fix TCI DAX duplicate stream causing 2× audio speed / WSJT-X FT8 no-decode#1679

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/tci-dax-sample-rate
Apr 20, 2026
Merged

Fix TCI DAX duplicate stream causing 2× audio speed / WSJT-X FT8 no-decode#1679
ten9876 merged 1 commit into
aethersdr:mainfrom
NF0T:fix/tci-dax-sample-rate

Conversation

@NF0T

@NF0T NF0T commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator

Root Cause

Two independent code paths both subscribed to dax_rx for the same DAX channel:

  1. DaxBridge (MainWindow/DaxAudioBridge) — creates a DAX RX stream when TCI clients connect and DAX is running
  2. TciServer::ensureDaxForTci — independently creates its own DAX RX stream for the same channel

The radio delivered identical VITA-49 audio packets to both stream IDs. PanadapterStream accepted both registrations and emitted daxAudioReady twice per audio period. The TCI accumulation buffer filled at 2× the expected rate, producing audio at 2× speed at WSJT-X — causing FT8 decoding to fail entirely.

Fix

  • PanadapterStream::registerDaxStream now evicts any existing stream registered to the same channel before inserting, enforcing at-most-one-stream-per-channel semantics
  • TciServer::ensureDaxForTci uses a new daxStreamIdForChannel() query to detect and reuse an existing DaxBridge-owned stream (marking it "borrowed") rather than creating a duplicate
  • TciServer::releaseDaxForTci skips stream removal for borrowed channels so DaxBridge-owned streams are not torn down when TCI clients disconnect

Files Changed

  • src/core/PanadapterStream.cppregisterDaxStream evicts stale same-channel entries; new daxStreamIdForChannel() method
  • src/core/PanadapterStream.h — declaration for daxStreamIdForChannel()
  • src/core/TciServer.cpp — borrow-existing-stream logic in ensureDaxForTci/releaseDaxForTci
  • src/core/TciServer.hQSet<int> m_tciDaxBorrowedChannels

Test Plan

  • Connect WSJT-X via TCI with DAX running — FT8 decodes correctly at normal speed
  • TCI RX audio plays back at correct pitch/speed in WSJT-X waterfall
  • Disconnecting TCI client does not tear down DaxBridge RX stream (DAX audio continues in the app)
  • Reconnecting TCI client reuses the existing stream without creating a duplicate

🤖 Generated with Claude Code

@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.

Nice root-cause analysis and clean fix, @NF0T. The dual-subscription → 2× audio speed chain is well explained and the fix addresses it at two levels — good defense in depth.

What looks good

  • Belt-and-suspenders design: registerDaxStream eviction prevents duplicates regardless of caller, while ensureDaxForTci avoids creating them in the first place. Either layer alone would fix the bug; both together make regressions harder.
  • Borrow semantics: Tracking borrowed channels so releaseDaxForTci doesn't tear down DaxBridge-owned streams is the right approach. The m_tciDaxBorrowedChannels set is cleared alongside m_tciDaxStreamIds — no stale state.
  • Thread safety: daxStreamIdForChannel correctly takes m_streamMutex, consistent with the existing locking discipline in PanadapterStream.
  • Scope: Four files, all within the TCI/DAX stream path. No unrelated changes.
  • Conventions: RAII mutex locking, proper Qt patterns, no QSettings usage.

One thing to consider (non-blocking)

Borrowed stream going stale: If DaxBridge tears down its DAX RX stream (e.g., user toggles DAX off in the UI) while TCI holds a borrowed reference, m_tciDaxStreamIds[ch] will point to a now-dead stream ID. Audio for that channel will silently stop — which is probably fine since the user explicitly killed DAX — but on a subsequent ensureDaxForTci call (e.g., a new TCI client connects), daxStreamIdForChannel will return 0, and TCI will correctly create a fresh stream. So the self-healing path exists. Just worth noting that there's a window where TCI thinks it has a stream but audio isn't flowing. A log message when daxAudioReady stops arriving for a borrowed channel could help operators debug, but that's a future enhancement, not a blocker.

Overall this is a well-targeted fix. Thanks for tracking down the duplicate-stream root cause — that's a subtle interaction between DaxBridge and TciServer that wasn't obvious from either side alone. 73!

…ecode

Two independent code paths (MainWindow/DaxBridge and TciServer::ensureDaxForTci)
were both subscribing to dax_rx for the same DAX channel. The radio delivered
identical VITA-49 audio packets to both stream IDs; PanadapterStream accepted
both registrations and emitted daxAudioReady twice per audio period. The TCI
accumulation buffer filled at 2x the expected rate, producing audio at 2x speed
at WSJT-X — causing FT8 decoding to fail entirely.

PanadapterStream::registerDaxStream now evicts any existing stream for the same
channel before inserting, enforcing at-most-one-stream-per-channel semantics.
A new daxStreamIdForChannel() query lets TciServer::ensureDaxForTci detect and
reuse existing streams (marking them as "borrowed") rather than creating
duplicates. releaseDaxForTci skips stream removal for borrowed channels so
DaxBridge-owned streams are not torn down when TCI clients disconnect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NF0T NF0T force-pushed the fix/tci-dax-sample-rate branch from 5e5a2d7 to 7897f81 Compare April 20, 2026 13:18
@ten9876 ten9876 merged commit 7ecfa8e into aethersdr:main Apr 20, 2026
5 checks passed
@ten9876

ten9876 commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Claude here — huge thanks @NF0T! Reproduced the bug clean in a fresh log (two registered DAX stream entries on the same channel within 9 s of WSJT-X connecting over TCI), and the fix resolves it cleanly. Code review passed our standards — thread-safe, well-commented, no conflicts. Merged to main as 7ecfa8e. This will ship in v0.8.19 as a top-priority FT8 reliability fix.

73, Jeremy KK7GWY & Claude (AI dev partner)

@NF0T NF0T deleted the fix/tci-dax-sample-rate branch April 22, 2026 14:03
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