BackgroundAudio 1.4.1
Loading...
Searching...
No Matches
ESP32I2SAudio.h
1/*
2 BackgroundAudio
3 Plays an audio file using IRQ driven decompression. Main loop() writes
4 data to the buffer but isn't blocked while playing
5
6 Copyright (c) 2025 Earle F. Philhower, III <earlephilhower@yahoo.com>
7
8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20*/
21
22#pragma once
23
24#include <driver/i2s_std.h>
25#include <atomic>
26#include "WrappedAudioOutputBase.h"
27
35#if SOC_I2S_HW_VERSION_2
36#undef I2S_STD_CLK_DEFAULT_CONFIG
37#define I2S_STD_CLK_DEFAULT_CONFIG(rate) \
38 { .sample_rate_hz = rate, .clk_src = I2S_CLK_SRC_DEFAULT, .ext_clk_freq_hz = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_256, }
39#endif
40
44class ESP32I2SAudio : public AudioOutputBase {
45public:
54 ESP32I2SAudio(int8_t bclk = 0, int8_t ws = 1, int8_t dout = 2, int8_t mclk = -1) {
55 _bclk = bclk;
56 _ws = ws;
57 _dout = dout;
58 _mclk = mclk;
59 _running = false;
60 _sampleRate = 44100;
61 _buffers = 5;
62 _bufferWords = 1023;
63 _silenceSample = 0;
64 _cb = nullptr;
65 _underflowed = false;
66 }
67
68 virtual ~ESP32I2SAudio() {
69 }
70
79 void setPins(int8_t bclk, int8_t ws, int8_t dout, int8_t mclk = -1) {
80 _bclk = bclk;
81 _ws = ws;
82 _dout = dout;
83 _mclk = mclk;
84 }
85
93 void setInverted(bool bclk, bool ws, bool mclk = false) {
94 _bclkInv = bclk;
95 _wsInv = ws;
96 _mclkInv = mclk;
97 }
98
108 bool setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample = 0) override {
109 if (!_running) {
110 _buffers = buffers;
111 _bufferWords = bufferWords;
112 // We need DMA buffers of 1023 4-byte samples or less. Adjust behind the scenes.
113 // The upper level code will get 2x or 4x the IRQs, but availableForWrite() should
114 // still report the proper space and let it do nothing for the extra IRQs.
115 while (_bufferWords > 1023) {
116 _buffers *= 2;
117 _bufferWords /= 2;
118 }
119 _silenceSample = silenceSample;
120 }
121 return !_running;
122 }
123
131 bool setBitsPerSample(int bps) override {
132 if (!_running && bps == 16) {
133 return true;
134 }
135 return false;
136 }
137
145 bool setFrequency(int freq) override {
146 if (_running && (_sampleRate != freq)) {
147 i2s_std_clk_config_t clk_cfg;
148 clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG((uint32_t)freq);
149 i2s_channel_disable(_tx_handle);
150 i2s_channel_reconfig_std_clock(_tx_handle, &clk_cfg);
151 i2s_channel_enable(_tx_handle);
152 }
153 _sampleRate = freq;
154 return true;
155 }
156
164 bool setStereo(bool stereo = true) override {
165 return stereo;
166 }
167
178 bool begin() override {
179 if (_running) {
180 return false;
181 }
182
183 // Make a new channel of the requested buffers (which may be ignored by the IDF!)
184 i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
185 chan_cfg.dma_desc_num = _buffers;
186 chan_cfg.dma_frame_num = _bufferWords;
187 assert(ESP_OK == i2s_new_channel(&chan_cfg, &_tx_handle, nullptr));
188
189 i2s_std_config_t std_cfg = {
190 .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sampleRate),
191 .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
192 .gpio_cfg = {
193 .mclk = _mclk < 0 ? I2S_GPIO_UNUSED : (gpio_num_t)_mclk,
194 .bclk = (gpio_num_t)_bclk,
195 .ws = (gpio_num_t)_ws,
196 .dout = (gpio_num_t)_dout,
197 .din = I2S_GPIO_UNUSED,
198 .invert_flags = {
199 .mclk_inv = _mclkInv,
200 .bclk_inv = _bclkInv,
201 .ws_inv = _wsInv,
202 },
203 },
204 };
205 assert(ESP_OK == i2s_channel_init_std_mode(_tx_handle, &std_cfg));
206
207 i2s_chan_info_t _info;
208 i2s_channel_get_info(_tx_handle, &_info);
209 // If the IDF has changed our buffer size or count then we can't work
210 assert(_info.total_dma_buf_size == _buffers * _bufferWords * 4);
211 _totalAvailable = _info.total_dma_buf_size;
212
213 // Prefill silence and calculate how bug we really have
214 int16_t a[2] = {0, 0};
215 size_t written = 0;
216 do {
217 i2s_channel_preload_data(_tx_handle, (void*)a, sizeof(a), &written);
218 } while (written);
219
220 // The IRQ callbacks which will just trigger the playback task
221 i2s_event_callbacks_t _cbs = {
222 .on_recv = nullptr,
223 .on_recv_q_ovf = nullptr,
224 .on_sent = _onSent,
225 .on_send_q_ovf = nullptr
226 };
227 assert(ESP_OK == i2s_channel_register_event_callback(_tx_handle, &_cbs, (void *)this));
228 xTaskCreate(_taskShim, "BackgroundAudioI2S", 8192, (void*)this, 2, &_taskHandle);
229 _running = ESP_OK == i2s_channel_enable(_tx_handle);
230 return _running;
231 }
232
238 static IRAM_ATTR bool _onSent(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
239 return ((ESP32I2SAudio *)user_ctx)->_onSentCB(handle, event);
240 }
241
245 static void _taskShim(void *pvParameters) {
246 ((ESP32I2SAudio *)pvParameters)->_backgroundTask();
247 }
248
253 while (true) {
254 // Pause the task until notification comes in from the IRQ callbacks
255 uint32_t ulNotifiedValue;
256 xTaskNotifyWait(0, ULONG_MAX, &ulNotifiedValue, portMAX_DELAY);
257 // One DMA transfer == one frame
258 _frames++;
259 // Use the notification value to transmit how much data was in the DMA block. When negative this was an underflow notification
260 // Saturating add: _available = min(_available + size, _totalAvailable)
261 _saturating_add_available((uint32_t)ulNotifiedValue);
262 // Callback used from within a normal FreeRTOS task, so it can be slow and/or write I2S
263 if (_cb) {
264 _cb(_cbData);
265 }
266 }
267 }
268
269 inline void _saturating_add_available(uint32_t add) {
270 uint32_t cur = _available.load(std::memory_order_relaxed);
271 do {
272 uint64_t sum = (uint64_t)cur + add;
273 uint32_t capped = sum;
274 if (sum > _totalAvailable) {
275 capped = (uint32_t) _totalAvailable; // Cap to the total available
276 _underflowed.store(true, std::memory_order_release);
277 _underflows.fetch_add(1, std::memory_order_relaxed);
278 }
279
280 if (_available.compare_exchange_weak(cur, capped, std::memory_order_release, std::memory_order_relaxed)) {
281 return;
282 }
283 // cur reloaded on failure
284 } while (true);
285 }
286
287 inline void _saturating_sub_available(uint32_t sub) {
288 uint32_t cur = _available.load(std::memory_order_relaxed);
289 do {
290 uint32_t next = (cur > sub) ? (cur - sub) : 0u;
291 if (_available.compare_exchange_weak(cur, next, std::memory_order_release, std::memory_order_relaxed)) {
292 return;
293 }
294 } while (true);
295 }
296
297
303 uint32_t frames() {
304 return _frames;
305 }
306
312 uint32_t irqs() {
313 return _irqs;
314 }
315
321 uint32_t underflows() {
322 return _underflows.load(std::memory_order_acquire);
323 }
324
330 IRAM_ATTR bool _onSentCB(i2s_chan_handle_t handle, i2s_event_data_t *event) {
331 BaseType_t xHigherPriorityTaskWoken;
332 xHigherPriorityTaskWoken = pdFALSE;
333 if (_taskHandle) {
334 _irqs++;
335 xTaskNotifyFromISR(_taskHandle, event->size, eSetValueWithoutOverwrite, &xHigherPriorityTaskWoken);
336 }
337 if (xHigherPriorityTaskWoken) {
338 portYIELD_FROM_ISR();
339 }
340 return (bool)xHigherPriorityTaskWoken;
341 }
342
343
349 bool end() override {
350 if (_running) {
351 i2s_channel_disable(_tx_handle);
352 i2s_del_channel(_tx_handle);
353 vTaskDelete(_taskHandle);
354 _running = false;
355 }
356 return true;
357 }
358
364 bool getUnderflow() override {
365 // Atomically read-and-clear
366 return _underflowed.exchange(false, std::memory_order_acq_rel);
367 }
368
375 void onTransmit(void(*cb)(void *), void *cbData) override {
376 noInterrupts();
377 _cb = cb;
378 _cbData = cbData;
379 interrupts();
380 }
381
390 size_t write(const uint8_t *buffer, size_t size) override {
391 // The ESP32 i2s_channel_write will stop at a DMA buffer end even if add'l DMA buffers are free
392 // So explicitly loop until either we run out of DMA buffers or data to fill them with
393 size_t cumWritten = 0;
394 while (size) {
395 size_t written = 0;
396 i2s_channel_write(_tx_handle, buffer, size, &written, 100);
397 _saturating_sub_available((uint32_t)written);
398 buffer += written;
399 size -= written;
400 cumWritten += written;
401 // If we're full-up, don't block just exit
402 if (!written) {
403 break;
404 }
405 }
406 return cumWritten;
407 }
408
414 size_t write(uint8_t d) override {
415 return 0; // No bytes!
416 }
417
423 int availableForWrite() override {
424 return (int)_available.load(std::memory_order_acquire);
425 }
426
427protected:
428 bool _running;
429 int8_t _bclk = 0;
430 int8_t _ws = 1;
431 int8_t _dout = 2;
432 int8_t _mclk = -1;
433 bool _bclkInv = false;
434 bool _wsInv = false;
435 bool _mclkInv = false;
436 std::atomic<bool> _underflowed{false};
437 size_t _sampleRate;
438 size_t _buffers;
439 size_t _bufferWords;
440 uint32_t _silenceSample;
441 TaskHandle_t _taskHandle = 0;
442 void (*_cb)(void *);
443 void *_cbData;
444 // I2S IDF object and info
445 i2s_chan_handle_t _tx_handle;
446 size_t _totalAvailable = 0;
447 std::atomic<uint32_t> _available{0};
448 uint32_t _irqs = 0; // Number of I2S IRQs received
449 uint32_t _frames = 0; // Number of DMA buffers sent
450 std::atomic<uint32_t> _underflows{0}; // Number of underflowed DMA buffers
451};
I2S object with IRQ-based callbacks to a FreeRTOS task, for use with BackgroundAudio.
Definition ESP32I2SAudio.h:44
ESP32I2SAudio(int8_t bclk=0, int8_t ws=1, int8_t dout=2, int8_t mclk=-1)
Construct ESP32-based I2S object with IRQ-based callbacks to a FreeRTOS task, for use with Background...
Definition ESP32I2SAudio.h:54
static void _taskShim(void *pvParameters)
C-language shim to start the real object's task.
Definition ESP32I2SAudio.h:245
void _backgroundTask()
Background I2S DMA buffer notification task. Tracks number of bytes available to be written.
Definition ESP32I2SAudio.h:252
uint32_t irqs()
Get the number of input data shifts processed by decoder since begin
Definition ESP32I2SAudio.h:312
bool end() override
Stop the I2S device.
Definition ESP32I2SAudio.h:349
uint32_t frames()
Get number of DMA frames(buffers) processed.
Definition ESP32I2SAudio.h:303
bool setStereo(bool stereo=true) override
Set mono or stereo mode. Only stereo supported.
Definition ESP32I2SAudio.h:164
bool setFrequency(int freq) override
Set the sample rate (LRCLK/WS) of the I2S interface. Can be called while running.
Definition ESP32I2SAudio.h:145
uint32_t underflows()
Get the number of times the MP3 decoder has underflowed waiting on raw data since begin
Definition ESP32I2SAudio.h:321
IRAM_ATTR bool _onSentCB(i2s_chan_handle_t handle, i2s_event_data_t *event)
Object-based callback for I2S Sent notification.
Definition ESP32I2SAudio.h:330
void setPins(int8_t bclk, int8_t ws, int8_t dout, int8_t mclk=-1)
Set the I2S GPIO pins before calling begin
Definition ESP32I2SAudio.h:79
bool getUnderflow() override
Determine if there was an underflow since the last time this was called. Cleared on read.
Definition ESP32I2SAudio.h:364
int availableForWrite() override
Determine the number of frames we can write to the DMA buffers at this instant.
Definition ESP32I2SAudio.h:423
static IRAM_ATTR bool _onSent(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
C-language wrapper for I2S Sent event.
Definition ESP32I2SAudio.h:238
size_t write(uint8_t d) override
Write single byte to I2S buffers. Not supported.
Definition ESP32I2SAudio.h:414
bool setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample=0) override
Set the size and number of the I2S buffers before begin
Definition ESP32I2SAudio.h:108
bool setBitsPerSample(int bps) override
Set the bits per sample for the I2S output. Only 16-bit supported.
Definition ESP32I2SAudio.h:131
void onTransmit(void(*cb)(void *), void *cbData) override
Set the callback function to be called every DMA buffer completion.
Definition ESP32I2SAudio.h:375
bool begin() override
Start the I2S interface.
Definition ESP32I2SAudio.h:178
size_t write(const uint8_t *buffer, size_t size) override
Write data to the I2S interface. Not legal from IRQ context. Will not block and may write less than r...
Definition ESP32I2SAudio.h:390
void setInverted(bool bclk, bool ws, bool mclk=false)
Set the I2S GPIO inversions before calling begin
Definition ESP32I2SAudio.h:93