Skip to content

CW sidetone: PortAudio backend for sub-5ms latency on Linux/macOS#2075

Merged
ten9876 merged 1 commit into
mainfrom
auto/CW-sidetone-PortAudio-backend-for-sub-5ms-latency-
Apr 27, 2026
Merged

CW sidetone: PortAudio backend for sub-5ms latency on Linux/macOS#2075
ten9876 merged 1 commit into
mainfrom
auto/CW-sidetone-PortAudio-backend-for-sub-5ms-latency-

Conversation

@ten9876

@ten9876 ten9876 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

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

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>
@ten9876 ten9876 requested a review from jensenpat as a code owner April 27, 2026 03:30
@ten9876 ten9876 enabled auto-merge (squash) April 27, 2026 03:30
@ten9876 ten9876 merged commit aea0f37 into main Apr 27, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/CW-sidetone-PortAudio-backend-for-sub-5ms-latency- branch April 27, 2026 03:42
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.

CW sidetone: replace QAudioSink with PortAudio backend for sub-5ms latency

1 participant