Skip to content

Commit e9b223a

Browse files
committed
Use queued input capture on Windows to avoid callback-thread crash
1 parent 3fd5d0e commit e9b223a

File tree

2 files changed

+82
-36
lines changed

2 files changed

+82
-36
lines changed

src/gui/audio_engine.cpp

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ bool AudioEngine::openInput(const std::string& device) {
133133
want.callback = inputCallback;
134134
want.userdata = this;
135135

136+
#ifdef _WIN32
137+
// Win10 low-end stability: prefer queued capture (no SDL input callback thread).
138+
// Main thread will poll/dequeue via getRxSamples().
139+
want.callback = nullptr;
140+
want.userdata = nullptr;
141+
input_queue_mode_ = true;
142+
#else
143+
input_queue_mode_ = false;
144+
#endif
145+
136146
const char* dev_name = (device.empty() || device == "Default") ? nullptr : device.c_str();
137147

138148
input_device_ = SDL_OpenAudioDevice(dev_name, 1, &want, &have, 0);
@@ -142,8 +152,11 @@ bool AudioEngine::openInput(const std::string& device) {
142152
return false;
143153
}
144154

145-
LOG_MODEM(INFO, "AudioEngine: Opened INPUT device: %s (id=%d, rate=%d)",
146-
dev_name ? dev_name : "Default", input_device_, have.freq);
155+
LOG_MODEM(INFO, "AudioEngine: Opened INPUT device: %s (id=%d, rate=%d, mode=%s)",
156+
dev_name ? dev_name : "Default", input_device_, have.freq,
157+
input_queue_mode_ ? "queue" : "callback");
158+
input_dc_x_prev_ = 0.0f;
159+
input_dc_y_prev_ = 0.0f;
147160
return true;
148161
}
149162

@@ -161,6 +174,7 @@ void AudioEngine::closeInput() {
161174
input_device_ = 0;
162175
}
163176
capturing_ = false;
177+
input_queue_mode_ = false;
164178
}
165179

