Background
PR #2318 proposed wiring PanadapterStream::daxAudioReady → DaxBridge::feedDaxAudio with Qt::DirectConnection so DAX audio frames are delivered on PanadapterStream's network thread instead of being queued through the GUI event loop. Under heavy GUI load (multi-pan waterfall paints) the queued path adds 10-50 ms of variable, GUI-correlated latency before samples reach the DAX bridge — visible as WSJT-X DT jitter.
That PR is now stale (4 days no activity). The Linux dependency #2312 has already landed (v0.9.7), making PipeWireAudioBridge lock-free / atomic. But macOS VirtualAudioBridge was never audited for the same thread-safety, so DirectConnection would crash or warn on macOS.
Concrete blockers in VirtualAudioBridge
src/core/VirtualAudioBridge.cpp:243-300 has at least these unsafe-from-network-thread call sites in feedDaxAudio:
m_silenceTimer->stop() at line 254 — QTimer::stop() is not thread-safe; the timer is parented to VirtualAudioBridge which lives on the GUI thread.
m_transmitting = false at line 255 — plain bool member, would race with GUI-thread reads/writes.
m_channelGain[channel - 1] at line 277 — plain float array; gain changes from the GUI thread would race with the audio-thread read.
m_rxTiming[channel - 1] stats — stats.windowElapsed, stats.writtenSamples are non-atomic; any GUI-thread inspection (diagnostics dialog) would race.
Proposed fix
Mirror what #2312 did for PipeWireAudioBridge:
- Convert
m_open, m_transmitting, m_channelGain to std::atomic (atomic_bool, atomic).
- Convert relevant stats fields to atomics or document the read-races as best-effort diagnostics.
- Replace
m_silenceTimer->stop() from feedDaxAudio with an atomic flag (e.g. m_silenceFillEnabled) that the GUI-thread timer reads on each tick — when false, the timer self-stops next iteration. Set the flag from the audio thread; let the GUI thread observe it. No cross-thread QTimer manipulation.
- Audit any other QObject member calls in feedDaxAudio for cross-thread safety.
Probably ~30-50 lines, similar in shape to #2312.
Once this lands
PR #2318's one-line change (Qt::DirectConnection on the daxAudioReady → feedDaxAudio connect) becomes platform-agnostic and can ship cross-platform, removing the GUI-event-queue jitter on macOS too.
Workaround for the impatient
If we want the Linux latency improvement now, an #ifdef Q_OS_LINUX guard on the DirectConnection argument is a pragmatic stopgap. Linux uses PipeWireAudioBridge (already thread-safe per #2312), macOS retains queued semantics until this issue lands. But the per-platform divergence in the connect site is ugly; better to do the macOS audit first if there's bandwidth.
Test plan
References
Priority
Low-medium — DAX RX works on macOS today via the queued path. Latency improvement would benefit WSJT-X / FT8 operators on macOS during multi-pan use, but isn't a regression.
73, Jeremy KK7GWY & Claude (AI dev partner)
Background
PR #2318 proposed wiring
PanadapterStream::daxAudioReady → DaxBridge::feedDaxAudiowithQt::DirectConnectionso DAX audio frames are delivered on PanadapterStream's network thread instead of being queued through the GUI event loop. Under heavy GUI load (multi-pan waterfall paints) the queued path adds 10-50 ms of variable, GUI-correlated latency before samples reach the DAX bridge — visible as WSJT-X DT jitter.That PR is now stale (4 days no activity). The Linux dependency #2312 has already landed (v0.9.7), making
PipeWireAudioBridgelock-free / atomic. But macOSVirtualAudioBridgewas never audited for the same thread-safety, so DirectConnection would crash or warn on macOS.Concrete blockers in VirtualAudioBridge
src/core/VirtualAudioBridge.cpp:243-300has at least these unsafe-from-network-thread call sites infeedDaxAudio:m_silenceTimer->stop()at line 254 —QTimer::stop()is not thread-safe; the timer is parented to VirtualAudioBridge which lives on the GUI thread.m_transmitting = falseat line 255 — plain bool member, would race with GUI-thread reads/writes.m_channelGain[channel - 1]at line 277 — plain float array; gain changes from the GUI thread would race with the audio-thread read.m_rxTiming[channel - 1]stats —stats.windowElapsed,stats.writtenSamplesare non-atomic; any GUI-thread inspection (diagnostics dialog) would race.Proposed fix
Mirror what #2312 did for PipeWireAudioBridge:
m_open,m_transmitting,m_channelGaintostd::atomic(atomic_bool, atomic).m_silenceTimer->stop()fromfeedDaxAudiowith an atomic flag (e.g.m_silenceFillEnabled) that the GUI-thread timer reads on each tick — when false, the timer self-stops next iteration. Set the flag from the audio thread; let the GUI thread observe it. No cross-thread QTimer manipulation.Probably ~30-50 lines, similar in shape to #2312.
Once this lands
PR #2318's one-line change (
Qt::DirectConnectionon thedaxAudioReady → feedDaxAudioconnect) becomes platform-agnostic and can ship cross-platform, removing the GUI-event-queue jitter on macOS too.Workaround for the impatient
If we want the Linux latency improvement now, an
#ifdef Q_OS_LINUXguard on the DirectConnection argument is a pragmatic stopgap. Linux uses PipeWireAudioBridge (already thread-safe per #2312), macOS retains queued semantics until this issue lands. But the per-platform divergence in the connect site is ugly; better to do the macOS audit first if there's bandwidth.Test plan
References
Priority
Low-medium — DAX RX works on macOS today via the queued path. Latency improvement would benefit WSJT-X / FT8 operators on macOS during multi-pan use, but isn't a regression.
73, Jeremy KK7GWY & Claude (AI dev partner)