Summary
The current dax_rx stream borrow pattern (used by TciServer and now activateRADE() on non-bridge platforms) has a lifecycle gap: if the stream creator removes the stream while a borrower still holds its ID, the borrower's audio silently dies with no error or re-registration.
Details
Both TciServer and the new RADE fix use the same pattern:
- Check
panStream()->daxStreamIdForChannel(ch) — if non-zero, reuse (borrow) rather than create
- On release, skip
stream remove for borrowed streams
The gap: if creator deactivates while borrower is active, the creator calls panStream()->unregisterDaxStream() and sends stream remove. The borrower still has the stream ID in its own tracking (m_tciDaxStreamIds[ch] or equivalent) but PanadapterStream no longer has it registered. Subsequent daxAudioReady signals for that channel stop firing because the VITA-49 routing is gone.
Concrete scenario: RADE activates (creates dax_rx stream) → TCI client connects (borrows it) → user deactivates RADE → stream removed → TCI audio dies silently.
The current workaround in the RADE fix skips stream remove if TCI has active clients, which prevents the problem in the most common case but is not a robust general solution.
Proposed Fix
Add reference counting to PanadapterStream for dax_rx stream registrations:
registerDaxStream(streamId, ch) increments a ref count per channel
unregisterDaxStream(streamId) decrements; only removes internal registration when count reaches zero
- Callers that send
stream remove should check the ref count first: only send when dropping to zero
This would allow any creator/borrower to release independently without needing to know about other consumers.
Scope of Audit
All places that create or remove dax_rx streams should be reviewed under this model:
Related
Summary
The current
dax_rxstream borrow pattern (used byTciServerand nowactivateRADE()on non-bridge platforms) has a lifecycle gap: if the stream creator removes the stream while a borrower still holds its ID, the borrower's audio silently dies with no error or re-registration.Details
Both
TciServerand the new RADE fix use the same pattern:panStream()->daxStreamIdForChannel(ch)— if non-zero, reuse (borrow) rather than createstream removefor borrowed streamsThe gap: if creator deactivates while borrower is active, the creator calls
panStream()->unregisterDaxStream()and sendsstream remove. The borrower still has the stream ID in its own tracking (m_tciDaxStreamIds[ch]or equivalent) butPanadapterStreamno longer has it registered. SubsequentdaxAudioReadysignals for that channel stop firing because the VITA-49 routing is gone.Concrete scenario: RADE activates (creates dax_rx stream) → TCI client connects (borrows it) → user deactivates RADE → stream removed → TCI audio dies silently.
The current workaround in the RADE fix skips
stream removeif TCI has active clients, which prevents the problem in the most common case but is not a robust general solution.Proposed Fix
Add reference counting to
PanadapterStreamfordax_rxstream registrations:registerDaxStream(streamId, ch)increments a ref count per channelunregisterDaxStream(streamId)decrements; only removes internal registration when count reaches zerostream removeshould check the ref count first: only send when dropping to zeroThis would allow any creator/borrower to release independently without needing to know about other consumers.
Scope of Audit
All places that create or remove
dax_rxstreams should be reviewed under this model:MainWindow::startDax()/stopDax()(Mac/PipeWire)TciServer::acquireDaxForTci()/releaseDaxForTci()MainWindow::activateRADE()/deactivateRADE()(non-bridge platforms, Fix RADE RX not decoding on Windows: create dax_rx stream in activateRADE() #1820)stream removewholesaleRelated