Skip to content

Commit bb5395d

Browse files
committed
Harden Win10 audio path by moving RX feed off SDL callback thread
1 parent 2b697d2 commit bb5395d

File tree

4 files changed

+81
-21
lines changed

4 files changed

+81
-21
lines changed

src/gui/app.cpp

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,10 @@ void App::render() {
14911491
ultra::gui::startupTrace("App", "render-set-output-gain-exit");
14921492
}
14931493

1494+
// Process captured RX audio in the main thread.
1495+
// Avoids feeding modem state directly from SDL callback threads.
1496+
pollRadioRx();
1497+
14941498
// Safe-startup mode: auto-start audio shortly after first frame.
14951499
// Keeps process bring-up lightweight while preserving "auto-listen" behavior.
14961500
if (deferred_audio_auto_init_pending_ &&
@@ -1719,12 +1723,8 @@ void App::startRadioRx() {
17191723
return;
17201724
}
17211725

1722-
audio_.setRxCallback([this](const std::vector<float>& samples) {
1723-
modem_.feedAudio(samples);
1724-
if (waterfall_) {
1725-
waterfall_->addSamples(samples.data(), samples.size());
1726-
}
1727-
});
1726+
// Main thread polls captured samples via pollRadioRx().
1727+
audio_.setRxCallback(AudioEngine::RxCallback{});
17281728

17291729
audio_.setLoopbackEnabled(false);
17301730
audio_.startCapture();
@@ -1734,9 +1734,31 @@ void App::startRadioRx() {
17341734
void App::stopRadioRx() {
17351735
audio_.stopCapture();
17361736
audio_.closeInput();
1737+
audio_.setRxCallback(AudioEngine::RxCallback{});
17371738
radio_rx_enabled_ = false;
17381739
}
17391740

1741+
void App::pollRadioRx() {
1742+
if (!audio_initialized_ || simulation_enabled_ || !radio_rx_enabled_) {
1743+
return;
1744+
}
1745+
1746+
// Bounded drain per frame to keep UI responsive while preventing RX backlog.
1747+
constexpr size_t kChunkSamples = 2048;
1748+
constexpr int kMaxChunksPerFrame = 8;
1749+
for (int i = 0; i < kMaxChunksPerFrame; ++i) {
1750+
auto samples = audio_.getRxSamples(kChunkSamples);
1751+
if (samples.empty()) {
1752+
break;
1753+
}
1754+
1755+
modem_.feedAudio(samples);
1756+
if (waterfall_) {
1757+
waterfall_->addSamples(samples.data(), samples.size());
1758+
}
1759+
}
1760+
}
1761+
17401762
void App::stopTxNow(const char* reason) {
17411763
size_t dropped_audio = audio_.getTxQueueSize();
17421764
if (dropped_audio > 0) {

src/gui/app.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ class App {
197197
void updateAdaptiveAdvisory(float snr_db, float fading_index);
198198
void startRadioRx();
199199
void stopRadioRx();
200+
void pollRadioRx();
200201

201202
// Helper to get device name from settings (returns empty string for "Default")
202203
std::string getInputDeviceName() const;

src/gui/audio_engine.cpp

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ std::vector<std::string> AudioEngine::getInputDevices() {
8181
return devices;
8282
}
8383

84+
void AudioEngine::setRxCallback(RxCallback callback) {
85+
std::lock_guard<AudioEngineMutex> lock(rx_callback_mutex_);
86+
rx_callback_ = std::move(callback);
87+
}
88+
8489
bool AudioEngine::openOutput(const std::string& device) {
8590
if (!initialized_) {
8691
if (!initialize()) return false;
@@ -159,7 +164,7 @@ void AudioEngine::closeInput() {
159164
}
160165

161166
void AudioEngine::queueTxSamples(const std::vector<float>& samples) {
162-
std::lock_guard<std::mutex> lock(tx_mutex_);
167+
std::lock_guard<AudioEngineMutex> lock(tx_mutex_);
163168
for (float s : samples) {
164169
tx_queue_.push(s);
165170
}
@@ -170,7 +175,7 @@ void AudioEngine::queueTxSamples(const std::vector<float>& samples) {
170175
std::vector<float> loopback_samples = samples;
171176
addChannelNoise(loopback_samples);
172177

173-
std::lock_guard<std::mutex> rx_lock(rx_mutex_);
178+
std::lock_guard<AudioEngineMutex> rx_lock(rx_mutex_);
174179

175180
// Cap buffer size to prevent unbounded growth
176181
if (rx_buffer_.size() + loopback_samples.size() > MAX_RX_BUFFER_SAMPLES) {
@@ -191,23 +196,23 @@ void AudioEngine::queueTxSamples(const std::vector<float>& samples) {
191196
}
192197

193198
void AudioEngine::clearTxQueue() {
194-
std::lock_guard<std::mutex> lock(tx_mutex_);
199+
std::lock_guard<AudioEngineMutex> lock(tx_mutex_);
195200
std::queue<float> empty;
196201
std::swap(tx_queue_, empty);
197202
}
198203

199204
bool AudioEngine::isTxQueueEmpty() const {
200-
std::lock_guard<std::mutex> lock(tx_mutex_);
205+
std::lock_guard<AudioEngineMutex> lock(tx_mutex_);
201206
return tx_queue_.empty();
202207
}
203208

204209
size_t AudioEngine::getTxQueueSize() const {
205-
std::lock_guard<std::mutex> lock(tx_mutex_);
210+
std::lock_guard<AudioEngineMutex> lock(tx_mutex_);
206211
return tx_queue_.size();
207212
}
208213

209214
std::vector<float> AudioEngine::getRxSamples(size_t max_samples) {
210-
std::lock_guard<std::mutex> lock(rx_mutex_);
215+
std::lock_guard<AudioEngineMutex> lock(rx_mutex_);
211216

212217
size_t count = std::min(max_samples, rx_buffer_.size());
213218
std::vector<float> samples(rx_buffer_.begin(), rx_buffer_.begin() + count);
@@ -217,7 +222,7 @@ std::vector<float> AudioEngine::getRxSamples(size_t max_samples) {
217222
}
218223

219224
size_t AudioEngine::getRxBufferSize() const {
220-
std::lock_guard<std::mutex> lock(rx_mutex_);
225+
std::lock_guard<AudioEngineMutex> lock(rx_mutex_);
221226
return rx_buffer_.size();
222227
}
223228

@@ -250,7 +255,7 @@ void AudioEngine::stopCapture() {
250255
}
251256

252257
void AudioEngine::clearRxBuffer() {
253-
std::lock_guard<std::mutex> lock(rx_mutex_);
258+
std::lock_guard<AudioEngineMutex> lock(rx_mutex_);
254259
rx_buffer_.clear();
255260
}
256261

@@ -267,7 +272,7 @@ void AudioEngine::outputCallback(void* userdata, Uint8* stream, int len) {
267272
float sum_sq = 0.0f;
268273
float gain = engine->output_gain_.load();
269274

270-
std::lock_guard<std::mutex> lock(engine->tx_mutex_);
275+
std::lock_guard<AudioEngineMutex> lock(engine->tx_mutex_);
271276

272277
for (int i = 0; i < samples; ++i) {
273278
float out = 0.0f;
@@ -318,7 +323,7 @@ void AudioEngine::inputCallback(void* userdata, Uint8* stream, int len) {
318323
engine->input_level_ = rms;
319324

320325
{
321-
std::lock_guard<std::mutex> lock(engine->rx_mutex_);
326+
std::lock_guard<AudioEngineMutex> lock(engine->rx_mutex_);
322327

323328
// Cap buffer size to prevent unbounded growth if main loop stalls
324329
if (engine->rx_buffer_.size() + captured.size() > MAX_RX_BUFFER_SAMPLES) {
@@ -333,8 +338,13 @@ void AudioEngine::inputCallback(void* userdata, Uint8* stream, int len) {
333338
}
334339

335340
// Notify via callback (skip if muted during TX)
336-
if (engine->rx_callback_ && !engine->rx_muted_.load()) {
337-
engine->rx_callback_(captured);
341+
AudioEngine::RxCallback rx_cb;
342+
{
343+
std::lock_guard<AudioEngineMutex> cb_lock(engine->rx_callback_mutex_);
344+
rx_cb = engine->rx_callback_;
345+
}
346+
if (rx_cb && !engine->rx_muted_.load()) {
347+
rx_cb(captured);
338348
}
339349
}
340350

src/gui/audio_engine.hpp

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,36 @@
77
#include <atomic>
88
#include <string>
99
#include <functional>
10+
#ifdef _WIN32
11+
#ifndef NOMINMAX
12+
#define NOMINMAX
13+
#endif
14+
#include <windows.h>
15+
#ifdef ERROR
16+
#undef ERROR
17+
#endif
18+
#endif
1019

1120
namespace ultra {
1221
namespace gui {
1322

23+
#ifdef _WIN32
24+
class AudioEngineMutex {
25+
public:
26+
AudioEngineMutex() noexcept = default;
27+
AudioEngineMutex(const AudioEngineMutex&) = delete;
28+
AudioEngineMutex& operator=(const AudioEngineMutex&) = delete;
29+
30+
void lock() noexcept { AcquireSRWLockExclusive(&lock_); }
31+
void unlock() noexcept { ReleaseSRWLockExclusive(&lock_); }
32+
33+
private:
34+
SRWLOCK lock_ = SRWLOCK_INIT;
35+
};
36+
#else
37+
using AudioEngineMutex = std::mutex;
38+
#endif
39+
1440
// Audio engine for real-time audio I/O using SDL2
1541
class AudioEngine {
1642
public:
@@ -80,7 +106,7 @@ class AudioEngine {
80106

81107
// Callback for when RX has data ready
82108
using RxCallback = std::function<void(const std::vector<float>&)>;
83-
void setRxCallback(RxCallback callback) { rx_callback_ = callback; }
109+
void setRxCallback(RxCallback callback);
84110

85111
private:
86112
// SDL audio callbacks
@@ -95,11 +121,11 @@ class AudioEngine {
95121

96122
// TX buffer (samples waiting to be played)
97123
std::queue<float> tx_queue_;
98-
mutable std::mutex tx_mutex_;
124+
mutable AudioEngineMutex tx_mutex_;
99125

100126
// RX buffer (captured samples)
101127
std::vector<float> rx_buffer_;
102-
mutable std::mutex rx_mutex_;
128+
mutable AudioEngineMutex rx_mutex_;
103129

104130
// Loopback settings
105131
std::atomic<bool> loopback_enabled_{false};
@@ -121,6 +147,7 @@ class AudioEngine {
121147

122148
// RX callback
123149
RxCallback rx_callback_;
150+
mutable AudioEngineMutex rx_callback_mutex_;
124151

125152
// Random generator for noise
126153
uint32_t noise_seed_ = 12345;

0 commit comments

Comments
 (0)