BackgroundAudio 1.3.3
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 "WrappedAudioOutputBase.h"
26
34#if SOC_I2S_HW_VERSION_2
35#undef I2S_STD_CLK_DEFAULT_CONFIG
36#define I2S_STD_CLK_DEFAULT_CONFIG(rate) \
37 { .sample_rate_hz = rate, .clk_src = I2S_CLK_SRC_DEFAULT, .ext_clk_freq_hz = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_256, }
38#endif
39
43class ESP32I2SAudio : public AudioOutputBase {
44public:
53 ESP32I2SAudio(int8_t bclk = 0, int8_t ws = 1, int8_t dout = 2, int8_t mclk = -1) {
54 _bclk = bclk;
55 _ws = ws;
56 _dout = dout;
57 _mclk = mclk;
58 _running = false;
59 _sampleRate = 44100;
60 _buffers = 5;
61 _bufferWords = 512;
62 _silenceSample = 0;
63 _cb = nullptr;
64 _underflowed = false;
65 }
66
67 virtual ~ESP32I2SAudio() {
68 }
69
78 void setPins(int8_t bclk, int8_t ws, int8_t dout, int8_t mclk = -1) {
79 _bclk = bclk;
80 _ws = ws;
81 _dout = dout;
82 _mclk = mclk;
83 }
84
92 void setInverted(bool bclk, bool ws, bool mclk = false) {
93 _bclkInv = bclk;
94 _wsInv = ws;
95 _mclkInv = mclk;
96 }
97
107 bool setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample = 0) override {
108 if (!_running) {
109 _buffers = buffers;
110 _bufferWords = bufferWords;
111 _silenceSample = silenceSample;
112 }
113 return !_running;
114 }
115
123 bool setBitsPerSample(int bps) override {
124 if (!_running && bps == 16) {
125 return true;
126 }
127 return false;
128 }
129
137 bool setFrequency(int freq) override {
138 if (_running && (_sampleRate != freq)) {
139 i2s_std_clk_config_t clk_cfg;
140 clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG((uint32_t)freq);
141 i2s_channel_disable(_tx_handle);
142 i2s_channel_reconfig_std_clock(_tx_handle, &clk_cfg);
143 i2s_channel_enable(_tx_handle);
144 }
145 _sampleRate = freq;
146 return true;
147 }
148
156 bool setStereo(bool stereo = true) override {
157 return stereo;
158 }
159
170 bool begin() override {
171 if (_running) {
172 return false;
173 }
174
175 // Make a new channel of the requested buffers (which may be ignored by the IDF!)
176 i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
177 chan_cfg.dma_desc_num = _buffers;
178 chan_cfg.dma_frame_num = _bufferWords * 4;
179 i2s_new_channel(&chan_cfg, &_tx_handle, nullptr);
180
181 i2s_std_config_t std_cfg = {
182 .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sampleRate),
183 .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
184 .gpio_cfg = {
185 .mclk = _mclk < 0 ? I2S_GPIO_UNUSED : (gpio_num_t)_mclk,
186 .bclk = (gpio_num_t)_bclk,
187 .ws = (gpio_num_t)_ws,
188 .dout = (gpio_num_t)_dout,
189 .din = I2S_GPIO_UNUSED,
190 .invert_flags = {
191 .mclk_inv = _mclkInv,
192 .bclk_inv = _bclkInv,
193 .ws_inv = _wsInv,
194 },
195 },
196 };
197 i2s_channel_init_std_mode(_tx_handle, &std_cfg);
198
199 // Prefill silence and calculate how bug we really have
200 int16_t a[2] = {0, 0};
201 size_t written = 0;
202 do {
203 i2s_channel_preload_data(_tx_handle, (void*)a, sizeof(a), &written);
204 _totalAvailable += written;
205 } while (written);
206
207 // The IRQ callbacks which will just trigger the playback task
208 i2s_event_callbacks_t _cbs = {
209 .on_recv = nullptr,
210 .on_recv_q_ovf = nullptr,
211 .on_sent = _onSent,
212 .on_send_q_ovf = _onSentUnder
213 };
214 i2s_channel_register_event_callback(_tx_handle, &_cbs, (void *)this);
215 xTaskCreate(_taskShim, "BackgroundAudioI2S", 8192, (void*)this, 2, &_taskHandle);
216 _running = ESP_OK == i2s_channel_enable(_tx_handle);
217 return _running;
218 }
219
225 static IRAM_ATTR bool _onSent(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
226 return ((ESP32I2SAudio *)user_ctx)->_onSentCB(handle, event);
227 }
228
234 static IRAM_ATTR bool _onSentUnder(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
235 return ((ESP32I2SAudio *)user_ctx)->_onSentCB(handle, event, true);
236 }
237
241 static void _taskShim(void *pvParameters) {
242 ((ESP32I2SAudio *)pvParameters)->_backgroundTask();
243 }
244
249 while (true) {
250 // Pause the task until notification comes in from the IRQ callbacks
251 uint32_t ulNotifiedValue;
252 xTaskNotifyWait(0, ULONG_MAX, &ulNotifiedValue, portMAX_DELAY);
253 // One DMA transfer == one frame
254 _frames++;
255 // Use the notification value to transmit how much data was in the DMA block. When negative this was an underflow notification
256 int32_t size = (int32_t)ulNotifiedValue;
257 if (size < 0) {
258 _underflows++;
259 size = -size;
260 }
261 // Track the amount believed available, with some sanity checking
262 _available += size;
263 if (_available > _totalAvailable) {
264 _available = _totalAvailable;
265 }
266 // Callback used from within a normal FreeRTOS task, so it can be slow and/or write I2S
267 if (_cb) {
268 _cb(_cbData);
269 }
270 }
271 }
272
273
279 uint32_t frames() {
280 return _frames;
281 }
282
288 uint32_t irqs() {
289 return _irqs;
290 }
291
297 uint32_t underflows() {
298 return _underflows;
299 }
300
306 IRAM_ATTR bool _onSentCB(i2s_chan_handle_t handle, i2s_event_data_t *event, bool underflow = false) {
307 BaseType_t xHigherPriorityTaskWoken;
308 xHigherPriorityTaskWoken = pdFALSE;
309 if (_taskHandle) {
310 _irqs++;
311 if (underflow) {
312 _underflowed = true;
313 }
314 xTaskNotifyFromISR(_taskHandle, event->size * (underflow ? -1 : 1), eSetValueWithOverwrite, &xHigherPriorityTaskWoken);
315 }
316 return (bool)xHigherPriorityTaskWoken;
317 }
318
319
325 bool end() override {
326 if (_running) {
327 i2s_channel_disable(_tx_handle);
328 i2s_del_channel(_tx_handle);
329 vTaskDelete(_taskHandle);
330 _running = false;
331 }
332 return true;
333 }
334
340 bool getUnderflow() override {
341 noInterrupts();
342 auto ret = _underflowed;
343 _underflowed = false;
344 interrupts();
345 return ret;
346 }
347
354 void onTransmit(void(*cb)(void *), void *cbData) override {
355 noInterrupts();
356 _cb = cb;
357 _cbData = cbData;
358 interrupts();
359 }
360
369 size_t write(const uint8_t *buffer, size_t size) override {
370 size_t written = 0;
371 i2s_channel_write(_tx_handle, buffer, size, &written, 0);
372 noInterrupts(); // TODO - Freertos task protection instead?
373 if (written != size) {
374 _available = 0;
375 } else if (_available >= written) {
376 _available -= written;
377 } else {
378 _available = 0;
379 }
380 interrupts();
381 return written;
382 }
383
389 size_t write(uint8_t d) override {
390 return 0; // No bytes!
391 }
392
398 int availableForWrite() override {
399 return _available; // It's our best guess for now
400 }
401
402private:
403 bool _running;
404 int8_t _bclk = 0;
405 int8_t _ws = 1;
406 int8_t _dout = 2;
407 int8_t _mclk = -1;
408 bool _bclkInv = false;
409 bool _wsInv = false;
410 bool _mclkInv = false;
411 bool _underflowed = false;
412 size_t _sampleRate;
413 size_t _buffers;
414 size_t _bufferWords;
415 uint32_t _silenceSample;
416 TaskHandle_t _taskHandle = 0;
417 void (*_cb)(void *);
418 void *_cbData;
419 // I2S IDF object and info
420 i2s_chan_handle_t _tx_handle;
421 size_t _totalAvailable = 0;
422 size_t _available = 0;
423 uint32_t _irqs = 0; // Number of I2S IRQs received
424 uint32_t _frames = 0; // Number of DMA buffers sent
425 uint32_t _underflows = 0; // Number of underflowed DMA buffers
426};
I2S object with IRQ-based callbacks to a FreeRTOS task, for use with BackgroundAudio.
Definition ESP32I2SAudio.h:43
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:53
static void _taskShim(void *pvParameters)
C-language shim to start the real object's task.
Definition ESP32I2SAudio.h:241
void _backgroundTask()
Background I2S DMA buffer notification task. Tracks number of bytes available to be written.
Definition ESP32I2SAudio.h:248
uint32_t irqs()
Get the number of input data shifts processed by decoder since begin
Definition ESP32I2SAudio.h:288
bool end() override
Stop the I2S device.
Definition ESP32I2SAudio.h:325
IRAM_ATTR bool _onSentCB(i2s_chan_handle_t handle, i2s_event_data_t *event, bool underflow=false)
Object-based callback for I2S Sent notification.
Definition ESP32I2SAudio.h:306
uint32_t frames()
Get number of DMA frames(buffers) processed.
Definition ESP32I2SAudio.h:279
bool setStereo(bool stereo=true) override
Set mono or stereo mode. Only stereo supported.
Definition ESP32I2SAudio.h:156
bool setFrequency(int freq) override
Set the sample rate (LRCLK/WS) of the I2S interface. Can be called while running.
Definition ESP32I2SAudio.h:137
uint32_t underflows()
Get the number of times the MP3 decoder has underflowed waiting on raw data since begin
Definition ESP32I2SAudio.h:297
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:78
bool getUnderflow() override
Determine if there was an underflow since the last time this was called. Cleared on read.
Definition ESP32I2SAudio.h:340
int availableForWrite() override
Determine the number of bytes we can write to the DMA buffers at this instant.
Definition ESP32I2SAudio.h:398
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:225
size_t write(uint8_t d) override
Write single byte to I2S buffers. Not supported.
Definition ESP32I2SAudio.h:389
static IRAM_ATTR bool _onSentUnder(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
C-language wrapper for I2S Sent Underflow event.
Definition ESP32I2SAudio.h:234
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:107
bool setBitsPerSample(int bps) override
Set the bits per sample for the I2S output. Only 16-bit supported.
Definition ESP32I2SAudio.h:123
void onTransmit(void(*cb)(void *), void *cbData) override
Set the callback function to be called every DMA buffer completion.
Definition ESP32I2SAudio.h:354
bool begin() override
Start the I2S interface.
Definition ESP32I2SAudio.h:170
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:369
void setInverted(bool bclk, bool ws, bool mclk=false)
Set the I2S GPIO inversions before calling begin
Definition ESP32I2SAudio.h:92