Skip to content

Prefer 48 kHz monitor playback on macOS#2634

Closed
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/bluetooth-playback-samplerate
Closed

Prefer 48 kHz monitor playback on macOS#2634
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/bluetooth-playback-samplerate

Conversation

@rfoust

@rfoust rfoust commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix Aetherial Audio monitor playback on macOS Bluetooth outputs by preferring a 48 kHz playback sink before falling back to 24 kHz.

The monitor records 24 kHz stereo PCM, but opening its dedicated QAudioSink at 24 kHz on macOS can cause Bluetooth headsets to switch into HFP/telephony mode. The main RX path already avoids this class of issue by preferring 48 kHz on macOS; this applies the same policy to the Aetherial Audio monitor playback path only.

Non-macOS behavior is unchanged: playback still tries 24 kHz first and falls back to 48 kHz.

Testing

  • cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
  • cmake --build build -j8
  • Manual macOS Bluetooth playback test confirmed Aetherial Audio monitor playback has no popping after the 24 kHz to 48 kHz upsample path.

@rfoust rfoust requested a review from ten9876 as a code owner May 13, 2026 17:03
Copilot AI review requested due to automatic review settings May 13, 2026 17:03
@rfoust rfoust requested a review from jensenpat as a code owner May 13, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Aetherial Audio monitor playback path to prefer opening a 48 kHz QAudioSink on macOS (to avoid Bluetooth devices switching into HFP/telephony mode), while keeping existing behavior on other platforms.

Changes:

  • On macOS, try 48 kHz playback first and fall back to 24 kHz if unsupported.
  • On non-macOS platforms, continue trying 24 kHz first and fall back to 48 kHz.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +191 to +197
static constexpr int kFallbackSampleRate = 48000;
#ifdef Q_OS_MAC
const int preferredRate = kFallbackSampleRate;
const int fallbackRate = kSampleRate;
#else
const int preferredRate = kSampleRate;
const int fallbackRate = kFallbackSampleRate;
@jensenpat

Copy link
Copy Markdown
Collaborator

Thanks for chasing this down, but I don’t think we should support this change as-is now that #2615 has landed.

#2634 changes ClientPuduMonitor::startPlayback() so macOS tries a 48 kHz QAudioSink first and only falls back to the existing 24 kHz monitor rate. The PR rationale is that opening the monitor playback sink at 24 kHz can push Bluetooth headsets into HFP/telephony mode, so the monitor path should mirror the main macOS RX preference for 48 kHz. The actual diff does that by setting preferredRate = 48000 under Q_OS_MAC, with kSampleRate/24 kHz as fallback.

However, #2615 specifically changed the conclusion of this investigation. The root cause was not the Aetherial/Pudu recorder playback policy: the #2615 write-up says the recorder playback path was originally suspected because the crackle was easy to hear there, but that hypothesis was incomplete because AirPods Pro still crackled after changing recorder playback policy. The useful signal came from comparing CoreAudio HAL state against the Qt-selected capture format: failing Bluetooth headset runs had HAL exposing a low-rate Bluetooth voice input while AetherSDR/Qt opened the device at 48 kHz, causing an unwanted hidden native→48 kHz conversion before AetherSDR’s own 48→24 kHz conversion.

That means #2634 is addressing the symptom from the wrong side of the pipeline. #2615 intentionally made macOS Bluetooth mic capture trust the HAL-native low-rate input profile when CoreAudio reports no high-rate input mode, and only keeps the 48/44.1/24 kHz order for normal USB, built-in, or high-rate devices. The code added in #2615 resolves the Qt device UID to a HAL device, checks Bluetooth/BluetoothLE transport plus available nominal sample rates, returns a native 8/16/24 kHz rate for low-rate-only Bluetooth inputs, and prepends that rate to the TX input candidates. The docs were also updated to record that Bluetooth headset inputs reported as native 8/16/24 kHz-only should be opened at that native rate first.

So I would not add a second macOS-specific workaround in monitor playback that reintroduces a blanket “prefer 48 kHz for Bluetooth-ish behavior” rule. It risks encoding the old, superseded diagnosis in another path, while #2615 deliberately narrowed the behavior to the actual failing case: Bluetooth voice-input devices whose HAL capture side is low-rate-only. The validated clean cases in #2615 were exactly the opposite of a blanket 48 kHz preference: AirPods Pro became clean when opened at HAL-native 24 kHz, and Soundcore Life Q20 / Liberty 5 became clean when opened at HAL-native 16 kHz.

There is also a maintenance concern. #2615 gives us one place to reason about macOS Bluetooth voice-profile behavior: TX mic input negotiation. #2634 adds a separate output-path policy based on platform alone, not on CoreAudio transport type, actual HAL device capabilities, or whether the selected output is a Bluetooth voice-profile device. That makes future behavior harder to explain: the mic path becomes HAL-driven and conservative, while the monitor playback path becomes unconditional “macOS means 48 kHz first.” Those policies point in different directions.

I’d prefer closing #2634 as superseded by #2615 unless we can reproduce a remaining monitor-playback-only issue on current main after #2615. If such a bug still exists, the follow-up should be more targeted: collect current HAL/Qt input and output device state, confirm that the TX capture fix is active, and then decide whether the monitor sink needs a capability-based CoreAudio policy rather than a broad macOS 48 kHz preference.

@jensenpat jensenpat closed this May 13, 2026
ten9876 added a commit that referenced this pull request May 23, 2026
…#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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
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.

3 participants