Skip to content

[bug] Fix PC audio remote stream ownership#2226

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/pc-audio-stream-start
May 1, 2026
Merged

[bug] Fix PC audio remote stream ownership#2226
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/pc-audio-stream-start

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

Fixes the PC Audio / multi-slice audio startup failure by hardening remote_audio_rx stream 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:

  • AetherSDR requested stream create type=remote_audio_rx compression=none
  • The radio reported the real stream status as stream 0x04000009 type=remote_audio_rx ... client_handle=<ours>
  • The create response body returned 4000009
  • AetherSDR parsed bare 4000009 as decimal instead of unprefixed hex
  • That produced 0x003d0909 instead of 0x04000009
  • AetherSDR then believed the status stream and create-response stream were different, removed the real stream 0x04000009, and left the audio engine with no incoming remote audio packets

That explains why single-slice and multi-slice audio both failed: the shared remote_audio_rx pipe was being removed, so no slice-level audio state could recover it.

What Changed

  • Added RadioStatusOwnership as a shared, testable helper for client ownership decisions and remote audio stream tracking.
  • Parse remote_audio_rx create-response stream ids as Flex stream ids, including bare numeric-looking responses like 4000009 as hex.
  • Adopt owned remote_audio_rx stream status if it arrives before the create callback.
  • Defer initial stream creation briefly during connect so restored/SmartConnect status can settle before requesting another stream.
  • Avoid stealing or deleting streams owned by another client.
  • Keep legacy no-client_handle compatibility for older firmware while deferring unknown-owner status in multi-client scenarios.
  • Add duplicate cleanup only when the parsed create response is truly a different stream.
  • Add summary logging for remote_audio_rx state transitions, including stream id, owner, PC Audio state, AutoStartTCI state, pending create/remove state, and compression.
  • Surface remote audio stream state in Slice Troubleshooter diagnostics.
  • Register the new regression test with CTest.

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:

  • multi-client panadapter startup ownership
  • SmartSDR/DAX running before AetherSDR
  • older firmware status lines without client_handle

Validation

Automated checks:

  • cmake -S . -B build -G Ninja
  • cmake --build build -j10
  • ctest --test-dir build --output-on-failure -j10
  • build/radio_status_ownership_test
  • git diff --check

Manual radio validation by @jensenpat:

  • Multi-slice PC Audio works.
  • PC Audio toggled 10 times; audio returned every time.
  • SmartSDR and DAX started before AetherSDR; AetherSDR audio still worked.
  • Completed two TCI RX2-channel FT8 QSOs.
  • Completed DAX-based FT8 QSOs.

Reviewer Notes

The most important regression test is the exact log-derived case:

  • parseCreateResponseStreamId("4000009") == 0x04000009

Without this, AetherSDR stores the wrong id, removes the real stream, and the audio engine watchdog reports repeated no audio data received restarts.

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat

@jensenpat jensenpat changed the title Fix PC audio remote stream ownership [bug] Fix PC audio remote stream ownership May 1, 2026
@jensenpat jensenpat marked this pull request as ready for review May 1, 2026 02:21

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

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

  • RemoteAudioRxTracking state machine with createPending/removeRequested flags 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.
  • allowUnknownOwner keyed to m_clientInfoMap.size() <= 1 is 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.

@ten9876 ten9876 force-pushed the aether/pc-audio-stream-start branch from 4ff79d1 to 8663832 Compare May 1, 2026 03:28
@ten9876 ten9876 merged commit f2d22c1 into aethersdr:main May 1, 2026
4 checks passed
ten9876 added a commit that referenced this pull request May 1, 2026
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>
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