[bug] Fix PC audio remote stream ownership#2226
Conversation
There was a problem hiding this comment.
Review: PC audio remote stream ownership fix
Thanks for this thorough investigation, @jensenpat. The root cause analysis — bare 4000009 parsed as decimal instead of hex, producing 0x003d0909 vs 0x04000009 — is convincing, and the fix in parseCreateResponseStreamId directly addresses it. The test coverage for the exact log-derived case is exactly what we need.
Overall
This is well-structured work. Extracting RadioStatusOwnership as a header-only, testable helper with clear enums and pure-function ownership classification is a good pattern. The state machine in RemoteAudioRxTracking is clean and the test exercises the key transitions. The diagnostic surfacing in SliceTroubleshooter and DeviceDiagnostics will make future debugging much easier.
Items worth a look
1. parseCreateResponseStreamId always parses as hex — is that safe for all stream types?
The parseCreateResponseStreamId inner lambda strips 0x and then unconditionally parses as base 16. This is correct for Flex stream IDs (which are always hex), but worth a comment noting this intentional choice, since parseStatusHandle in StreamStatus.h tries toUInt(&ok, 0) first (auto-detecting base) before falling back to hex. The difference is deliberate here, but someone might later try to "unify" these without understanding why.
2. streamCommandId omits the 0x prefix, but hexId includes it
streamCommandId(0x0400000A) returns "0400000a" while hexId(0x0400000A) returns "0x0400000a". The stream remove command uses hexId() in removeRxAudioStream but streamCommandId() in disconnectFromRadio/handleForcedClientDisconnect (via gracefulDisconnect). Is the radio's command parser consistent about accepting both 0x-prefixed and bare hex? The old code used "0x%1" with m_rxAudioStreamId for remove, and bare m_rxAudioStreamId for graceful disconnect. If the radio accepts both forms, this is fine — just want to confirm the intent.
3. ensureOwnedPanadapter captures raw pan pointer in lambda
In ensureOwnedPanadapter, the rfgain_info callback captures pan (a raw pointer). This mirrors the existing code exactly, but if the panadapter is removed before the async response arrives, the lambda holds a dangling pointer. The existing code had the same pattern, so this isn't a regression — just noting it as pre-existing.
4. 350ms settle delay is a magic number
scheduleRxAudioStreamEnsure uses a 350ms QTimer::singleShot. This is reasonable for letting SmartConnect status settle, but consider defining it as a named constant (e.g., kStreamSettleDelayMs) so it's discoverable and tunable if needed. Minor.
5. enable_testing() placement in CMakeLists.txt
enable_testing() is called inside the new test block, but it's typically called once near the top of the root CMakeLists. If it's already called elsewhere (likely), this is harmless but redundant. If not, it should be moved to a more visible location so all tests benefit.
6. Panadapter changes are part of #2222 base — scope note
The PR description correctly notes this is stacked on #2222. The ensureOwnedPanadapter, normalizePanadapterId, m_pendingPanStatuses, createDefaultSliceOnPan, panadapter fallback from display panafall create to panadapter create, and waterfall ownership changes are substantial and should collapse once #2222 lands. After rebase, worth confirming the diff is purely the audio-specific changes.
Looks good
RemoteAudioRxTrackingstate machine withcreatePending/removeRequestedflags handles the async race between create-response and status-arrival correctly.- The deferred-create-then-remove pattern (
if (m_rxAudio.removeRequested) removeRxAudioStream()) at the end of the create callback is the right way to handle toggle-off-during-pending. allowUnknownOwnerkeyed tom_clientInfoMap.size() <= 1is a clean way to handle legacy single-client firmware without a separate code path.- The duplicate cleanup in
createRxAudioStream(removing old stream only when parsed ID differs) avoids the original bug. - Logging is thorough without being excessive — the summary line format will be easy to grep in user logs.
Nice work tracking this down. The core fix is solid and the test coverage is on point.
4ff79d1 to
8663832
Compare
Heavy community-contribution release: - AetherSweep — in-panadapter SWR analyzer (jensenpat, 3 PRs) - ShackSwitch antenna switch integration (nigelfenton, 2 PRs) - Multi-client startup hardening: panadapter creation (#2222) + PC audio remote stream ownership (#2226), both jensenpat - macOS DMG dark-theme fix (Chaosuk97) - Windows-without-Qt6::SerialPort build fix (NF0T) - NRL gating correctness on 6000-series (#2219) - CMakeLists.txt / README.md / CLAUDE.md: 0.9.3 → 0.9.4 - CHANGELOG: v0.9.4 entry covers 10 PRs since v0.9.3 - WhatsNewData.cpp: regenerated from CHANGELOG Acknowledgements: - jensenpat — AetherSweep, multi-client startup, PC audio ownership - nigelfenton — ShackSwitch integration - NF0T — Windows build fix - Chaosuk97 — macOS DMG dark theme fix Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Fixes the PC Audio / multi-slice audio startup failure by hardening
remote_audio_rxstream ownership tracking and parsing.This PR is intentionally stacked on #2222 (
Fix multi-client startup panadapter creation) so the follow-on audio changes use the same ownership helper and avoid drifting from the panadapter fix. Until #2222 lands, the PR diff will include that base commit; after #2222 merges, this branch can be rebased and the visible diff should collapse to the PC Audio-specific changes.Root Cause
The failing logs showed that slices were valid and unmuted, and PC Audio was enabled, but the audio engine never received remote audio packets.
The key sequence was:
stream create type=remote_audio_rx compression=nonestream 0x04000009 type=remote_audio_rx ... client_handle=<ours>40000094000009as decimal instead of unprefixed hex0x003d0909instead of0x040000090x04000009, and left the audio engine with no incoming remote audio packetsThat explains why single-slice and multi-slice audio both failed: the shared
remote_audio_rxpipe was being removed, so no slice-level audio state could recover it.What Changed
RadioStatusOwnershipas a shared, testable helper for client ownership decisions and remote audio stream tracking.remote_audio_rxcreate-response stream ids as Flex stream ids, including bare numeric-looking responses like4000009as hex.remote_audio_rxstream status if it arrives before the create callback.client_handlecompatibility for older firmware while deferring unknown-owner status in multi-client scenarios.remote_audio_rxstate transitions, including stream id, owner, PC Audio state, AutoStartTCI state, pending create/remove state, and compression.Issue Coverage
This covers the PC Audio issue cluster where Windows users needed multiple toggles or saw no audio after connect:
It also preserves and tests the ownership behavior needed by #2222 / #2194:
client_handleValidation
Automated checks:
cmake -S . -B build -G Ninjacmake --build build -j10ctest --test-dir build --output-on-failure -j10build/radio_status_ownership_testgit diff --checkManual radio validation by @jensenpat:
Reviewer Notes
The most important regression test is the exact log-derived case:
parseCreateResponseStreamId("4000009") == 0x04000009Without this, AetherSDR stores the wrong id, removes the real stream, and the audio engine watchdog reports repeated
no audio data receivedrestarts.👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat