Fix macOS Bluetooth audio route handling#1486
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes macOS CoreAudio/Bluetooth routing regressions by tightening when local mic capture is started, preferring 48 kHz RX output to keep Bluetooth headsets on A2DP when possible, and hardening macOS TX lifecycle handling to avoid restart loops and stale callbacks.
Changes:
- Make mic selection updates immediate/optimistic in the transmit model (emit
micStateChangedon user change). - Gate local PC mic capture to
mic_selection == PCand queue macOS Bluetooth telephony-route toggles onto the audio thread. - Update
AudioEnginemacOS RX format/device selection (prefer 48 kHz) and add macOS TX lifecycle generation + restart suppression on backend errors/too-short runtimes.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/models/TransmitModel.cpp | Optimistically updates m_micSelection and emits micStateChanged immediately on user selection changes. |
| src/gui/MainWindow.cpp | Starts/stops local TX capture based on mic selection; queues macOS telephony-route allowance changes onto the audio thread. |
| src/core/AudioEngine.h | Adds macOS Bluetooth telephony allowance API + TX lifecycle tracking fields. |
| src/core/AudioEngine.cpp | Prefers 48 kHz RX output on macOS, avoids RX auto-restart on sink errors, and hardens macOS TX teardown/restart behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| qCWarning(lcAudio) << "AudioEngine: output device does not support 24kHz stereo Int16, trying 48kHz"; | ||
| fmt.setSampleRate(48000); | ||
| m_resampleTo48k = true; | ||
| if (!dev.isFormatSupported(fmt)) { | ||
| qCWarning(lcAudio) << "AudioEngine: output device does not support 48kHz stereo Int16 either"; | ||
| qCWarning(lcAudio) << "No audio device detected"; | ||
| return false; | ||
| candidateFmt.setSampleRate(48000); | ||
| if (dev.isFormatSupported(candidateFmt)) { | ||
| m_resampleTo48k = true; | ||
| return true; | ||
| } | ||
| } else { | ||
| m_resampleTo48k = false; | ||
|
|
||
| qCWarning(lcAudio) << "AudioEngine: output device does not support 48kHz stereo Int16 either"; | ||
| return false; |
| if (!m_audio->isTxStreaming()) { | ||
| audioStartTx(m_radioModel.radioAddress(), 4991); | ||
| } | ||
| } else if (m_audio->isTxStreaming()) { |
|
Hey @rfoust, thanks for your contribution! I took a look at the CI failure on commit CI Failure AnalysisThe failure is not in your code. The build itself passed cleanly. What failedThe "Cleanup artifacts" job failed at the "Delete artifacts" step. This is a post-build housekeeping job in our CI pipeline that cleans up temporary build artifacts after the main workflow completes. It's part of our CI infrastructure, not related to compilation or code quality. Check run summary
What this means for your PR
If the Thanks again for contributing — your changes look good from a build perspective! 🛠️ |
|
@rfoust Excited for this one. SmartSDR Mac has this issue as well. When BT phone mode kicks in, it also engages the bluetooth headset's noise cancellation, which also degrades RX audio. Thanks for this |
|
Claude here — merged to main. Clean rebase, no conflicts, builds clean on Linux. The cross-platform mic_selection=PC gating is a nice correctness fix that benefits all platforms. Great work Robbie! 73, Jeremy KK7GWY & Claude (AI dev partner) |
…#2973) ## Summary Implements #2971 by adding a dedicated default-on `aether.audio.summary` logging path for support-bundle-friendly audio routing and negotiation summaries. This adds compact, deduped summary blocks for: - startup audio environment: selected/default input and output devices, saved-device presence, PC Audio state, and TX mic route intent - RX sink startup: actual output device, sample rate, channel count, sample format, resampling state, and fallback/substitution history - TX source startup: actual input device, negotiated rate/channels, mono/stereo state, sample format, resampling-to-24 kHz state, and fallback history - CW sidetone startup: backend, actual/backend-selected device, rate, backend substitution, and fallback history - auxiliary local sinks when instantiated: Quindar local sink and Aetherial monitor playback sink - final open failures: attempted rates/channels/formats, backend, device, failure reason, and fallback history ## Why The normal support log already caught many warning paths, but it did not reliably capture successful runtime sink/source negotiation. That left recurring audio/CW reports without the details needed to distinguish wrong endpoint, sample-rate mismatch, resampler path, backend fallback, and complete open failure cases. ## Important Safety Detail The startup summary uses a new shallow `DeviceDiagnostics::buildAudioStartupSnapshot(...)` helper. It intentionally does not call support-bundle-grade capability probes such as `preferredFormat()` or `isFormatSupported()` across devices at app startup. Full device capability probing remains in `buildAudioDevicesSnapshot(...)` for explicit diagnostics/support flows. Failure summaries only report negotiation work the audio path already attempted; they do not add extra probing. ## Historical Cases Covered The summary path is designed around recurring issue/PR classes including: - macOS Bluetooth/HFP route substitution and 24 kHz vs 48 kHz behavior (#1486, #2634) - CommonRadioAudio / Float32 vs Int16 format issues (#1070, #1090) - CW sidetone endpoint/backend routing (#2072, #2075, #2899, #2901) - CW sidetone missing/distorted reports (#2629, #2694) - no-audio-with-active-meters / output negotiation support gaps (#1855) ## Implementation Notes - Adds `AudioSummaryLogger` with canonical one-block formatters and dedupe by event key. - Adds `aether.audio.summary` as a default-on info category without enabling the full `aether.audio` info/debug stream. - Keeps detailed existing `aether.audio` diagnostics intact. - Documents the summary logging policy in `docs/audio-pipeline.md`. ## Validation - `cmake --build build -j22` - `./build/device_diagnostics_test` - `./build/cw_sidetone_test` - `git diff --check` - Built and deployed a macOS test bundle; the primary test-machine replacement path is currently blocked by a Desktop delete ACL, so a separate quarantine-cleared test bundle was prepared. 👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
…aethersdr#2973) ## Summary Implements aethersdr#2971 by adding a dedicated default-on `aether.audio.summary` logging path for support-bundle-friendly audio routing and negotiation summaries. This adds compact, deduped summary blocks for: - startup audio environment: selected/default input and output devices, saved-device presence, PC Audio state, and TX mic route intent - RX sink startup: actual output device, sample rate, channel count, sample format, resampling state, and fallback/substitution history - TX source startup: actual input device, negotiated rate/channels, mono/stereo state, sample format, resampling-to-24 kHz state, and fallback history - CW sidetone startup: backend, actual/backend-selected device, rate, backend substitution, and fallback history - auxiliary local sinks when instantiated: Quindar local sink and Aetherial monitor playback sink - final open failures: attempted rates/channels/formats, backend, device, failure reason, and fallback history ## Why The normal support log already caught many warning paths, but it did not reliably capture successful runtime sink/source negotiation. That left recurring audio/CW reports without the details needed to distinguish wrong endpoint, sample-rate mismatch, resampler path, backend fallback, and complete open failure cases. ## Important Safety Detail The startup summary uses a new shallow `DeviceDiagnostics::buildAudioStartupSnapshot(...)` helper. It intentionally does not call support-bundle-grade capability probes such as `preferredFormat()` or `isFormatSupported()` across devices at app startup. Full device capability probing remains in `buildAudioDevicesSnapshot(...)` for explicit diagnostics/support flows. Failure summaries only report negotiation work the audio path already attempted; they do not add extra probing. ## Historical Cases Covered The summary path is designed around recurring issue/PR classes including: - macOS Bluetooth/HFP route substitution and 24 kHz vs 48 kHz behavior (aethersdr#1486, aethersdr#2634) - CommonRadioAudio / Float32 vs Int16 format issues (aethersdr#1070, aethersdr#1090) - CW sidetone endpoint/backend routing (aethersdr#2072, aethersdr#2075, aethersdr#2899, aethersdr#2901) - CW sidetone missing/distorted reports (aethersdr#2629, aethersdr#2694) - no-audio-with-active-meters / output negotiation support gaps (aethersdr#1855) ## Implementation Notes - Adds `AudioSummaryLogger` with canonical one-block formatters and dedupe by event key. - Adds `aether.audio.summary` as a default-on info category without enabling the full `aether.audio` info/debug stream. - Keeps detailed existing `aether.audio` diagnostics intact. - Documents the summary logging policy in `docs/audio-pipeline.md`. ## Validation - `cmake --build build -j22` - `./build/device_diagnostics_test` - `./build/cw_sidetone_test` - `git diff --check` - Built and deployed a macOS test bundle; the primary test-machine replacement path is currently blocked by a Desktop delete ACL, so a separate quarantine-cleared test bundle was prepared. 👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
Summary
This fixes a cluster of related macOS CoreAudio/Bluetooth regressions in AetherSDR's RX/TX audio handling.
User-visible symptoms before this patch:
MIC.coreaudiodwas restarted.mic_selection=PC, switching the selected PC input device on macOS could briefly enter telephony mode, drop the mic, flash the macOS menu-bar microphone indicator, and in some cases crash the app.Root Causes
There were multiple bugs interacting here:
remote_audio_txexisted, even when the radio mic source was notPC.PCmic selection waited for the radio status echo before stopping local capture, leaving a window where the local mic path kept running unnecessarily.AudioEngineteardown/startup.QAudioSourcerecovery path could restart or tear down the wrong source after an input-device switch because stalestateChanged(StoppedState)callbacks from an old source were still acting on the current one.What This Changes
RX / Bluetooth output routing on macOS
PC.PC mic gating and mic selection handling
mic_selection == PC.PCstops local capture right away instead of waiting for the radio echo.macOS TX lifecycle hardening
QAudioSourceon macOS if it stopped with an actual backend error.QAudioSourcethat stops immediately after opening; shut it down instead. This prevents the menu-bar microphone indicator from flashing due to a rapid reopen loop.QAudioSource::stateChanged(StoppedState)signals from a previous input device cannot tear down a newly opened source after a device switch.Scope
This PR is intentionally scoped to the validated audio fixes.
Platform impact:
mic_selection == PCgating and immediate local model update are cross-platform correctness fixes, but they only prevent starting/stopping the local PC mic path at the wrong time.Verification
Verified manually on macOS by building with:
Manual repro/verification covered:
MICselected with Bluetooth headphones as output: headset stays out of telephony mode and relaunch audio works.PCselected with Bluetooth headset used for input/output: telephony mode is only used when expected.Yeti X) and the Bluetooth headset no longer causes a TX restart loop, menu-bar mic flashing, or loss of the newly selected microphone.