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