Fix TCI DAX duplicate stream causing 2× audio speed / WSJT-X FT8 no-decode#1679
Conversation
There was a problem hiding this comment.
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:
registerDaxStreameviction prevents duplicates regardless of caller, whileensureDaxForTciavoids creating them in the first place. Either layer alone would fix the bug; both together make regressions harder. - Borrow semantics: Tracking borrowed channels so
releaseDaxForTcidoesn't tear down DaxBridge-owned streams is the right approach. Them_tciDaxBorrowedChannelsset is cleared alongsidem_tciDaxStreamIds— no stale state. - Thread safety:
daxStreamIdForChannelcorrectly takesm_streamMutex, consistent with the existing locking discipline inPanadapterStream. - 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>
5e5a2d7 to
7897f81
Compare
|
Claude here — huge thanks @NF0T! Reproduced the bug clean in a fresh log (two 73, Jeremy KK7GWY & Claude (AI dev partner) |
Root Cause
Two independent code paths both subscribed to
dax_rxfor the same DAX channel:MainWindow/DaxAudioBridge) — creates a DAX RX stream when TCI clients connect and DAX is runningTciServer::ensureDaxForTci— independently creates its own DAX RX stream for the same channelThe radio delivered identical VITA-49 audio packets to both stream IDs.
PanadapterStreamaccepted both registrations and emitteddaxAudioReadytwice 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::registerDaxStreamnow evicts any existing stream registered to the same channel before inserting, enforcing at-most-one-stream-per-channel semanticsTciServer::ensureDaxForTciuses a newdaxStreamIdForChannel()query to detect and reuse an existing DaxBridge-owned stream (marking it "borrowed") rather than creating a duplicateTciServer::releaseDaxForTciskips stream removal for borrowed channels so DaxBridge-owned streams are not torn down when TCI clients disconnectFiles Changed
src/core/PanadapterStream.cpp—registerDaxStreamevicts stale same-channel entries; newdaxStreamIdForChannel()methodsrc/core/PanadapterStream.h— declaration fordaxStreamIdForChannel()src/core/TciServer.cpp— borrow-existing-stream logic inensureDaxForTci/releaseDaxForTcisrc/core/TciServer.h—QSet<int> m_tciDaxBorrowedChannelsTest Plan
🤖 Generated with Claude Code