166180
void AudioEngine::queueTxSamples(const std::vector<float>& samples) {
@@ -212,6 +226,25 @@ size_t AudioEngine::getTxQueueSize() const {
212226
}
213227

214228
std::vector<float> AudioEngine::getRxSamples(size_t max_samples) {
229+
if (input_queue_mode_ && input_device_ != 0) {
230+
// Dequeue capture data in main thread (no SDL callback thread).
231+
Uint32 queued_bytes = SDL_GetQueuedAudioSize(input_device_);
232+
if (queued_bytes > 0) {
233+
size_t queued_samples = queued_bytes / sizeof(float);
234+
// Drain a bounded chunk each poll to keep latency low without UI stalls.
235+
size_t drain_samples = std::min(queued_samples, std::max<size_t>(max_samples * 4, max_samples));
236+
if (drain_samples > 0) {
237+
std::vector<float> raw(drain_samples, 0.0f);
238+
Uint32 got_bytes = SDL_DequeueAudio(input_device_, raw.data(),
239+
static_cast<Uint32>(drain_samples * sizeof(float)));
240+
size_t got_samples = got_bytes / sizeof(float);
241+
if (got_samples > 0) {
242+
appendCapturedSamples(raw.data(), got_samples, input_gain_.load());
243+
}
244+
}
245+
}
246+
}
247+
215248
std::lock_guard<AudioEngineMutex> lock(rx_mutex_);
216249

217250
size_t count = std::min(max_samples, rx_buffer_.size());
@@ -297,44 +330,15 @@ void AudioEngine::inputCallback(void* userdata, Uint8* stream, int len) {
297330
const float* input = reinterpret_cast<const float*>(stream);
298331
int samples = len / sizeof(float);
299332

300-
// Apply input gain and DC blocking filter
301-
// DC blocker: y[n] = x[n] - x[n-1] + alpha * y[n-1]
302-
// This removes DC offset that can cause false sync detection
303-
static float dc_x_prev = 0.0f;
304-
static float dc_y_prev = 0.0f;
305-
constexpr float DC_ALPHA = 0.995f;
306-
307-
float gain = engine->input_gain_.load();
308-
std::vector<float> captured(samples);
309-
for (int i = 0; i < samples; ++i) {
310-
float x = input[i] * gain;
311-
float y = x - dc_x_prev + DC_ALPHA * dc_y_prev;
312-
dc_x_prev = x;
313-
dc_y_prev = y;
314-
captured[i] = y;
315-
}
316-
317-
// Compute input level (RMS) after gain
318-
float sum_sq = 0.0f;
319-
for (int i = 0; i < samples; ++i) {
320-
sum_sq += captured[i] * captured[i];
321-
}
322-
float rms = std::sqrt(sum_sq / samples);
323-
engine->input_level_ = rms;
333+
engine->appendCapturedSamples(input, static_cast<size_t>(samples), engine->input_gain_.load());
324334

335+
std::vector<float> captured;
325336
{
326337
std::lock_guard<AudioEngineMutex> lock(engine->rx_mutex_);
327-
328-
// Cap buffer size to prevent unbounded growth if main loop stalls
329-
if (engine->rx_buffer_.size() + captured.size() > MAX_RX_BUFFER_SAMPLES) {
330-
size_t to_remove = engine->rx_buffer_.size() + captured.size() - MAX_RX_BUFFER_SAMPLES;
331-
if (to_remove >= engine->rx_buffer_.size()) {
332-
engine->rx_buffer_.clear();
333-
} else {
334-
engine->rx_buffer_.erase(engine->rx_buffer_.begin(), engine->rx_buffer_.begin() + to_remove);
335-
}
338+
size_t count = std::min<size_t>(samples, engine->rx_buffer_.size());
339+
if (count > 0) {
340+
captured.assign(engine->rx_buffer_.end() - count, engine->rx_buffer_.end());
336341
}
337-
engine->rx_buffer_.insert(engine->rx_buffer_.end(), captured.begin(), captured.end());
338342
}
339343

340344
// Notify via callback (skip if muted during TX)
@@ -348,6 +352,44 @@ void AudioEngine::inputCallback(void* userdata, Uint8* stream, int len) {
348352
}
349353
}
350354

355+
void AudioEngine::appendCapturedSamples(const float* input, size_t samples, float gain) {
356+
if (!input || samples == 0) {
357+
return;
358+
}
359+
360+
// Apply input gain and DC blocking filter.
361+
constexpr float DC_ALPHA = 0.995f;
362+
363+
std::vector<float> captured(samples);
364+
for (size_t i = 0; i < samples; ++i) {
365+
float x = input[i] * gain;
366+
float y = x - input_dc_x_prev_ + DC_ALPHA * input_dc_y_prev_;
367+
input_dc_x_prev_ = x;
368+
input_dc_y_prev_ = y;
369+
captured[i] = y;
370+
}
371+
372+
float sum_sq = 0.0f;
373+
for (float s : captured) {
374+
sum_sq += s * s;
375+
}
376+
float rms = std::sqrt(sum_sq / static_cast<float>(samples));
377+
input_level_ = rms;
378+
379+
std::lock_guard<AudioEngineMutex> lock(rx_mutex_);
380+
381+
// Cap buffer size to prevent unbounded growth if main loop stalls.
382+
if (rx_buffer_.size() + captured.size() > MAX_RX_BUFFER_SAMPLES) {
383+
size_t to_remove = rx_buffer_.size() + captured.size() - MAX_RX_BUFFER_SAMPLES;
384+
if (to_remove >= rx_buffer_.size()) {
385+
rx_buffer_.clear();
386+
} else {
387+
rx_buffer_.erase(rx_buffer_.begin(), rx_buffer_.begin() + to_remove);
388+
}
389+
}
390+
rx_buffer_.insert(rx_buffer_.end(), captured.begin(), captured.end());
391+
}
392+
351393
void AudioEngine::addChannelNoise(std::vector<float>& samples) {
352394
float snr_db = loopback_snr_db_.load();
353395

src/gui/audio_engine.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,11 @@ class AudioEngine {
117117

118118
// Add noise for loopback channel simulation
119119
void addChannelNoise(std::vector<float>& samples);
120+
void appendCapturedSamples(const float* input, size_t samples, float gain);
120121

121122
SDL_AudioDeviceID output_device_ = 0;
122123
SDL_AudioDeviceID input_device_ = 0;
124+
bool input_queue_mode_ = false;
123125

124126
// TX buffer (samples waiting to be played)
125127
std::queue<float> tx_queue_;
@@ -160,6 +162,8 @@ class AudioEngine {
160162

161163
// Input gain (0.0 to 2.0, default 1.0)
162164
std::atomic<float> input_gain_{1.0f};
165+
float input_dc_x_prev_ = 0.0f;
166+
float input_dc_y_prev_ = 0.0f;
163167

164168
// Output gain (0.0 to 1.0, default 1.0)
165169
std::atomic<float> output_gain_{1.0f};

0 commit comments

Comments
 (0)