CW sidetone: PortAudio backend for sub-5ms latency on Linux/macOS#2075
Merged
ten9876 merged 1 commit intoApr 27, 2026
Merged
Conversation
The QAudioSink-based sidetone path requires a 50 ms buffer to keep
Pulse/PipeWire pull-mode from flapping Idle/Active, which puts net
perceived latency at ~25 ms — workable for slow CW, marginal at 25+ WPM.
PortAudio with paFramesPerBufferUnspecified bypasses the OS mixer entirely
and reaches sub-5 ms key-down → audible-onset on Linux PipeWire and
macOS CoreAudio.
Architecture:
- CwSidetoneSinkBackend (new abstract interface) — start/stop/isRunning/
actualRateHz/name. AudioEngine holds a unique_ptr<CwSidetoneSinkBackend>
and chooses the implementation at start time.
- CwSidetoneQAudioSink (extracted from AudioEngine.cpp) — the existing
Qt Multimedia push-mode + 2 ms timer code, refactored unchanged into
the new interface. Default fallback.
- CwSidetonePortAudioSink (new) — Pa_OpenStream with default low-latency
hint; paCallback invokes CwSidetoneGenerator::process() directly with
zero allocations and no Qt event loop.
Selection logic in AudioEngine::startSidetoneStream:
HAVE_PORTAUDIO && AppSettings["CwSidetoneBackend"] != "QAudioSink"
→ PortAudio (default on Linux + macOS)
otherwise → QAudioSink (Windows, or explicit fallback).
If PortAudio init fails on a quirky device (HFP, exotic USB DAC),
AudioEngine auto-retries with QAudioSink and logs a warning so the
user still gets working sidetone — just at the higher latency.
CwSidetoneGenerator unchanged — it was already callback-safe.
CMakeLists.txt: pkg_check_modules(PORTAUDIO portaudio-2.0) was already
present; just added CwSidetonePortAudioSink.cpp to the conditionally-
compiled source list. Dockerfile already includes portaudio19-dev.
Closes #2072.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The QAudioSink-based sidetone path requires a 50 ms buffer to keep
Pulse/PipeWire pull-mode from flapping Idle/Active, which puts net
perceived latency at ~25 ms — workable for slow CW, marginal at 25+ WPM.
PortAudio with paFramesPerBufferUnspecified bypasses the OS mixer entirely
and reaches sub-5 ms key-down → audible-onset on Linux PipeWire and
macOS CoreAudio.
Architecture:
actualRateHz/name. AudioEngine holds a unique_ptr
and chooses the implementation at start time.
Qt Multimedia push-mode + 2 ms timer code, refactored unchanged into
the new interface. Default fallback.
hint; paCallback invokes CwSidetoneGenerator::process() directly with
zero allocations and no Qt event loop.
Selection logic in AudioEngine::startSidetoneStream:
HAVE_PORTAUDIO && AppSettings["CwSidetoneBackend"] != "QAudioSink"
→ PortAudio (default on Linux + macOS)
otherwise → QAudioSink (Windows, or explicit fallback).
If PortAudio init fails on a quirky device (HFP, exotic USB DAC),
AudioEngine auto-retries with QAudioSink and logs a warning so the
user still gets working sidetone — just at the higher latency.
CwSidetoneGenerator unchanged — it was already callback-safe.
CMakeLists.txt: pkg_check_modules(PORTAUDIO portaudio-2.0) was already
present; just added CwSidetonePortAudioSink.cpp to the conditionally-
compiled source list. Dockerfile already includes portaudio19-dev.
Closes #2072.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com