arduino-audio-tools
Loading...
Searching...
No Matches
MiniAudioStream.h
Go to the documentation of this file.
1#pragma once
8#include <atomic>
9#include <mutex>
10
11#include "AudioTools.h"
12
13#define MINIAUDIO_IMPLEMENTATION
14#include "miniaudio.h"
15
16#define MA_BUFFER_COUNT 10
17#define MA_BUFFER_SIZE 1200
18#define MA_START_COUNT 2
19#define MA_DELAY 10
20
21namespace audio_tools {
22
28class MiniAudioConfig : public AudioInfo {
29 public:
31 sample_rate = 44100;
32 channels = 2;
33 bits_per_sample = 16;
34 };
41
42 bool is_input = false;
43 bool is_output = true;
49 true; // Automatically restart after buffer underrun
50 int underrun_tolerance = 5; // Number of empty reads before stopping playback
51};
52
64 public:
65 MiniAudioStream() = default;
67
70 info.sample_rate = 44100;
71 info.channels = 2;
73 switch (mode) {
74 case RX_MODE:
75 info.is_input = true;
76 info.is_output = false;
77 break;
78 case TX_MODE:
79 info.is_input = false;
80 info.is_output = true;
81 break;
82 case RXTX_MODE:
83 info.is_input = true;
84 info.is_output = true;
85 break;
86 default:
87 info.is_input = false;
88 info.is_output = false;
89 break;
90 }
91 return info;
92 }
93
94 void setAudioInfo(AudioInfo in) override {
96 if (in.sample_rate != config.sample_rate ||
97 in.channels != config.channels ||
99 config.copyFrom(in);
100 if (stopDevice()) {
101 begin();
102 }
103 }
104 }
105
108 this->config = info;
109 return begin();
110 }
111
112 bool begin() override {
113 TRACEI();
114 // release an already started device
115 stopDevice();
118 config_ma = ma_device_config_init(ma_device_type_playback);
119 else if (!config.is_output && config.is_input)
120 config_ma = ma_device_config_init(ma_device_type_capture);
121 else if (config.is_output && config.is_input)
122 config_ma = ma_device_config_init(ma_device_type_duplex);
123 else if (!config.is_output && !config.is_input)
124 config_ma = ma_device_config_init(ma_device_type_loopback);
125
126 config_ma.pUserData = this;
127 config_ma.sampleRate = config.sample_rate;
128 config_ma.dataCallback = data_callback;
129 ma_format format;
130 switch (config.bits_per_sample) {
131 case 8:
132 format = ma_format_u8;
133 break;
134 case 16:
135 format = ma_format_s16;
136 break;
137 case 24:
138 format = ma_format_s24;
139 break;
140 case 32:
141 format = ma_format_s32;
142 break;
143 default:
144 LOGE("Invalid format");
145 return false;
146 }
147 // set both sides: otherwise a capture (or duplex/loopback) device uses
148 // the native format of the device, while data_callback() interprets the
149 // data in the requested format
150 config_ma.playback.format = format;
151 config_ma.playback.channels = config.channels;
152 config_ma.capture.format = format;
153 config_ma.capture.channels = config.channels;
154
155 if (ma_device_init(NULL, &config_ma, &device_ma) != MA_SUCCESS) {
156 // Failed to initialize the device.
157 return false;
158 }
159
160 // The device is sleeping by default so you'll need to start it manually.
161 if (ma_device_start(&device_ma) != MA_SUCCESS) {
162 // Failed to initialize the device.
163 ma_device_uninit(&device_ma);
164 return false;
165 }
166
167 is_active.store(true);
168 return is_active.load();
169 }
170
171 void end() override {
172 stopDevice();
173 // release buffer memory
174 buffer_in.resize(0);
176 is_buffers_setup.store(false);
177 }
178
182 bool stopDevice() {
183 if (!is_active.exchange(false)) return false;
184 is_playing.store(false);
185 // This will stop the device so no need to do that manually.
186 ma_device_uninit(&device_ma);
187 return true;
188 }
189
190 int availableForWrite() override {
191 return buffer_out.size() == 0 ? 0 : DEFAULT_BUFFER_SIZE;
192 }
193
194 size_t write(const uint8_t* data, size_t len) override {
195 // Input validation
196 if (!data || len == 0) {
197 LOGW("Invalid write parameters: data=%p, len=%zu", data, len);
198 return 0;
199 }
200
201 if (buffer_out.size() == 0) {
202 LOGW("Output buffer not initialized");
203 return 0;
204 }
205
206 if (!is_active.load()) {
207 LOGW("Stream not active");
208 return 0;
209 }
210
211 LOGD("write: %zu", len);
212
213 // write data to buffer
214 int open = len;
215 int written = 0;
216 int retry_count = 0;
217 const int max_retries = 1000; // Prevent infinite loops
218
219 while (open > 0 && retry_count < max_retries) {
220 size_t result = 0;
221 {
222 std::lock_guard<std::mutex> guard(write_mtx);
223 result = buffer_out.writeArray(data + written, open);
224 open -= result;
225 written += result;
226 }
227
228 if (result == 0) {
229 retry_count++;
230 doWait();
231 } else {
232 retry_count = 0; // Reset on successful write
233 }
234 }
235
236 if (retry_count >= max_retries) {
237 LOGE("Write timeout after %d retries, written %d of %zu bytes",
238 max_retries, written, len);
239 }
240
241 // activate playing
242 // if (!is_playing && buffer_out.bufferCountFilled()>=MA_START_COUNT) {
243 int current_buffer_size = buffer_size.load();
244 bool should_start_playing = false;
245
246 // Start playing if we have enough data and either:
247 // 1. We're not playing yet, or
248 // 2. We stopped due to buffer underrun but now have data again
249 if (current_buffer_size > 0) {
250 int available_data = buffer_out.available();
251 int threshold = config.buffer_start_count * current_buffer_size;
252
253 if (!is_playing.load() && available_data >= threshold) {
254 should_start_playing = true;
255 } else if (is_playing.load() && available_data == 0) {
256 // Stop playing if buffer is completely empty (helps with long delays)
257 LOGW("Buffer empty, pausing playback");
258 is_playing.store(false);
259 }
260 }
261
262 if (should_start_playing) {
263 LOGI("starting audio playback");
264 is_playing.store(true);
265 }
266
267 // std::this_thread::yield();
268 return written;
269 }
270
271 int available() override {
272 return buffer_in.size() == 0 ? 0 : buffer_in.available();
273 }
274
275 size_t readBytes(uint8_t* data, size_t len) override {
276 if (!data || len == 0) {
277 LOGW("Invalid read parameters: data=%p, len=%zu", data, len);
278 return 0;
279 }
280
281 if (buffer_in.size() == 0) {
282 LOGW("Input buffer not initialized");
283 return 0;
284 }
285
286 if (!is_active.load()) {
287 LOGW("Stream not active");
288 return 0;
289 }
290
291 LOGD("read: %zu", len);
292 std::lock_guard<std::mutex> guard(read_mtx);
293 return buffer_in.readArray(data, len);
294 }
295
298 if (!is_active.load()) {
299 LOGW("Cannot restart playback - stream not active");
300 return;
301 }
302
303 int current_buffer_size = buffer_size.load();
304 if (current_buffer_size > 0 && buffer_out.available() > 0) {
305 LOGI("Manually restarting playback");
306 is_playing.store(true);
307 } else {
308 LOGW("Cannot restart playback - no data available");
309 }
310 }
311
313 bool isPlaying() const { return is_playing.load(); }
314
315 protected:
317 ma_device_config config_ma;
318 ma_device device_ma;
319 std::atomic<bool> is_playing{false};
320 std::atomic<bool> is_active{false};
321 std::atomic<bool> is_buffers_setup{false};
324 std::mutex write_mtx;
325 std::mutex read_mtx;
326 std::atomic<int> buffer_size{0};
327
328 // In playback mode copy data to pOutput. In capture mode read data from
329 // pInput. In full-duplex mode, both pOutput and pInput will be valid and
330 // you can move data from pInput into pOutput. Never process more than
331 // frameCount frames.
332
333 void setupBuffers(int size = MA_BUFFER_SIZE) {
334 std::lock_guard<std::mutex> guard(write_mtx);
335 if (is_buffers_setup.load()) return;
336
337 // Validate buffer size
338 if (size <= 0 || size > 1024 * 1024) { // Max 1MB per buffer chunk
339 LOGE("Invalid buffer size: %d", size);
340 return;
341 }
342
343 buffer_size.store(size);
344 int buffer_count = config.buffer_count;
345
346 // Validate total buffer size to prevent excessive memory allocation
347 size_t total_size = static_cast<size_t>(size) * buffer_count;
348 if (total_size > 100 * 1024 * 1024) { // Max 100MB total
349 LOGE("Buffer size too large: %zu bytes", total_size);
350 return;
351 }
352
353 LOGI("setupBuffers: %d * %d = %zu bytes", size, buffer_count, total_size);
354
355 if (buffer_out.size() == 0 && config.is_output) {
356 if (!buffer_out.resize(size * buffer_count)) {
357 LOGE("Failed to resize output buffer");
358 return;
359 }
360 }
361 if (buffer_in.size() == 0 && config.is_input) {
362 if (!buffer_in.resize(size * buffer_count)) {
363 LOGE("Failed to resize input buffer");
364 return;
365 }
366 }
367 is_buffers_setup.store(true);
368 }
369
370 void doWait() {
371 // std::this_thread::yield();
373 // std::this_thread::sleep_for (std::chrono::milliseconds(MA_DELAY));
374 }
375
376 static void data_callback(ma_device* pDevice, void* pOutput,
377 const void* pInput, ma_uint32 frameCount) {
378 MiniAudioStream* self = (MiniAudioStream*)pDevice->pUserData;
379 if (!self || !self->is_active.load()) {
380 return; // Safety check
381 }
382
383 AudioInfo cfg = self->audioInfo();
384 if (cfg.channels == 0 || cfg.bits_per_sample == 0) {
385 LOGE("Invalid audio configuration in callback");
386 return;
387 }
388
389 int bytes = frameCount * cfg.channels * cfg.bits_per_sample / 8;
390 if (bytes <= 0 || bytes > 1024 * 1024) { // Sanity check
391 LOGE("Invalid byte count in callback: %d", bytes);
392 return;
393 }
394
395 self->setupBuffers(bytes);
396
397 if (pInput && self->buffer_in.size() > 0) {
398 int open = bytes;
399 int processed = 0;
400 int retry_count = 0;
401 const int max_retries = 100;
402
403 while (open > 0 && retry_count < max_retries && self->is_active.load()) {
404 int len = 0;
405 {
406 std::unique_lock<std::mutex> guard(self->read_mtx);
407 len = self->buffer_in.writeArray((uint8_t*)pInput + processed, open);
408 open -= len;
409 processed += len;
410 }
411 if (len == 0) {
412 retry_count++;
413 self->doWait();
414 } else {
415 retry_count = 0;
416 }
417 }
418 }
419
420 if (pOutput) {
421 memset(pOutput, 0, bytes);
422 if (self->is_playing.load() && self->buffer_out.size() > 0) {
423 int open = bytes;
424 int processed = 0;
425 int consecutive_failures = 0;
426 const int max_failures = self->config.underrun_tolerance;
427
428 while (open > 0 && self->is_active.load()) {
429 size_t len = 0;
430 {
431 std::lock_guard<std::mutex> guard(self->write_mtx);
432 len =
433 self->buffer_out.readArray((uint8_t*)pOutput + processed, open);
434 open -= len;
435 processed += len;
436 }
437
438 if (len == 0) {
439 consecutive_failures++;
440 // If we can't get data for too long, stop playing to prevent issues
441 if (consecutive_failures >= max_failures &&
443 LOGW("Buffer underrun detected, stopping playback");
444 self->is_playing.store(false);
445 break;
446 }
447 // Don't wait in callback for too long - just output silence
448 break;
449 } else {
450 consecutive_failures = 0;
451 }
452 }
453 }
454 }
455 }
456};
457
458} // namespace audio_tools
#define LOGW(...)
Definition AudioLoggerIDF.h:29
#define TRACEI()
Definition AudioLoggerIDF.h:32
#define LOGI(...)
Definition AudioLoggerIDF.h:28
#define LOGD(...)
Definition AudioLoggerIDF.h:27
#define LOGE(...)
Definition AudioLoggerIDF.h:30
#define MA_DELAY
Definition MiniAudioStream.h:19
#define MA_BUFFER_SIZE
Definition MiniAudioStream.h:17
#define MA_BUFFER_COUNT
Definition MiniAudioStream.h:16
#define MA_START_COUNT
Definition MiniAudioStream.h:18
#define DEFAULT_BUFFER_SIZE
Definition avr.h:20
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition BaseStream.h:120
AudioInfo info
Definition BaseStream.h:171
virtual void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition BaseStream.h:128
virtual AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition BaseStream.h:151
Configuration for MiniAudio.
Definition MiniAudioStream.h:28
int underrun_tolerance
Definition MiniAudioStream.h:50
int buffer_count
Definition MiniAudioStream.h:46
MiniAudioConfig()
Definition MiniAudioStream.h:30
int delay_ms_if_buffer_full
Definition MiniAudioStream.h:44
MiniAudioConfig(const AudioInfo &in)
Definition MiniAudioStream.h:36
bool auto_restart_on_underrun
Definition MiniAudioStream.h:48
bool is_output
Definition MiniAudioStream.h:43
bool is_input
Definition MiniAudioStream.h:42
MiniAudioConfig(const MiniAudioConfig &)=default
int buffer_start_count
Definition MiniAudioStream.h:47
int buffer_size
Definition MiniAudioStream.h:45
MiniAudio: https://miniaud.io/.
Definition MiniAudioStream.h:63
bool stopDevice()
Definition MiniAudioStream.h:182
std::mutex read_mtx
Definition MiniAudioStream.h:325
bool isPlaying() const
Check if playback is currently active.
Definition MiniAudioStream.h:313
void setupBuffers(int size=1200)
Definition MiniAudioStream.h:333
std::mutex write_mtx
Definition MiniAudioStream.h:324
size_t readBytes(uint8_t *data, size_t len) override
Definition MiniAudioStream.h:275
std::atomic< int > buffer_size
Definition MiniAudioStream.h:326
std::atomic< bool > is_active
Definition MiniAudioStream.h:320
void restartPlayback()
Manually restart playback (useful after long delays)
Definition MiniAudioStream.h:297
void end() override
Definition MiniAudioStream.h:171
int available() override
Definition MiniAudioStream.h:271
size_t write(const uint8_t *data, size_t len) override
Definition MiniAudioStream.h:194
std::atomic< bool > is_playing
Definition MiniAudioStream.h:319
ma_device device_ma
Definition MiniAudioStream.h:318
int availableForWrite() override
Definition MiniAudioStream.h:190
void doWait()
Definition MiniAudioStream.h:370
void setAudioInfo(AudioInfo in) override
Defines the input AudioInfo.
Definition MiniAudioStream.h:94
bool begin() override
Definition MiniAudioStream.h:112
RingBuffer< uint8_t > buffer_in
Definition MiniAudioStream.h:323
RingBuffer< uint8_t > buffer_out
Definition MiniAudioStream.h:322
ma_device_config config_ma
Definition MiniAudioStream.h:317
MiniAudioConfig defaultConfig(RxTxMode mode=RXTX_MODE)
Definition MiniAudioStream.h:68
MiniAudioConfig config
Definition MiniAudioStream.h:316
~MiniAudioStream()
Definition MiniAudioStream.h:66
std::atomic< bool > is_buffers_setup
Definition MiniAudioStream.h:321
bool begin(MiniAudioConfig info)
Definition MiniAudioStream.h:106
static void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount)
Definition MiniAudioStream.h:376
Implements a typed Ringbuffer.
Definition Buffers.h:363
virtual int readArray(T data[], int len) override
reads multiple values
Definition Buffers.h:412
virtual int writeArray(const T data[], int len) override
Fills the buffer data.
Definition Buffers.h:432
virtual size_t size() override
Returns the maximum capacity of the buffer.
Definition Buffers.h:493
virtual bool resize(size_t len)
Resizes the buffer if supported: returns false if not supported.
Definition Buffers.h:483
virtual int available() override
provides the number of entries that are available to read
Definition Buffers.h:475
RxTxMode
The Microcontroller is the Audio Source (TX_MODE) or Audio Sink (RX_MODE). RXTX_MODE is Source and Si...
Definition AudioTypes.h:26
@ RXTX_MODE
Definition AudioTypes.h:26
@ TX_MODE
Definition AudioTypes.h:26
@ RX_MODE
Definition AudioTypes.h:26
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
void delay(uint32_t ms)
Definition Arduino.h:259
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:56
void copyFrom(AudioInfo info)
Same as set.
Definition AudioTypes.h:106
sample_rate_t sample_rate
Sample Rate: e.g 44100.
Definition AudioTypes.h:58
uint16_t channels
Number of channels: 2=stereo, 1=mono.
Definition AudioTypes.h:60
uint8_t bits_per_sample
Number of bits per sample (int16_t = 16 bits)
Definition AudioTypes.h:62