fix(audio): cut RX speaker latency for USB and local audio (#3193)#3897
Conversation
2bb5b29 to
9a6666a
Compare
There was a problem hiding this comment.
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), theif (winRxBufBytes > 0)guard covers the invalid-format0return, andsetBufferSize()is correctly placed beforestart()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 theeffectiveBufMsstd::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:97does 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:
-
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.
-
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
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>
9a6666a to
f34717f
Compare
#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>
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:
App-side RX buffer cap is conservative.
m_rxBufferCapMshas 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.The Windows RX sink never constrains its device buffer.
startRxStream()constructs the RXQAudioSinkand callsstart()with no priorsetBufferSize(). 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(), onQ_OS_WIN, we callsetBufferSize(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
#ifdef Q_OS_WINand not exercised by the macOS build.Addresses #3193.
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat