Skip to content

Prefer 48kHz RX audio sink on Windows to avoid WASAPI resampler artifacts (#2120)#2123

Merged
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2120
Apr 30, 2026
Merged

Prefer 48kHz RX audio sink on Windows to avoid WASAPI resampler artifacts (#2120)#2123
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2120

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #2120

What was changed

Prefer 48kHz RX audio sink on Windows to avoid WASAPI resampler artifacts (#2120)

Files modified

  • src/core/AudioEngine.cpp
 src/core/AudioEngine.cpp | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)

…acts (#2120)

WASAPI shared mode accepts 24kHz but delegates rate conversion internally,
introducing low-level artifacts that become audible when radio-side NR
(especially RNN) aggressively removes the noise floor. Swap the sink
preference to try 48kHz first, routing through r8brain for clean 24k→48k
conversion — matching the macOS TX-side approach for the same class of issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aethersdr-agent aethersdr-agent Bot requested a review from ten9876 as a code owner April 28, 2026 04:18
@aethersdr-agent aethersdr-agent Bot enabled auto-merge (squash) April 28, 2026 04:18

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Clean, well-scoped change. The logic correctly inverts the Windows RX sink preference from 24kHz-first to 48kHz-first, with 24kHz as fallback — mirroring the macOS path that already prefers 48kHz for the same class of resampler artifact issue.

A few observations:

  1. Correctness: The fmt starts at DEFAULT_SAMPLE_RATE (24000) from makeFormat(). The PR sets it to 48000 before the first sink attempt, and the fallback correctly resets to DEFAULT_SAMPLE_RATE with m_resampleTo48k = false. The flag and format stay consistent in both success paths. Looks good.

  2. Fallback log messages: Updated to match the new order ("48kHz failed, trying 24kHz" / "24kHz also failed"). Correct.

  3. Scope: Single file, only the Windows #ifdef Q_OS_WIN block is touched. No files outside the stated scope.

  4. No new resource or null-pointer risks: The sink creation/deletion pattern is unchanged — just the order is swapped. The existing delete m_audioSink + re-create pattern on fallback is preserved.

One minor note for the maintainer: the comment block was updated from describing WASAPI's transparent sample rate conversion to explaining why we now avoid relying on it. The new comment references the macOS TX-side fix and issue #2120, which is helpful context. Worth confirming the referenced macOS fix is the one in the non-Windows configureOutputFormat lambda (lines ~528-534) that already prefers 48kHz — it appears to be.

No bugs or convention issues found. Thanks for the fix, @aethersdr-agent — this is a clean, focused change. 👍

@ten9876 ten9876 merged commit 029613c into main Apr 30, 2026
5 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2120 branch April 30, 2026 04:31
ten9876 pushed a commit that referenced this pull request Jun 6, 2026
…3397)

## What

First, **pure-addition** step of the consolidated audio sink factory
(#3306): a single shared, OS-parameterized format/rate **negotiation
policy** that every audio sink and source will migrate onto, replacing
the ~9 divergent per-sink fallback ladders and per-OS `#ifdef` branches
that are the root of a recurring class of platform audio bugs.

Zero behaviour change — this is the maintainer-endorsed *"land the
helper first, migrate sinks incrementally"* approach (#3306, and the
#3194 thread's "sensitive audio-stack changes must be separate PRs with
soak time").

## Why this shape

The historical bugs escaped CI because the per-OS rate ladders were
compiled behind `Q_OS_*`, so a Linux runner could only ever exercise the
Linux path. The new policy is a **pure function over an injected
`DeviceCaps` snapshot with `TargetOs` as a parameter, not an `#ifdef`**
— so one headless, hardware-free test binary exercises the Windows,
macOS *and* Linux ladders on any runner.

## Contents

- **`src/core/AudioFormatNegotiator.{h,cpp}`** — dependency-free policy
(links only `Qt6::Core`). One ladder owns:
- Windows force-48k + r8brain (#2120/#2123), macOS 48k/A2DP (#1705),
Linux native-24k — divergences preserved as **data**, not `#ifdef`.
- The **universal 44.1 kHz rung** that RX/Quindar/QSO are missing today
(#3385).
- `Int16`↔`Float32` + `preferredFormat()` catch-all (#2669 / #1090 /
#3231).
- macOS mic preferred-rate-first (#2930) and Bluetooth-HFP native rate
(#2615); Windows probe-at-open (#2929).
- The two stereo resampler strategies — **PreservePan** (RX/QSO) vs
**MonoCollapse** (TCI-TX/RADE) — kept deliberately distinct
(#2403/#2459).
- **`tests/audio_format_negotiation_test.cpp`** — table-driven golden
matrix, registered under CTest. **25/25 pass**, including the
44.1k-only-device regression guard.
- **`docs/audio-sink-factory.md`** — the full three-layer design (pure
policy → live Qt wrapper → device-ownership router), the
device-following **"uncoupling"** fix (CW / RADE / Aetherial-Audio
"Pudu" recorder following the selected output), the regression-guard
invariants, and the one-sink-per-PR migration plan.

## Test

```
$ ./build/audio_format_negotiation_test
...
25/25 checks passed
```

## Follow-ups (per the migration plan in the doc)

Live Qt wrapper → migrate RX speaker → migrate PC mic →
`AudioOutputRouter` (closes the uncoupling class) → TCI-TX client-rate →
PipeWire DAX shared resampler. Each a separate, soakable PR.

Refs #3306

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

Co-authored-by: Codex <noreply@openai.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…#3306) (aethersdr#3397)

## What

First, **pure-addition** step of the consolidated audio sink factory
(aethersdr#3306): a single shared, OS-parameterized format/rate **negotiation
policy** that every audio sink and source will migrate onto, replacing
the ~9 divergent per-sink fallback ladders and per-OS `#ifdef` branches
that are the root of a recurring class of platform audio bugs.

Zero behaviour change — this is the maintainer-endorsed *"land the
helper first, migrate sinks incrementally"* approach (aethersdr#3306, and the
aethersdr#3194 thread's "sensitive audio-stack changes must be separate PRs with
soak time").

## Why this shape

The historical bugs escaped CI because the per-OS rate ladders were
compiled behind `Q_OS_*`, so a Linux runner could only ever exercise the
Linux path. The new policy is a **pure function over an injected
`DeviceCaps` snapshot with `TargetOs` as a parameter, not an `#ifdef`**
— so one headless, hardware-free test binary exercises the Windows,
macOS *and* Linux ladders on any runner.

## Contents

- **`src/core/AudioFormatNegotiator.{h,cpp}`** — dependency-free policy
(links only `Qt6::Core`). One ladder owns:
- Windows force-48k + r8brain (aethersdr#2120/aethersdr#2123), macOS 48k/A2DP (aethersdr#1705),
Linux native-24k — divergences preserved as **data**, not `#ifdef`.
- The **universal 44.1 kHz rung** that RX/Quindar/QSO are missing today
(aethersdr#3385).
- `Int16`↔`Float32` + `preferredFormat()` catch-all (aethersdr#2669 / aethersdr#1090 /
aethersdr#3231).
- macOS mic preferred-rate-first (aethersdr#2930) and Bluetooth-HFP native rate
(aethersdr#2615); Windows probe-at-open (aethersdr#2929).
- The two stereo resampler strategies — **PreservePan** (RX/QSO) vs
**MonoCollapse** (TCI-TX/RADE) — kept deliberately distinct
(aethersdr#2403/aethersdr#2459).
- **`tests/audio_format_negotiation_test.cpp`** — table-driven golden
matrix, registered under CTest. **25/25 pass**, including the
44.1k-only-device regression guard.
- **`docs/audio-sink-factory.md`** — the full three-layer design (pure
policy → live Qt wrapper → device-ownership router), the
device-following **"uncoupling"** fix (CW / RADE / Aetherial-Audio
"Pudu" recorder following the selected output), the regression-guard
invariants, and the one-sink-per-PR migration plan.

## Test

```
$ ./build/audio_format_negotiation_test
...
25/25 checks passed
```

## Follow-ups (per the migration plan in the doc)

Live Qt wrapper → migrate RX speaker → migrate PC mic →
`AudioOutputRouter` (closes the uncoupling class) → TCI-TX client-rate →
PipeWire DAX shared resampler. Each a separate, soakable PR.

Refs aethersdr#3306

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

Co-authored-by: Codex <noreply@openai.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.

Garbled RNN audio when RNN is accessed from AetherSDR

1 participant