Skip to content

fix(audio): WASAPI mono-only USB PnP mic silent-open recovery (#2929). Principle VIII.#3036

Merged
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2929
May 24, 2026
Merged

fix(audio): WASAPI mono-only USB PnP mic silent-open recovery (#2929). Principle VIII.#3036
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2929

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #2929

What was changed

fix(audio): WASAPI mono-only USB PnP mic silent-open recovery (#2929). Principle VIII.

Files modified

  • src/core/AudioEngine.cpp
  • src/core/AudioEngine.h
 src/core/AudioEngine.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++++----
 src/core/AudioEngine.h   |  6 +++++
 2 files changed, 60 insertions(+), 4 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)


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

…. Principle VIII.

Generic USB PnP microphones (CMedia-class etc.) often expose a single
native capture channel. Qt's WASAPI backend accepts our unsupported
48 kHz stereo open without error -- QAudioSource::start() returns a
non-null QIODevice -- but then delivers zero bytes. The existing
fallback ladder is gated on start() returning null, so it never fires;
the mic meter stays flat and no PC audio reaches the radio.

Fix (contained to AudioEngine::startTxStream / onTxAudioReady):

1. Device-driven initial channel count. On Windows, clamp the initial
   open's channel count to dev.maximumChannelCount() so mono-only mics
   open as mono on the first attempt. Voicemeeter / FlexRadio DAX
   virtual devices still report >= 2 channels and continue to open at
   stereo -- the existing isFormatSupported()-bypass behaviour is
   preserved.

2. Fallback-ladder skip-check now compares (rate, ch) tuples instead of
   just rate. The old `if (rate == fmt.sampleRate()) continue;` skipped
   48 kHz entirely on retry, preventing a 48 kHz mono attempt for
   mono-only USB mics.

3. Silence watchdog. After a successful open on the Windows pull-mode
   path, arm a one-shot 1500 ms timer. If the source reports
   ActiveState but no bytes have arrived (m_txReceivedAnyBytes set in
   onTxAudioReady), log a clear warning, tear down, and reopen as mono.
   Bounded by m_txSilenceRetryDone so a still-silent mono open fails
   loudly instead of looping. Lifecycle-guarded by
   m_txLifecycleGeneration so a manual stop in between is a no-op.

Principle VIII: verification is by behaviour. The warning string
"TX source opened but produced no bytes in 1.5 s" is grep-able in the
support-bundle log so a maintainer can confirm the fix path was taken.
Principle I (FlexLib authority) is unaffected -- no SmartSDR command
or status surface is touched; the fix is entirely local to the Qt
audio open path.

Blast radius: risk_score=0.454, 18 high-risk affected (top:
MainWindow::MainWindow, MainWindow::wirePanadapter,
MainWindow::buildMenuBar). All affected symbols include AudioEngine.h
transitively and recompile; no public API or ABI surface changed --
three new private bool members and modifications confined to two
private member-function bodies. Callers of startTxStream(addr, port)
see the same return contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

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

Thanks for tackling this — the WASAPI silent-open behavior on mono-only USB mics is exactly the right diagnosis for #2929, and the changes hang together well. A couple of items worth a second look:

1. Watchdog QAudio::ActiveState check may miss the silent-open case

if (m_audioSource->state() != QAudio::ActiveState) return;

For the WASAPI silent-open bug, QAudioSource returns a non-null QIODevice but never delivers bytes. In that condition Qt's WASAPI plugin often transitions to QAudio::IdleState (capture buffer never fills), so this guard could cause the watchdog to bail out exactly when it's most needed — meaning the recovery only kicks in if the source happens to still be reported as Active at the 1.5 s mark.

The real signal is already !m_txReceivedAnyBytes. Consider either dropping the state check entirely or weakening it to state() == QAudio::StoppedState ? return : continue; so an Idle source still triggers the retry. If you've verified on real hardware that the broken state is Active, a short comment to that effect would lock the behavior in.

2. Fallback-loop skip fix is good and worth calling out

The change from if (rate == fmt.sampleRate()) continue; to skipping only the exact (rate, ch) combo is a real bug fix — the old loop refused to try 48 kHz mono when 48 kHz stereo had just failed, which is the exact combination a mono-only 48 kHz USB mic needs. Nice catch alongside the primary fix.

3. Lifecycle generation handshake — looks correct

I traced the m_txLifecycleGeneration capture/check across stopTxStream() → startTxStream() on the Windows path. The Windows start path doesn't bump the generation (only macOS does at line 4044), but stopTxStream() at line 4209 does, so the watchdog's watchdogGen != m_txLifecycleGeneration guard correctly invalidates the lambda when a stop has intervened. The one-shot m_txSilenceRetryDone flag also correctly prevents the post-retry start from re-arming the watchdog. Good.

Minor

  • The three new bool members are only touched on the AudioEngine thread (start/stop, readyRead slot, queued lambda), so no atomicity needed — worth a brief comment in the header if you want to make that ownership explicit, but not required.
  • QTimer::singleShot(1500, this, ...) correctly auto-cancels on destruction via the context object — good.

Otherwise the change is well-scoped and the diagnostics-style qCWarning line will make this case obvious in support bundles. Just the watchdog state check is the one item I'd ask you to verify before merge.


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

@ten9876 ten9876 merged commit eaeda16 into main May 24, 2026
5 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2929 branch May 24, 2026 13:44
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…sdr#2929). Principle VIII. (aethersdr#3036)

## Summary

Fixes aethersdr#2929

### What was changed

fix(audio): WASAPI mono-only USB PnP mic silent-open recovery (aethersdr#2929).
Principle VIII.

### Files modified

- `src/core/AudioEngine.cpp`
- `src/core/AudioEngine.h`

```
 src/core/AudioEngine.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++++----
 src/core/AudioEngine.h   |  6 +++++
 2 files changed, 60 insertions(+), 4 deletions(-)
```

---
Generated by AetherClaude (automated agent for AetherSDR)

---
<sub>🤖 aethersdr-agent · cost: $32.2212 · model: claude-opus-4-7</sub>

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <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.

AetherSDR does not recognize input from USB microphone.

1 participant