7
7
8
8
#include " AudioTools.h"
9
9
#include < mutex>
10
+ #include < atomic>
10
11
11
12
#define MINIAUDIO_IMPLEMENTATION
12
13
#include " miniaudio.h"
@@ -41,6 +42,8 @@ class MiniAudioConfig : public AudioInfo {
41
42
int delay_ms_if_buffer_full = MA_DELAY;
42
43
int buffer_count = MA_BUFFER_COUNT;
43
44
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
44
47
};
45
48
46
49
/* *
@@ -86,9 +89,9 @@ class MiniAudioStream : public AudioStream {
86
89
in.channels != config.channels ||
87
90
in.bits_per_sample != config.bits_per_sample ) {
88
91
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 ) ;
92
95
// This will stop the device so no need to do that manually.
93
96
ma_device_uninit (&device_ma);
94
97
@@ -144,35 +147,55 @@ class MiniAudioStream : public AudioStream {
144
147
// The device is sleeping by default so you'll need to start it manually.
145
148
if (ma_device_start (&device_ma) != MA_SUCCESS) {
146
149
// Failed to initialize the device.
150
+ ma_device_uninit (&device_ma);
147
151
return false ;
148
152
}
149
153
150
- is_active = true ;
151
- return is_active;
154
+ is_active. store ( true ) ;
155
+ return is_active. load () ;
152
156
}
153
157
154
158
void end () override {
155
- is_active = false ;
156
- is_playing = false ;
159
+ is_active. store ( false ) ;
160
+ is_playing. store ( false ) ;
157
161
// This will stop the device so no need to do that manually.
158
162
ma_device_uninit (&device_ma);
159
163
// release buffer memory
160
164
buffer_in.resize (0 );
161
165
buffer_out.resize (0 );
166
+ is_buffers_setup.store (false );
162
167
}
163
168
164
169
int availableForWrite () override {
165
170
return buffer_out.size () == 0 ? 0 : DEFAULT_BUFFER_SIZE;
166
171
}
167
172
168
173
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
+
170
190
LOGD (" write: %zu" , len);
171
191
172
192
// write data to buffer
173
193
int open = len;
174
194
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) {
176
199
size_t result = 0 ;
177
200
{
178
201
std::lock_guard<std::mutex> guard (write_mtx);
@@ -181,15 +204,44 @@ class MiniAudioStream : public AudioStream {
181
204
written += result;
182
205
}
183
206
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);
185
217
}
186
218
187
219
// activate playing
188
220
// 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 );
192
243
}
244
+
193
245
// std::this_thread::yield();
194
246
return written;
195
247
}
@@ -199,40 +251,100 @@ class MiniAudioStream : public AudioStream {
199
251
}
200
252
201
253
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);
204
270
std::lock_guard<std::mutex> guard (read_mtx);
205
271
return buffer_in.readArray (data, len);
206
272
}
207
273
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
+
208
295
protected:
209
296
MiniAudioConfig config;
210
297
ma_device_config config_ma;
211
298
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 } ;
215
302
RingBuffer<uint8_t > buffer_out{0 };
216
303
RingBuffer<uint8_t > buffer_in{0 };
217
304
std::mutex write_mtx;
218
305
std::mutex read_mtx;
219
- int buffer_size = 0 ;
306
+ std::atomic< int > buffer_size{ 0 } ;
220
307
221
308
// In playback mode copy data to pOutput. In capture mode read data from
222
309
// pInput. In full-duplex mode, both pOutput and pInput will be valid and
223
310
// you can move data from pInput into pOutput. Never process more than
224
311
// frameCount frames.
225
312
226
313
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);
229
324
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 );
236
348
}
237
349
238
350
void doWait () {
@@ -244,42 +356,77 @@ class MiniAudioStream : public AudioStream {
244
356
static void data_callback (ma_device *pDevice, void *pOutput,
245
357
const void *pInput, ma_uint32 frameCount) {
246
358
MiniAudioStream *self = (MiniAudioStream *)pDevice->pUserData ;
359
+ if (!self || !self->is_active .load ()) {
360
+ return ; // Safety check
361
+ }
362
+
247
363
AudioInfo cfg = self->audioInfo ();
364
+ if (cfg.channels == 0 || cfg.bits_per_sample == 0 ) {
365
+ LOGE (" Invalid audio configuration in callback" );
366
+ return ;
367
+ }
248
368
249
369
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
+
250
375
self->setupBuffers (bytes);
251
376
252
- if (pInput) {
377
+ if (pInput && self-> buffer_in . size () > 0 ) {
253
378
int open = bytes;
254
379
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 ()) {
256
384
int len = 0 ;
257
385
{
258
386
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);
261
388
open -= len;
262
389
processed += len;
263
390
}
264
- if (len == 0 ) self->doWait ();
391
+ if (len == 0 ) {
392
+ retry_count++;
393
+ self->doWait ();
394
+ } else {
395
+ retry_count = 0 ;
396
+ }
265
397
}
266
398
}
267
399
268
400
if (pOutput) {
269
401
memset (pOutput, 0 , bytes);
270
- if (self->is_playing ) {
402
+ if (self->is_playing . load () && self-> buffer_out . size () > 0 ) {
271
403
int open = bytes;
272
404
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 ()) {
274
409
size_t len = 0 ;
275
410
{
276
411
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);
279
413
open -= len;
280
414
processed += len;
281
415
}
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
+ }
283
430
}
284
431
}
285
432
}
0 commit comments