Skip to content

fix(cw): prefer WASAPI host API for CW sidetone on Windows (#3193)#3241

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/3193-cw-sidetone-wasapi-preference
May 28, 2026
Merged

fix(cw): prefer WASAPI host API for CW sidetone on Windows (#3193)#3241
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/3193-cw-sidetone-wasapi-preference

Conversation

@M7HNF-Ian

Copy link
Copy Markdown
Contributor

Standalone split of Fix #2 from #3194, as requested by @jensenpat.

Problem

CwSidetonePortAudioSink picks audio output devices via PortAudio, but on Windows:

  1. defaultPortAudioOutputDevice() falls straight through to Pa_GetDefaultOutputDevice(), which returns an MME device (the first host API PortAudio enumerates). MME carries 50–150 ms OS-level buffering, producing audible CW timing jitter on fast keying.

  2. findPortAudioOutputDevice() matches devices by name substring. On Windows, the same physical device is enumerated three times under MME, DirectSound, and WASAPI. The old code treated this as an ambiguous match and returned paNoDevice — causing the sidetone to fall back to CwSidetoneQAudioSink and lose low-latency timing. When it did resolve, it picked whichever host API appeared first (MME).

Fix

defaultPortAudioOutputDevice() — add a WASAPI preference pass before the Pa_GetDefaultOutputDevice() fallback, mirroring the existing Linux JACK preference exactly. WASAPI shared mode runs at ~10 ms, vs 50–150 ms for MME.

findPortAudioOutputDevice() — collect all partial-match candidates, then resolve to the WASAPI entry when exactly one exists. Logs a clear qCInfo message showing which host API won. Falls back to the existing "matched multiple" warning only if multiple WASAPI entries are somehow present (pathological case).

Scope

CwSidetonePortAudioSink.cpp only. No changes to AudioEngine.cpp, no RX path changes, no buffer size changes.

Related

…#3193)

Two related changes to CwSidetonePortAudioSink, both Windows-only:

1. defaultPortAudioOutputDevice(): Pa_GetDefaultOutputDevice() on Windows
   typically returns an MME device (first enumerated host API), which
   carries 50–150 ms OS-level buffering. Add a WASAPI preference pass
   before the MME fallback, mirroring the existing Linux JACK preference.
   WASAPI shared mode runs at ~10 ms, eliminating the timing jitter that
   makes CW sidetone sound uneven on fast keying.

2. findPortAudioOutputDevice(): a single physical device appears under
   multiple PortAudio host APIs on Windows (MME, DirectSound, WASAPI).
   The old first-match logic would either pick MME (first enumerated) or
   return paNoDevice with an "ambiguous" warning when all three matched.
   Rewrite to collect all partial-match candidates and resolve to the
   WASAPI entry when exactly one exists, with a clear log message.
   Falls back to the existing ambiguous-match warning if multiple WASAPI
   entries are somehow present.

No change to AudioEngine.cpp or any non-Windows code path.

Refs aethersdr#3193
@M7HNF-Ian M7HNF-Ian requested a review from a team as a code owner May 28, 2026 06:48

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

Thanks @M7HNF-Ian — this is a well-scoped, well-explained fix. The two changes mirror each other nicely and the comments in the code make the why obvious (Windows multi-host-API enumeration, MME latency rationale). A few notes:

Logic LGTM

  • findPortAudioOutputDevice() — the refactor from "first match + ambiguity flag" to "collect all + pick WASAPI" is straightforward, and the behaviour for non-Windows or exactly-one-match cases is preserved. The paInDevelopment sentinel is a neat trick and the inline comment justifies it.
  • defaultPortAudioOutputDevice() — the WASAPI preference pass on Windows is a clean mirror of the existing Linux JACK block. Nice symmetry.

One small observation (non-blocking)

start() at CwSidetonePortAudioSink.cpp:161–167 re-inspects the device's host API after defaultPortAudioOutputDevice() returns and sets m_fallbackOccurred = true / m_fallbackReason = "backend selected JACK default output" so the summary logger surfaces JACK selection as a backend substitution. The new WASAPI default-selection path doesn't get the same surfacing.

I think that's actually defensible — on Windows, WASAPI is the expected low-latency target, not an override the operator needs flagged the way JACK is on Linux — and you do emit a qCInfo at the selection site. Mentioning it only so you can confirm it's intentional. If you wanted parity, you'd add a qstrncmp(api->name, "Windows WASAPI", 14) == 0 arm to the same if (device.isNull()) block, but I don't think it's necessary.

Minor / cosmetic

  • The Candidate::apiType field is populated on every platform but only read inside #ifdef Q_OS_WIN. Trivially wasteful (one PaHostApiTypeId per match candidate, on a list that's almost always tiny). Not worth changing.
  • partials.size() - 1 in the qCInfo log expression is fine — we only enter that block when partials.size() >= 2, so the value can't be negative.

Scope / conventions

  • Single file changed, exactly as described.
  • No QSettings / AppSettings touched; no RAII surface; no new resources to leak.
  • No null-deref risks introduced — every Pa_GetHostApiInfo / Pa_GetDeviceInfo return is checked before deref.
  • The #ifdef Q_OS_WIN guards around the WASAPI-specific code keep this from affecting Linux/macOS builds.

Looks ready to me. 73 from a fellow CW operator-in-spirit — fast-keying jitter is a real annoyance and this should make a noticeable difference. Thanks for splitting it out of #3194 as requested.


🤖 aethersdr-agent · cost: $5.5824 · model: claude-opus-4-7

@ten9876 ten9876 merged commit 1613fe8 into aethersdr:main May 28, 2026
6 checks passed
ten9876 added a commit that referenced this pull request May 28, 2026
## Summary

Drop the \`check-paths\` allow-list job that gated \`check-windows\` and
\`check-macos\` based on a hand-maintained set of files. Both
cross-platform CI builds now run on every PR.

## Why

The path-filter approach has been a chronic leaker. Every time a new
file gained a platform-guarded branch (\`#ifdef Q_OS_WIN\`, \`#ifdef
Q_OS_MAC\`, \`HAVE_PIPEWIRE\`, etc.) that wasn't on the allow-list, CI
silently skipped the relevant cross-platform check and the regression
surfaced at release tag time. The follow-up was always to add another
path to the filter — and then the same class of issue would resurface
elsewhere a few PRs later.

Pattern receipts:
- #796 — original \"MSVC issue slipped past CI\" lesson
- #2633#2662 — macOS \`m_daxBridge\` reference inside macOS-only
guard, missed
- #2670 — Windows behavior change not exercised by CI
- #2671 — added MainWindow.cpp/.h to filter after a miss
- #2929#3052 — WASAPI mono-mic recovery landed unverified; added
AudioEngine.cpp/.h
- #3210 — re-added AudioEngine after a relapse
- v26.5.3 — ClientPhaseRotator.cpp \`M_PI\` MSVC break slipped through
- #3241 (yesterday) — CwSidetonePortAudioSink WASAPI fix; entire
\`#ifdef Q_OS_WIN\` block never compiled by CI because the file wasn't
on the filter

## Trade-off

Cost: ~15-20 min of GitHub Actions runner time per PR (Windows + macOS
in parallel, both cached aggressively — cold ~5 min, warm ~2-3 min).

Benefit: a whole class of release-time regression we've been paying for
repeatedly.

## Test plan

- [x] YAML valid (\`python3 -c 'import yaml; yaml.safe_load(...)'\`)
- [ ] Confirm \`check-windows\` and \`check-macos\` actually run on this
PR (no longer marked \`skipping\`)
- [ ] Both pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@M7HNF-Ian M7HNF-Ian deleted the fix/3193-cw-sidetone-wasapi-preference branch June 7, 2026 14:58
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…#3193) (aethersdr#3241)

Standalone split of Fix aethersdr#2 from aethersdr#3194, as requested by @jensenpat.

## Problem

`CwSidetonePortAudioSink` picks audio output devices via PortAudio, but
on Windows:

1. **`defaultPortAudioOutputDevice()`** falls straight through to
`Pa_GetDefaultOutputDevice()`, which returns an MME device (the first
host API PortAudio enumerates). MME carries 50–150 ms OS-level
buffering, producing audible CW timing jitter on fast keying.

2. **`findPortAudioOutputDevice()`** matches devices by name substring.
On Windows, the same physical device is enumerated three times under
MME, DirectSound, and WASAPI. The old code treated this as an ambiguous
match and returned `paNoDevice` — causing the sidetone to fall back to
`CwSidetoneQAudioSink` and lose low-latency timing. When it did resolve,
it picked whichever host API appeared first (MME).

## Fix

**`defaultPortAudioOutputDevice()`** — add a WASAPI preference pass
before the `Pa_GetDefaultOutputDevice()` fallback, mirroring the
existing Linux JACK preference exactly. WASAPI shared mode runs at ~10
ms, vs 50–150 ms for MME.

**`findPortAudioOutputDevice()`** — collect all partial-match
candidates, then resolve to the WASAPI entry when exactly one exists.
Logs a clear `qCInfo` message showing which host API won. Falls back to
the existing "matched multiple" warning only if multiple WASAPI entries
are somehow present (pathological case).

## Scope

`CwSidetonePortAudioSink.cpp` only. No changes to `AudioEngine.cpp`, no
RX path changes, no buffer size changes.

## Related

- Partially addresses aethersdr#3193 (CW sidetone latency component)
- May also help aethersdr#2528 (TX→RX delay when stopping CW) and the latency
component of aethersdr#2694
- Sibling of the held aethersdr#3194 Fix aethersdr#1 (RX WASAPI ring cap — separate review
track)
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
## Summary

Drop the \`check-paths\` allow-list job that gated \`check-windows\` and
\`check-macos\` based on a hand-maintained set of files. Both
cross-platform CI builds now run on every PR.

## Why

The path-filter approach has been a chronic leaker. Every time a new
file gained a platform-guarded branch (\`#ifdef Q_OS_WIN\`, \`#ifdef
Q_OS_MAC\`, \`HAVE_PIPEWIRE\`, etc.) that wasn't on the allow-list, CI
silently skipped the relevant cross-platform check and the regression
surfaced at release tag time. The follow-up was always to add another
path to the filter — and then the same class of issue would resurface
elsewhere a few PRs later.

Pattern receipts:
- aethersdr#796 — original \"MSVC issue slipped past CI\" lesson
- aethersdr#2633aethersdr#2662 — macOS \`m_daxBridge\` reference inside macOS-only
guard, missed
- aethersdr#2670 — Windows behavior change not exercised by CI
- aethersdr#2671 — added MainWindow.cpp/.h to filter after a miss
- aethersdr#2929aethersdr#3052 — WASAPI mono-mic recovery landed unverified; added
AudioEngine.cpp/.h
- aethersdr#3210 — re-added AudioEngine after a relapse
- v26.5.3 — ClientPhaseRotator.cpp \`M_PI\` MSVC break slipped through
- aethersdr#3241 (yesterday) — CwSidetonePortAudioSink WASAPI fix; entire
\`#ifdef Q_OS_WIN\` block never compiled by CI because the file wasn't
on the filter

## Trade-off

Cost: ~15-20 min of GitHub Actions runner time per PR (Windows + macOS
in parallel, both cached aggressively — cold ~5 min, warm ~2-3 min).

Benefit: a whole class of release-time regression we've been paying for
repeatedly.

## Test plan

- [x] YAML valid (\`python3 -c 'import yaml; yaml.safe_load(...)'\`)
- [ ] Confirm \`check-windows\` and \`check-macos\` actually run on this
PR (no longer marked \`skipping\`)
- [ ] Both pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 pushed a commit that referenced this pull request Jun 30, 2026
…3897)

## Problem

RX speaker latency is higher than it needs to be, most visibly for
Windows users on external class-compliant USB audio interfaces (#3193),
where the TX→RX transition and general monitoring lag by 300–500 ms+.
Two independent code paths contribute:

1. **App-side RX buffer cap is conservative.** `m_rxBufferCapMs` has
defaulted to **200 ms** since it was introduced (#1505) as a cushion for
high-jitter links. For the common wired/LAN case that is avoidable
latency.

2. **The Windows RX sink never constrains its device buffer.**
`startRxStream()` constructs the RX `QAudioSink` and calls `start()`
with no prior `setBufferSize()`. WASAPI shared mode then applies the
device's *default* ring buffer, which for USB interfaces (Scarlett Solo,
Focusrite, etc.) is typically **100–300 ms**. That stacks on top of the
app-side cap.

## Changes

**1. Lower the RX buffer cap default 200 ms → 100 ms.**
The setting remains fully user-adjustable (50–1000 ms) for VPN/SmartLink
users who need a larger cushion — only the default changes, and existing
users who have set their own value keep it. Updated consistently in
three places: the engine default (`AudioEngine.h`), the settings load
(`MainWindow.cpp`), and the Radio Setup dialog field
(`RadioSetupDialog.cpp`).

**2. Set an explicit 50 ms device buffer on the Windows RX path.**
Before `start()`, on `Q_OS_WIN`, we call
`setBufferSize(candidate.bytesForDuration(50 ms))`. 50 ms is comfortably
fed by the existing 10 ms RX drain timer and mirrors the explicit
buffers already used by the sidetone and Quindar sinks. macOS/Linux are
unchanged (guarded by `#ifdef`).

Combined, these take the worst-case Windows USB path from ~300–500 ms
toward ~150 ms, without touching the high-jitter escape hatch.

## Credit

The Windows device-buffer approach (change 2) revives the RX half of the
original fix proposed by **@M7HNF-Ian** in #3194, which was never merged
pending on-hardware validation. Ian is credited as a co-author on the
commit. (The CW-sidetone half of #3194 shipped separately as #3241.)

## Testing

- Built clean on macOS (RelWithDebInfo, Qt 6 / Homebrew). The Windows
device-buffer block is `#ifdef Q_OS_WIN` and not exercised by the macOS
build.
- **Needs Windows on-hardware validation** with a USB interface (the
#3193 reporter's Scarlett Solo Gen4 is ideal) to confirm the latency
reduction and that 50 ms does not introduce underruns. Left as **draft**
pending that.

Addresses #3193.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Co-authored-by: Ian M7HNF <idimmock@gmail.com>
Co-authored-by: Claude Opus 4.8 <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.

PHONE applet: DEXP commands rejected on firmware v1.4.0.0

2 participants