Skip to content

fix(audio): cut RX speaker latency for USB and local audio (#3193)#3897

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3193-rx-audio-latency
Jun 30, 2026
Merged

fix(audio): cut RX speaker latency for USB and local audio (#3193)#3897
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3193-rx-audio-latency

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

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 (Live retransmitted audio is distorted while recorded file is clean (AetherSDR 0.8.15.1) #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 External USB Audio Interface and Windows 11, latency is a huge issue #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

@jensenpat jensenpat force-pushed the fix/3193-rx-audio-latency branch from 2bb5b29 to 9a6666a Compare June 29, 2026 01:43
@jensenpat jensenpat marked this pull request as ready for review June 29, 2026 01:54
@jensenpat jensenpat requested review from a team as code owners June 29, 2026 01:54
@jensenpat jensenpat linked an issue Jun 29, 2026 that may be closed by this pull request
2 tasks

@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 for this, @jensenpat — nicely scoped and the writeup is excellent. I checked the diff against the code and it's correct on the mechanics:

  • bytesForDuration(kWinRxDeviceBufferMs * 1000LL) uses the right units (µs → 50 ms), the if (winRxBufBytes > 0) guard covers the invalid-format 0 return, and setBufferSize() is correctly placed before start() inside the ladder loop (fresh sink per candidate, so it applies on every attempt). ✅
  • The default-lowering is consistent across the three places the diff touches (AudioEngine.h, MainWindow.cpp, RadioSetupDialog.cpp), and the effectiveBufMs std::max(...) against the Flex/Kiwi presentation delays means the lower default can't undercut the presentation-buffering floors — good. ✅
  • The "mirrors the sidetone/Quindar sinks" claim checks out: CwSidetoneQAudioSink.cpp:97 does set an explicit 50 ms buffer. Worth noting it tops up on a 2 ms timer there, whereas the RX drain timer is 10 ms — so 50 ms gives the RX ring ~5 refills per drain vs. the sidetone's ~25. Still reasonable, and you've correctly flagged that this needs the Windows/USB on-hardware soak before it leaves draft.

Two non-blocking points:

  1. Stale comments left behind. The PR updates the default value but not the prose that describes it. After this change these now read wrong:

    • AudioEngine.cpp:1173"caps buffer at ~200ms"
    • AudioEngine.cpp:1182-1183"Default 200ms, user-adjustable..."
    • AudioEngine.h:738"keeps its 200 ms jitter cushion"

    Quick sweep to 100 ms would keep them honest.

  2. The Windows device buffer is a fixed 50 ms, not tied to the user-adjustable cap. The app-side cap exists as the escape hatch for high-jitter links (50–1000 ms), but a SmartLink/VPN user who raises it still gets a hardcoded 50 ms WASAPI ring. Network jitter is absorbed by the app-side queue, so that's mostly fine — but the device ring is still exposed to local scheduler stalls, and those users now have no knob to enlarge it. Worth considering scaling it with m_rxBufferCapMs (e.g. min/a fraction) rather than a flat constant, so the escape hatch fully controls device-side latency too. Fine to defer.

Mechanically sound and conservatively guarded — the remaining risk is exactly the underrun question your Windows-validation gate is there to answer. (Minor: no CHANGELOG entry, if the repo expects one for behavioural defaults.)


🤖 aethersdr-agent · cost: $6.5985 · model: claude-opus-4-8

Comment thread src/core/AudioEngine.cpp
Two independent contributors to RX speaker latency are addressed:

1. App-side RX buffer cap default lowered 200ms -> 100ms. The cap was
   originally set to 200ms (aethersdr#1505) as a conservative cushion for
   high-jitter links. For the common wired/LAN case that is avoidable
   latency. The setting remains user-adjustable (50-1000ms) for
   VPN/SmartLink users who need a larger cushion.

2. Explicit WASAPI shared-mode ring buffer on the Windows RX path. The
   RX QAudioSink was started with no setBufferSize(), so class-compliant
   USB interfaces inherited their driver's default ring (typically
   100-300ms), which stacked on top of the app-side cap and produced
   300-500ms+ speaker latency. We now request a 50ms device buffer
   before start() on Windows, comfortably fed by the existing 10ms RX
   drain timer and mirroring the explicit buffers already used by the
   sidetone and Quindar sinks.

The Windows device-buffer approach revives the RX half of the original
fix proposed in aethersdr#3194 by Ian M7HNF, which was never merged pending
on-hardware validation.

Defaults updated consistently in the engine, the MainWindow settings
load, and the Radio Setup dialog.

Co-Authored-By: Ian M7HNF <idimmock@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensenpat jensenpat force-pushed the fix/3193-rx-audio-latency branch from 9a6666a to f34717f Compare June 30, 2026 05:12

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving as-is per maintainer request. RX speaker-latency fix for USB/local audio (#3193). Enabling auto-merge (squash) so it lands once the pending build/check-windows finish green.

@ten9876 ten9876 enabled auto-merge (squash) June 30, 2026 05:20
@ten9876 ten9876 merged commit f99842e into aethersdr:main Jun 30, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jul 2, 2026
#3961)

## Release documentation prep for **v26.7.1** (2026-07-02)

29 commits since v26.6.5. Docs-only + version bump — no code behavior
change.

### Version bump 26.6.5 → 26.7.1
The three canonical locations (per `AGENTS.md`):
- `CMakeLists.txt:2` — `project(AetherSDR VERSION 26.7.1)` →
`AETHERSDR_VERSION` (the app's version string)
- `README.md` — Current version line
- `AGENTS.md` — Current version line

### `CHANGELOG.md`
Promoted `[Unreleased]` → **`## [v26.7.1] — 2026-07-02`** in house style
(v-prefix, em-dash, "N commits since…" opener + headline), authored from
all merged PRs and categorized:
- **Added:** 3D stacked-trace spectrum (#3899), 3D dBm scale (#3937),
in-process NVIDIA BNR + AFX download-on-demand (#3902), TX meter
mouse-over readouts (#3936), SWR manual sweep range (#3885), CW sidetone
capture (#3895), KiwiSDR metadata scaffolding (#3898), bridge verbs
(#3920)
- **Changed:** BNR container/NIM backend removed, 60 fps + per-pixel GPU
FFT trace (#3958), FlexLib-sourced model capabilities (#3954/#2177), RX
speaker-latency cut (#3897), Display pane sections (#3935)
- **Fixed:** #3922, #3926, #3941, #3940, #3942, #3921, #3903, #3892,
#3924, #3891, #3890, #3900, #3947
- Fresh empty `[Unreleased]` restored above the release block.
Internal/CI/docs/self-fix PRs (#3931/#3916/#3934/#3929) omitted per
house style.

### `ROADMAP.md`
- Cycle header → "post-v26.7.1"
- NVIDIA BNR removed from **In flight** (shipped this cycle)
- Five v26.7.1 highlights prepended to **Recently shipped**

### `README.md`
- Highlights: GPU spectrum bullet now notes the **per-pixel FFT trace at
up to 60 fps** and the **3D stacked-trace** spectrum mode

### Reviewer notes
- Touches protected paths (`*.md` + `AGENTS.md` Tier-1 +
`CMakeLists.txt`) — needs `@aethersdr/infrastructure` sign-off.
- Tagging `v26.7.1` and cutting the release build are **not** part of
this PR (docs prep only).

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

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.

External USB Audio Interface and Windows 11, latency is a huge issue

2 participants