aethersdr / aethersdr/AetherSDR
AudioEngine::audioEndpointDiagnostics() reads audio-thread state cross-thread (TX capture health snapshot)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Description
Non-blocking follow-up from the review of #4233 / #4251 (both merged). Flagged by @aethersdr-agent and confirmed independently.
Problem
AudioEngine::audioEndpointDiagnostics() (src/core/AudioEngine.cpp) is const and invoked from AutomationServer (get audio) and DeviceDiagnostics on the main/bridge thread, but AudioEngine lives on m_audioThread (MainWindow.cpp m_audio->moveToThread(m_audioThread)). The method reads state that is owned and mutated on the audio thread:
m_txCaptureHealth.snapshot()— theTxCaptureHealthTracker's non-atomic members (m_tciSuppressedCallbacks,m_lastMicReadMs,m_currentlySaturated, …), written inonTxAudioReady,recordMicRead, and the queuedstateChangedlambda;txCaptureBufferedBytes()→m_micDevice->bytesAvailable(), which races the Linux drain'sm_micDevice->readAll()(#4251) mutating the sameQIODevice;txCaptureBufferCapacityBytes()→m_audioSource->bufferSize(); and (macOS)m_micBuffer->size().
So the bridge thread reads while the audio thread writes → a data race on non-atomic members. Torn reads are diagnostic-only (benign counter values, and m_micDevice is a QPointer so it degrades to null rather than dangling), which is why it wasn't blocking.
Why it's worth fixing
It's consistent with a pre-existing pattern in the same method (it already reads m_audioSource->state() / error(), m_micDevice->isOpen() cross-thread), but the tell is the asymmetry the author deliberately created in #4233/#4251: recordLocalTxAttempt is marshaled onto the owning thread "so diagnostics cannot race readyRead/stateChanged" — yet the read side of the very same data is not marshaled. bytesAvailable() racing readAll() on one QIODevice is also a genuinely unsafe concurrent Qt-object call, not just a torn scalar.
Options
- Marshal the whole snapshot onto the audio thread (
QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection)) and return the assembled values — cleanest, matches the write-side discipline. - Or expose the diagnostic-relevant fields as
std::atomicand copy the buffer byte counts under a small mutex. - Ideally fix the whole method, not just the TX-capture-health additions, so the pre-existing
state()/isOpen()reads are covered too.
Low priority / diagnostic-only. Ref: #4233, #4251.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/core/AudioEngine.cpp at AudioEngine::audioEndpointDiagnostics(), then read MainWindow.cpp and the AutomationServer and DeviceDiagnostics callers to understand thread ownership. Trace the related TX capture state and buffer access, including the write paths mentioned in the issue. Done means the diagnostic snapshot no longer reads audio-thread-owned state or Qt objects concurrently, including the pre-existing reads.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- audio-video-rtc, desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100