Skip to content

fix(audio): silent SSB/voice TX on macOS — honour mic native rate + start capture for remote_audio_tx#2930

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
pepefrog1234:fix/macos-tx-mic-native-rate
May 23, 2026
Merged

fix(audio): silent SSB/voice TX on macOS — honour mic native rate + start capture for remote_audio_tx#2930
ten9876 merged 1 commit into
aethersdr:mainfrom
pepefrog1234:fix/macos-tx-mic-native-rate

Conversation

@pepefrog1234
Copy link
Copy Markdown
Contributor

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:

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

  • macOS + FLEX-8600 fw 4.x, mic_selection=PC, USB/LSB
  • 16 kHz USB webcam mic — voice TX now modulates (resampled 16→24 kHz)
  • 48 kHz USB audio interface — unchanged, still works
  • 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

…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>
Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

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.

@nigelfenton
Copy link
Copy Markdown
Contributor

As mentioned — happy to take a look on the Mac side.

Worth a cross-link with #2982 (just opened as a draft): same file (AudioEngine), same territory (macOS mic capture + TX audio path), opposite direction. That patch suppresses the local mic capture while a TCI client is feeding TX audio, to stop ambient mic packets from drowning out the TCI tone in the dax_tx packet stream. Different functions, no textual conflict expected — they should stack cleanly:

I've got the same class of webcam-default input device here (Mac mini Apple Silicon, macOS 26.x, FLEX 6300, AE on build/cal-2814 ≈ tx_gain+ALC #2950 + #2814 fix + #2982 stacked) so I can give defect #2 a direct check — will cherry-pick this on top and report back after I've worked through the Linux/Windows TCI Monitor cross-check today.

73, Nigel G0JKN

@nigelfenton
Copy link
Copy Markdown
Contributor

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:

AudioEngine: TX stream started -> 10.0.0.32 : 4991 streamId: 84000000
device: "HD Pro Webcam C920"
id: "AppleUSBAudioEngine:...:HD Pro Webcam C920:60CF881F:3"
rate: 32000 ch: 2 resample: true

C920's native rate is 32 kHz; Qt/CoreAudio previously claimed isFormatSupported(48000) == true but delivered zero samples. The preferredFormat() path picks 32 kHz, the resampler converts to 24 kHz internal. Clean.

Defect #1 (capture-start gate) — also fixed. Capture started despite there being no dax_tx stream for plain voice TX (the path used to require txStreamId() != 0).

End-to-end voice TX: slice in USB, mic_selection=PC, MOX → talked into the webcam → 9 W forward into the dummy load, clean SSB modulation visible in the panadapter (audio passband peak above the suppressed carrier), Phone/CW Level meter green throughout, PA warmed to 32.6 °C. Real transmit, not just keying.

Looks ready for review/merge from the macOS angle — happy to provide more detail or test variations on request.

73, Nigel G0JKN

@ten9876 ten9876 merged commit 07ea5e6 into aethersdr:main May 23, 2026
5 checks passed
@ten9876
Copy link
Copy Markdown
Collaborator

ten9876 commented May 23, 2026

Merged as 07ea5e6. Thanks @pepefrog1234 — the processedUSecs=0 / state=Active instrumentation table was the kind of evidence that turns a hard-to-reproduce 'no audio on macOS' bug into a two-line fix, and the layered diagnosis (capture-start gate vs. native-rate trap) was textbook. Each defect would have been hours of head-scratching on its own; both at once would have been hopeless without the probe data.

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 MainWindow.cpp:2353 or the macTxInputRateCandidates insertion point. 3-way merge applied cleanly. Cross-platform impact verified by walking every audioStartTx/startTxStream call site to confirm the micStateChanged gate was the sole buggy one (other sites either had the streamId just set or were restart-after-device-change paths).

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 txStreamId to non-zero and bypassed the buggy gate by accident).

73,
Jeremy KK7GWY & Claude (AI dev partner)

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.

3 participants