Skip to content

Commit e61826f

Browse files
committed
MiniAudioStream: improve stability
1 parent 23a42ef commit e61826f

File tree

1 file changed

+184
-37
lines changed

1 file changed

+184
-37
lines changed

src/AudioTools/AudioLibs/MiniAudioStream.h

Lines changed: 184 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "AudioTools.h"
99
#include <mutex>
10+
#include <atomic>
1011

1112
#define MINIAUDIO_IMPLEMENTATION
1213
#include "miniaudio.h"
@@ -41,6 +42,8 @@ class MiniAudioConfig : public AudioInfo {
4142
int delay_ms_if_buffer_full = MA_DELAY;
4243
int buffer_count = MA_BUFFER_COUNT;
4344
int buffer_start_count = MA_START_COUNT;
45+
bool auto_restart_on_underrun = true; // Automatically restart after buffer underrun
46+
int underrun_tolerance = 5; // Number of empty reads before stopping playback
4447
};
4548

4649
/**
@@ -86,9 +89,9 @@ class MiniAudioStream : public AudioStream {
8689
in.channels != config.channels ||
8790
in.bits_per_sample != config.bits_per_sample) {
8891
config.copyFrom(in);
89-
if (is_active) {
90-
is_active = false;
91-
is_playing = false;
92+
if (is_active.load()) {
93+
is_active.store(false);
94+
is_playing.store(false);
9295
// This will stop the device so no need to do that manually.
9396
ma_device_uninit(&device_ma);
9497

@@ -144,35 +147,55 @@ class MiniAudioStream : public AudioStream {
144147
// The device is sleeping by default so you'll need to start it manually.
145148
if (ma_device_start(&device_ma) != MA_SUCCESS) {
146149
// Failed to initialize the device.
150+
ma_device_uninit(&device_ma);
147151
return false;
148152
}
149153

150-
is_active = true;
151-
return is_active;
154+
is_active.store(true);
155+
return is_active.load();
152156
}
153157

154158
void end() override {
155-
is_active = false;
156-
is_playing = false;
159+
is_active.store(false);
160+
is_playing.store(false);
157161
// This will stop the device so no need to do that manually.
158162
ma_device_uninit(&device_ma);
159163
// release buffer memory
160164
buffer_in.resize(0);
161165
buffer_out.resize(0);
166+
is_buffers_setup.store(false);
162167
}
163168

164169
int availableForWrite() override {
165170
return buffer_out.size() == 0 ? 0 : DEFAULT_BUFFER_SIZE;
166171
}
167172

168173
size_t write(const uint8_t *data, size_t len) override {
169-
if (buffer_out.size() == 0) return 0;
174+
// Input validation
175+
if (!data || len == 0) {
176+
LOGW("Invalid write parameters: data=%p, len=%zu", data, len);
177+
return 0;
178+
}
179+
180+
if (buffer_out.size() == 0) {
181+
LOGW("Output buffer not initialized");
182+
return 0;
183+
}
184+
185+
if (!is_active.load()) {
186+
LOGW("Stream not active");
187+
return 0;
188+
}
189+
170190
LOGD("write: %zu", len);
171191

172192
// write data to buffer
173193
int open = len;
174194
int written = 0;
175-
while (open > 0) {
195+
int retry_count = 0;
196+
const int max_retries = 1000; // Prevent infinite loops
197+
198+
while (open > 0 && retry_count < max_retries) {
176199
size_t result = 0;
177200
{
178201
std::lock_guard<std::mutex> guard(write_mtx);
@@ -181,15 +204,44 @@ class MiniAudioStream : public AudioStream {
181204
written += result;
182205
}
183206

184-
if (result != len) doWait();
207+
if (result == 0) {
208+
retry_count++;
209+
doWait();
210+
} else {
211+
retry_count = 0; // Reset on successful write
212+
}
213+
}
214+
215+
if (retry_count >= max_retries) {
216+
LOGE("Write timeout after %d retries, written %d of %zu bytes", max_retries, written, len);
185217
}
186218

187219
// activate playing
188220
// if (!is_playing && buffer_out.bufferCountFilled()>=MA_START_COUNT) {
189-
if (!is_playing && buffer_out.available() >= config.buffer_start_count * buffer_size) {
190-
LOGI("starting audio");
191-
is_playing = true;
221+
int current_buffer_size = buffer_size.load();
222+
bool should_start_playing = false;
223+
224+
// Start playing if we have enough data and either:
225+
// 1. We're not playing yet, or
226+
// 2. We stopped due to buffer underrun but now have data again
227+
if (current_buffer_size > 0) {
228+
int available_data = buffer_out.available();
229+
int threshold = config.buffer_start_count * current_buffer_size;
230+
231+
if (!is_playing.load() && available_data >= threshold) {
232+
should_start_playing = true;
233+
} else if (is_playing.load() && available_data == 0) {
234+
// Stop playing if buffer is completely empty (helps with long delays)
235+
LOGW("Buffer empty, pausing playback");
236+
is_playing.store(false);
237+
}
238+
}
239+
240+
if (should_start_playing) {
241+
LOGI("starting audio playback");
242+
is_playing.store(true);
192243
}
244+
193245
// std::this_thread::yield();
194246
return written;
195247
}
@@ -199,40 +251,100 @@ class MiniAudioStream : public AudioStream {
199251
}
200252

201253
size_t readBytes(uint8_t *data, size_t len) override {
202-
if (buffer_in.size() == 0) return 0;
203-
LOGD("write: %zu", len);
254+
if (!data || len == 0) {
255+
LOGW("Invalid read parameters: data=%p, len=%zu", data, len);
256+
return 0;
257+
}
258+
259+
if (buffer_in.size() == 0) {
260+
LOGW("Input buffer not initialized");
261+
return 0;
262+
}
263+
264+
if (!is_active.load()) {
265+
LOGW("Stream not active");
266+
return 0;
267+
}
268+
269+
LOGD("read: %zu", len);
204270
std::lock_guard<std::mutex> guard(read_mtx);
205271
return buffer_in.readArray(data, len);
206272
}
207273

274+
/// Manually restart playback (useful after long delays)
275+
void restartPlayback() {
276+
if (!is_active.load()) {
277+
LOGW("Cannot restart playback - stream not active");
278+
return;
279+
}
280+
281+
int current_buffer_size = buffer_size.load();
282+
if (current_buffer_size > 0 && buffer_out.available() > 0) {
283+
LOGI("Manually restarting playback");
284+
is_playing.store(true);
285+
} else {
286+
LOGW("Cannot restart playback - no data available");
287+
}
288+
}
289+
290+
/// Check if playback is currently active
291+
bool isPlaying() const {
292+
return is_playing.load();
293+
}
294+
208295
protected:
209296
MiniAudioConfig config;
210297
ma_device_config config_ma;
211298
ma_device device_ma;
212-
bool is_playing = false;
213-
bool is_active = false;
214-
bool is_buffers_setup = false;
299+
std::atomic<bool> is_playing{false};
300+
std::atomic<bool> is_active{false};
301+
std::atomic<bool> is_buffers_setup{false};
215302
RingBuffer<uint8_t> buffer_out{0};
216303
RingBuffer<uint8_t> buffer_in{0};
217304
std::mutex write_mtx;
218305
std::mutex read_mtx;
219-
int buffer_size = 0;
306+
std::atomic<int> buffer_size{0};
220307

221308
// In playback mode copy data to pOutput. In capture mode read data from
222309
// pInput. In full-duplex mode, both pOutput and pInput will be valid and
223310
// you can move data from pInput into pOutput. Never process more than
224311
// frameCount frames.
225312

226313
void setupBuffers(int size) {
227-
if (is_buffers_setup) return;
228-
buffer_size = size;
314+
std::lock_guard<std::mutex> guard(write_mtx);
315+
if (is_buffers_setup.load()) return;
316+
317+
// Validate buffer size
318+
if (size <= 0 || size > 1024 * 1024) { // Max 1MB per buffer chunk
319+
LOGE("Invalid buffer size: %d", size);
320+
return;
321+
}
322+
323+
buffer_size.store(size);
229324
int buffer_count = config.buffer_count;
230-
LOGI("setupBuffers: %d * %d", size, buffer_count);
231-
if (buffer_out.size() == 0 && config.is_output)
232-
buffer_out.resize(size * buffer_count);
233-
if (buffer_in.size() == 0 && config.is_input)
234-
buffer_in.resize(size * buffer_count);
235-
is_buffers_setup = true;
325+
326+
// Validate total buffer size to prevent excessive memory allocation
327+
size_t total_size = static_cast<size_t>(size) * buffer_count;
328+
if (total_size > 100 * 1024 * 1024) { // Max 100MB total
329+
LOGE("Buffer size too large: %zu bytes", total_size);
330+
return;
331+
}
332+
333+
LOGI("setupBuffers: %d * %d = %zu bytes", size, buffer_count, total_size);
334+
335+
if (buffer_out.size() == 0 && config.is_output) {
336+
if (!buffer_out.resize(size * buffer_count)) {
337+
LOGE("Failed to resize output buffer");
338+
return;
339+
}
340+
}
341+
if (buffer_in.size() == 0 && config.is_input) {
342+
if (!buffer_in.resize(size * buffer_count)) {
343+
LOGE("Failed to resize input buffer");
344+
return;
345+
}
346+
}
347+
is_buffers_setup.store(true);
236348
}
237349

238350
void doWait() {
@@ -244,42 +356,77 @@ class MiniAudioStream : public AudioStream {
244356
static void data_callback(ma_device *pDevice, void *pOutput,
245357
const void *pInput, ma_uint32 frameCount) {
246358
MiniAudioStream *self = (MiniAudioStream *)pDevice->pUserData;
359+
if (!self || !self->is_active.load()) {
360+
return; // Safety check
361+
}
362+
247363
AudioInfo cfg = self->audioInfo();
364+
if (cfg.channels == 0 || cfg.bits_per_sample == 0) {
365+
LOGE("Invalid audio configuration in callback");
366+
return;
367+
}
248368

249369
int bytes = frameCount * cfg.channels * cfg.bits_per_sample / 8;
370+
if (bytes <= 0 || bytes > 1024 * 1024) { // Sanity check
371+
LOGE("Invalid byte count in callback: %d", bytes);
372+
return;
373+
}
374+
250375
self->setupBuffers(bytes);
251376

252-
if (pInput) {
377+
if (pInput && self->buffer_in.size() > 0) {
253378
int open = bytes;
254379
int processed = 0;
255-
while (open > 0) {
380+
int retry_count = 0;
381+
const int max_retries = 100;
382+
383+
while (open > 0 && retry_count < max_retries && self->is_active.load()) {
256384
int len = 0;
257385
{
258386
std::unique_lock<std::mutex> guard(self->read_mtx);
259-
int len =
260-
self->buffer_in.writeArray((uint8_t *)pInput + processed, open);
387+
len = self->buffer_in.writeArray((uint8_t *)pInput + processed, open);
261388
open -= len;
262389
processed += len;
263390
}
264-
if (len == 0) self->doWait();
391+
if (len == 0) {
392+
retry_count++;
393+
self->doWait();
394+
} else {
395+
retry_count = 0;
396+
}
265397
}
266398
}
267399

268400
if (pOutput) {
269401
memset(pOutput, 0, bytes);
270-
if (self->is_playing) {
402+
if (self->is_playing.load() && self->buffer_out.size() > 0) {
271403
int open = bytes;
272404
int processed = 0;
273-
while (open > 0) {
405+
int consecutive_failures = 0;
406+
const int max_failures = self->config.underrun_tolerance;
407+
408+
while (open > 0 && self->is_active.load()) {
274409
size_t len = 0;
275410
{
276411
std::lock_guard<std::mutex> guard(self->write_mtx);
277-
len = self->buffer_out.readArray((uint8_t *)pOutput + processed,
278-
bytes);
412+
len = self->buffer_out.readArray((uint8_t *)pOutput + processed, open);
279413
open -= len;
280414
processed += len;
281415
}
282-
if (len != bytes) self->doWait();
416+
417+
if (len == 0) {
418+
consecutive_failures++;
419+
// If we can't get data for too long, stop playing to prevent issues
420+
if (consecutive_failures >= max_failures && self->config.auto_restart_on_underrun) {
421+
LOGW("Buffer underrun detected, stopping playback");
422+
self->is_playing.store(false);
423+
break;
424+
}
425+
// Don't wait in callback for too long - just output silence
426+
break;
427+
} else {
428+
consecutive_failures = 0;
429+
}
283430
}
284431
}
285432
}

0 commit comments

Comments
 (0)