fix(audio): silent SSB/voice TX on macOS — honour mic native rate + start capture for remote_audio_tx#2930
Conversation
…tart capture for remote_audio_tx Two independent defects left USB/LSB/AM/FM voice TX with no modulating audio on macOS while DAX digital-mode TX worked fine. 1. Mic capture never started without a dax_tx stream The mic_selection=PC handler in MainWindow only started QAudioSource capture when `m_audio->txStreamId() != 0`. txStreamId() is the *dax_tx* stream id, but voice TX flows over *remote_audio_tx*. With no DAX bridge running there is no dax_tx stream, so switching mic_selection to PC for plain SSB never started mic capture — onTxAudioReady never fired and the radio got no PC audio. Add AudioEngine::hasAnyTxStream() (dax_tx OR remote_audio_tx) and gate capture start on that instead. 2. QAudioSource opened at 48 kHz on a device that runs at 16 kHz macTxInputRateCandidates() tried 48000 first. CoreAudio reports isFormatSupported(48000)==true for capture devices that actually run at a lower native rate (e.g. USB webcam mics at 16 kHz), so QAudioSource "opened" successfully — state=Active, error=NoError — but the device delivered zero samples. processedUSecs stayed 0, the push-mode QBuffer never advanced (pos stuck at 0), and TX was silent. Query the device's preferredFormat().sampleRate() and try it first; the existing resampler then converts to 24 kHz radio-native. 48000/44100/24000 remain as fallbacks for devices with no usable preferred rate. Diagnosed by instrumenting onTxAudioReady (mic buffer pos stuck at 0 with QAudioSource state=Active) and QAudioSource::processedUSecs (stuck at 0), then confirming the device's native rate via system_profiler (Current SampleRate: 16000 on the affected webcam mic). Tested on macOS with FLEX-8600 fw 4.x: USB voice TX now produces audio with both a 16 kHz webcam mic and a 48 kHz USB audio interface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Nice diagnosis on both defects — the processedUSecs=0 with state=Active symptom is exactly the kind of CoreAudio quiet-failure that's hard to track down, and the fix is appropriately surgical.
Defect 1 (hasAnyTxStream()): The new gate correctly mirrors the consumer-side guard in onTxAudioReady() (AudioEngine.cpp:3970 and :3978), which already accepts either stream. Capture-start and capture-consume now agree on which streams matter, which is the right invariant.
One thing worth double-checking on Linux/Windows (already called out in your test plan): the remoteTxStreamReady handler at MainWindow.cpp:2192 only gates on isTxStreaming(), so on those platforms remote_audio_tx becoming ready while mic_selection=PC was already starting capture even before this PR. The new gate in the micStateChanged handler is the symmetric fix for the reverse order (stream ready first, then mic switched to PC). Behavior should be a strict superset of the old gate.
Defect 2 (preferred rate): Ordering puts preferredFormat().sampleRate() after macBluetoothNativeInputRate(), which is the right call — Bluetooth telephony rates are more authoritative for those devices, and macOS sometimes reports preferred=48000 for SCO inputs too. The > 0 && !contains() guard handles the "no usable preferred rate" case cleanly, and the existing m_txResampler (AudioEngine.cpp:4001-4023) does the rate→24kHz conversion regardless of the chosen candidate, so 16 kHz / 32 kHz / etc. all just work.
The change is #ifdef Q_OS_MAC so Linux/Windows are untouched for that piece.
Thanks for the thorough diagnostic write-up — the probe table makes it easy to reproduce and verify. Happy to leave the Linux/Windows voice-TX regression check to a reviewer with that hardware.
|
As mentioned — happy to take a look on the Mac side. Worth a cross-link with #2982 (just opened as a draft): same file (
I've got the same class of webcam-default input device here (Mac mini Apple Silicon, macOS 26.x, FLEX 6300, AE on 73, Nigel G0JKN |
|
Verified on Mac mini Apple Silicon, macOS 26.5.0, FLEX-6300 — both defects fixed end-to-end. Defect #2 (preferred sample rate) — caught directly in our TX-stream startup log: C920's native rate is 32 kHz; Qt/CoreAudio previously claimed Defect #1 (capture-start gate) — also fixed. Capture started despite there being no End-to-end voice TX: slice in USB, Looks ready for review/merge from the macOS angle — happy to provide more detail or test variations on request. 73, Nigel G0JKN |
|
Merged as 07ea5e6. Thanks @pepefrog1234 — the Stale-code audit against current main (today saw merges across the audio path — #2982 TCI mic suppression, #2973 audio summary logging, #2978 BYPASS flag, #2926 audio device prompts, #3004 limiter default) — none touched the buggy site at Independent verification by @nigelfenton on his Mac mini + FLEX-6300 + C920 setup (32 kHz native webcam mic) was the gold-standard test — 9 W forward power into a dummy load with clean SSB modulation, both defects fixed end-to-end. That kind of cross-tester hardware confirmation makes the merge call easy. Linux/Windows TX-voice users on a strict 'no DAX, plain SSB' setup will benefit too — the bug was just much less likely to surface on those platforms because most users have DAX enabled (which set 73, |
…ethersdr#3306) macOS + Linux: AudioEngine::startTxStream() now drives the mic rate/format selection from the factory's Int16 Input ladder (AudioDeviceNegotiator), walking stereo-then-mono to preserve the existing channel fallback. This removes the per-OS #ifdef rate lists and the bespoke macTxInputRateCandidates() helper — its policy (preferred-rate-first to dodge the silent-48k-open trap aethersdr#2930; Bluetooth-HFP native rate first aethersdr#2615) now lives in the one factory ladder. The macOS CoreAudio-HAL detection the factory can't derive (macBluetoothNativeInputRate) is fed in via a new preferredRateOverride on the wrapper, so a BT-HFP mic still opens at its native low rate. Behaviour preserved: standard macOS mic -> preferred 48k (then 44.1k/24k/16k); Linux -> native 24k (then 48k/44.1k); BT-HFP mic -> native low rate first. The existing TX resampler (m_txInputRate -> 24k) and the push/pull open modes, watchdog, and metering are unchanged. Windows mic is intentionally left as-is: it already matches the factory's Windows policy (force 48k + WASAPI probe-at-open) and carries the mono-only USB-mic channel clamp (aethersdr#2929) that warrants its own soak — the remaining mic increment. Builds & links clean into the full app; both negotiation tests still pass. Refs aethersdr#3306 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
…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>
…ethersdr#3306) macOS + Linux: AudioEngine::startTxStream() now drives the mic rate/format selection from the factory's Int16 Input ladder (AudioDeviceNegotiator), walking stereo-then-mono to preserve the existing channel fallback. This removes the per-OS #ifdef rate lists and the bespoke macTxInputRateCandidates() helper — its policy (preferred-rate-first to dodge the silent-48k-open trap aethersdr#2930; Bluetooth-HFP native rate first aethersdr#2615) now lives in the one factory ladder. The macOS CoreAudio-HAL detection the factory can't derive (macBluetoothNativeInputRate) is fed in via a new preferredRateOverride on the wrapper, so a BT-HFP mic still opens at its native low rate. Behaviour preserved: standard macOS mic -> preferred 48k (then 44.1k/24k/16k); Linux -> native 24k (then 48k/44.1k); BT-HFP mic -> native low rate first. The existing TX resampler (m_txInputRate -> 24k) and the push/pull open modes, watchdog, and metering are unchanged. Windows mic is intentionally left as-is: it already matches the factory's Windows policy (force 48k + WASAPI probe-at-open) and carries the mono-only USB-mic channel clamp (aethersdr#2929) that warrants its own soak — the remaining mic increment. Builds & links clean into the full app; both negotiation tests still pass. Refs aethersdr#3306 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
…ethersdr#3306) macOS + Linux: AudioEngine::startTxStream() now drives the mic rate/format selection from the factory's Int16 Input ladder (AudioDeviceNegotiator), walking stereo-then-mono to preserve the existing channel fallback. This removes the per-OS #ifdef rate lists and the bespoke macTxInputRateCandidates() helper — its policy (preferred-rate-first to dodge the silent-48k-open trap aethersdr#2930; Bluetooth-HFP native rate first aethersdr#2615) now lives in the one factory ladder. The macOS CoreAudio-HAL detection the factory can't derive (macBluetoothNativeInputRate) is fed in via a new preferredRateOverride on the wrapper, so a BT-HFP mic still opens at its native low rate. Behaviour preserved: standard macOS mic -> preferred 48k (then 44.1k/24k/16k); Linux -> native 24k (then 48k/44.1k); BT-HFP mic -> native low rate first. The existing TX resampler (m_txInputRate -> 24k) and the push/pull open modes, watchdog, and metering are unchanged. Windows mic is intentionally left as-is: it already matches the factory's Windows policy (force 48k + WASAPI probe-at-open) and carries the mono-only USB-mic channel clamp (aethersdr#2929) that warrants its own soak — the remaining mic increment. Builds & links clean into the full app; both negotiation tests still pass. Refs aethersdr#3306 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
…ry (#3306) (#3398) > **Stacks on #3397** (the pure foundation). The first two commits here are #3397's (foundation + live wrapper); the last two are the RX and TX-mic migrations. Merge #3397 first and this collapses to just those deltas. Kept as separate commits per the maintainer's one-change-at-a-time / soak rule (#3194 thread). ## What Layer 2 + the first two sink migrations of the consolidated audio sink factory (#3306): 1. **Live Qt-Multimedia wrapper** `AudioDeviceNegotiator` — the thin, and only, platform-specific bridge. `probe(QAudioDevice)` → pure `DeviceCaps`; `negotiate()`/`formatLadder()` → openable `QAudioFormat`s. Smoke-tested against the real default devices (`audio_device_negotiator_test`, 8/8). 2. **RX speaker** — `startRxStream()` walks the factory's Float ladder with real `start()` attempts instead of two forked per-OS `#ifdef` blocks (**net −87 lines**). `m_resampleTo48k` (bool) → `m_rxOutputRate` (int). 3. **PC mic (TX), macOS + Linux** — `startTxStream()` drives mic rate/format from the factory's Int16 Input ladder (stereo-then-mono), removing the per-OS rate lists and the bespoke `macTxInputRateCandidates()` helper. ## Behaviour (preserved) - **RX:** Windows/macOS → 48k (WASAPI 24k-resampler artifacts #2120; macOS A2DP off HFP), Linux → 24k native. New strictly-additive wins: a 44.1k-only output now works (#3306/#3385); the Quindar local monitor now opens on Windows (old branch `return`ed before `startQuindarLocalSink()`). - **TX mic:** standard macOS mic → 48k; Linux → 24k native; **Bluetooth-HFP mic → native low rate first (#2615)**, fed in via `macBluetoothNativeInputRate()` + the wrapper's new `preferredRateOverride`; preferred-rate-first avoids the silent-48k-open trap (#2930). ## Scope notes / invariants - Two stereo resampler strategies kept distinct: **PreservePan** (RX, dual L/R, VITA-49 pan #2403/#2459) vs **MonoCollapse** (RADE/TCI-TX). - macOS telephony/HFP **output** substitution (#1705) unchanged (device selection, before the ladder). - **Windows mic left as-is** — already matches the factory's force-48k + probe-at-open policy and carries the mono-only USB-mic channel clamp (#2929) that needs its own soak. That + the device-following `AudioOutputRouter`, TCI-TX client-rate, and PipeWire DAX are the remaining increments (see `docs/audio-sink-factory.md`). ## Test / build - `audio_format_negotiation_test` 25/25, `audio_device_negotiator_test` 8/8. - Full macOS app builds & links clean (RelWithDebInfo); deployed to the macOS test box. Soak on Windows/Linux requested before un-drafting. Refs #3306 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
…tart capture for remote_audio_tx (aethersdr#2930) ## Summary On macOS, USB/LSB/AM/FM **voice** TX produced no modulating audio (carrier up, dead air) even though DAX digital-mode TX worked fine. Two independent defects stacked together; both are fixed here. ## Defect 1 — mic capture never started without a dax_tx stream `MainWindow`'s `mic_selection` handler only started `QAudioSource` capture when `m_audio->txStreamId() != 0`: ```cpp if (!m_audio->isTxStreaming() && m_audio->txStreamId() != 0) { audioStartTx(...); } ``` `txStreamId()` is the **dax_tx** stream id. Voice TX flows over **remote_audio_tx** (Opus), a different stream. When no DAX bridge is running there is no dax_tx stream, so switching `mic_selection` to PC for plain SSB never started mic capture — `onTxAudioReady()` never fired and the radio received no PC audio. `onTxAudioReady()` itself already accepts either stream (`if (m_txStreamId == 0 && m_remoteTxStreamId == 0) return;`), so only the capture-start gate was wrong. **Fix:** add `AudioEngine::hasAnyTxStream()` (dax_tx **or** remote_audio_tx) and gate capture start on that. ## Defect 2 — QAudioSource opened at 48 kHz on a 16 kHz-native device `macTxInputRateCandidates()` tried `48000` first. macOS CoreAudio reports `isFormatSupported(48000) == true` for many capture devices that actually run at a lower native rate (e.g. USB webcam mics at 16 kHz). `QAudioSource` then "opens" successfully — `state=Active`, `error=NoError` — but the device delivers **zero samples**: - `QAudioSource::processedUSecs()` stays `0` - the push-mode `QBuffer` write position never advances (`pos()` stuck at `0`) - TX is silent **Fix:** query `QAudioDevice::preferredFormat().sampleRate()` and try it first. The existing resampler converts the device-native rate to 24 kHz radio-native. `48000/44100/24000` remain as fallbacks for devices that report no usable preferred rate. ## Diagnosis Instrumented the TX path on the affected machine: | Probe | Observation | |---|---| | `onTxAudioReady()` entry | fired continuously, but `m_micBuffer->pos()` stuck at `0` | | `QAudioSource` health @1s | `state=Active error=NoError processedUSecs=0` | | `system_profiler SPAudioDataType` | affected mic (`4K SlimFit Cam`) `Current SampleRate: 16000` | `processedUSecs=0` with `state=Active` is the tell-tale of CoreAudio accepting a format the device can't actually source. ## Test plan - [x] macOS + FLEX-8600 fw 4.x, mic_selection=PC, USB/LSB - [x] 16 kHz USB webcam mic — voice TX now modulates (resampled 16→24 kHz) - [x] 48 kHz USB audio interface — unchanged, still works - [x] DAX digital-mode TX (VARA) — unaffected (separate dax_tx path) - [ ] Reviewer: Linux/Windows regression check — `macTxInputRateCandidates()` is `#ifdef Q_OS_MAC`; the `hasAnyTxStream()` gate change is cross-platform and should be verified on a Windows/Linux voice TX setup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#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>
…ry (aethersdr#3306) (aethersdr#3398) > **Stacks on aethersdr#3397** (the pure foundation). The first two commits here are aethersdr#3397's (foundation + live wrapper); the last two are the RX and TX-mic migrations. Merge aethersdr#3397 first and this collapses to just those deltas. Kept as separate commits per the maintainer's one-change-at-a-time / soak rule (aethersdr#3194 thread). ## What Layer 2 + the first two sink migrations of the consolidated audio sink factory (aethersdr#3306): 1. **Live Qt-Multimedia wrapper** `AudioDeviceNegotiator` — the thin, and only, platform-specific bridge. `probe(QAudioDevice)` → pure `DeviceCaps`; `negotiate()`/`formatLadder()` → openable `QAudioFormat`s. Smoke-tested against the real default devices (`audio_device_negotiator_test`, 8/8). 2. **RX speaker** — `startRxStream()` walks the factory's Float ladder with real `start()` attempts instead of two forked per-OS `#ifdef` blocks (**net −87 lines**). `m_resampleTo48k` (bool) → `m_rxOutputRate` (int). 3. **PC mic (TX), macOS + Linux** — `startTxStream()` drives mic rate/format from the factory's Int16 Input ladder (stereo-then-mono), removing the per-OS rate lists and the bespoke `macTxInputRateCandidates()` helper. ## Behaviour (preserved) - **RX:** Windows/macOS → 48k (WASAPI 24k-resampler artifacts aethersdr#2120; macOS A2DP off HFP), Linux → 24k native. New strictly-additive wins: a 44.1k-only output now works (aethersdr#3306/aethersdr#3385); the Quindar local monitor now opens on Windows (old branch `return`ed before `startQuindarLocalSink()`). - **TX mic:** standard macOS mic → 48k; Linux → 24k native; **Bluetooth-HFP mic → native low rate first (aethersdr#2615)**, fed in via `macBluetoothNativeInputRate()` + the wrapper's new `preferredRateOverride`; preferred-rate-first avoids the silent-48k-open trap (aethersdr#2930). ## Scope notes / invariants - Two stereo resampler strategies kept distinct: **PreservePan** (RX, dual L/R, VITA-49 pan aethersdr#2403/aethersdr#2459) vs **MonoCollapse** (RADE/TCI-TX). - macOS telephony/HFP **output** substitution (aethersdr#1705) unchanged (device selection, before the ladder). - **Windows mic left as-is** — already matches the factory's force-48k + probe-at-open policy and carries the mono-only USB-mic channel clamp (aethersdr#2929) that needs its own soak. That + the device-following `AudioOutputRouter`, TCI-TX client-rate, and PipeWire DAX are the remaining increments (see `docs/audio-sink-factory.md`). ## Test / build - `audio_format_negotiation_test` 25/25, `audio_device_negotiator_test` 8/8. - Full macOS app builds & links clean (RelWithDebInfo); deployed to the macOS test box. Soak on Windows/Linux requested before un-drafting. Refs aethersdr#3306 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Summary
On macOS, USB/LSB/AM/FM voice TX produced no modulating audio (carrier up, dead air) even though DAX digital-mode TX worked fine. Two independent defects stacked together; both are fixed here.
Defect 1 — mic capture never started without a dax_tx stream
MainWindow'smic_selectionhandler only startedQAudioSourcecapture whenm_audio->txStreamId() != 0:txStreamId()is the dax_tx stream id. Voice TX flows over remote_audio_tx (Opus), a different stream. When no DAX bridge is running there is no dax_tx stream, so switchingmic_selectionto PC for plain SSB never started mic capture —onTxAudioReady()never fired and the radio received no PC audio.onTxAudioReady()itself already accepts either stream (if (m_txStreamId == 0 && m_remoteTxStreamId == 0) return;), so only the capture-start gate was wrong.Fix: add
AudioEngine::hasAnyTxStream()(dax_tx or remote_audio_tx) and gate capture start on that.Defect 2 — QAudioSource opened at 48 kHz on a 16 kHz-native device
macTxInputRateCandidates()tried48000first. macOS CoreAudio reportsisFormatSupported(48000) == truefor many capture devices that actually run at a lower native rate (e.g. USB webcam mics at 16 kHz).QAudioSourcethen "opens" successfully —state=Active,error=NoError— but the device delivers zero samples:QAudioSource::processedUSecs()stays0QBufferwrite position never advances (pos()stuck at0)Fix: query
QAudioDevice::preferredFormat().sampleRate()and try it first. The existing resampler converts the device-native rate to 24 kHz radio-native.48000/44100/24000remain as fallbacks for devices that report no usable preferred rate.Diagnosis
Instrumented the TX path on the affected machine:
onTxAudioReady()entrym_micBuffer->pos()stuck at0QAudioSourcehealth @1sstate=Active error=NoError processedUSecs=0system_profiler SPAudioDataType4K SlimFit Cam)Current SampleRate: 16000processedUSecs=0withstate=Activeis the tell-tale of CoreAudio accepting a format the device can't actually source.Test plan
macTxInputRateCandidates()is#ifdef Q_OS_MAC; thehasAnyTxStream()gate change is cross-platform and should be verified on a Windows/Linux voice TX setup.🤖 Generated with Claude Code