In this part of the ESP32 audio project, the internet radio built in Part I gets an extension: bluetooth audio streaming. The hardware from the first part is used, i.e. an M5StickC plus with an I²S DAC. The software includes an additional library, the ESP32-A2DP library which allows to turn the ESP32 into a bluetooth audio (A2DP) sink.
The integration of both audio libraries, ESP32-audioI2S and ESP32-A2DP, in a single application turned out to be not that easy. Hence, this is the main focus of the project description.
Key Features (Part II)- Bluteooth audio streaming e.g. from Android or iOS devices
- Artist and song title shown on display
- Switching between bluetooth receiver mode and internet radio mode on button press
- Audio playback via I²S and an external digital-to-analog converter (DAC) board
I used Visual Studio Code with the PlatformIO IDE for this project. In the platformio.ini
file of the project, library dependencies to the M5StickCPlus
, the ESP32-audioI2S
, and the ESP32-A2DP
libraries are included.
[env:m5stick-c]
platform = espressif32
board = m5stick-c
framework = arduino
upload_speed = 1500000
monitor_speed = 115200
build_type = debug
build_flags = -D CORE_DEBUG_LEVEL=4 ; 'Debug'
monitor_filters = log2file, esp32_exception_decoder, default
board_build.partitions = huge_app.csv
lib_deps =
M5StickCPlus
https://github.com/schreibfaul1/ESP32-audioI2S
https://github.com/pschatzmann/ESP32-A2DP
With the monitor_filters
option, file logging and exception stack trace decoding are enabled.
The usage of the board_build.partitions
option is quite important for this project. It is explained in one of the subsequent sections.
The commented source code is available in the GitHub repository of this project in the a2dp
branch. Please note that a WifiCredentials.cpp
file needs to be created in the src
folder (see first part of this project).
Since the application uses both, Bluetooth and WiFi (and the lwIP TCP/IP stack on top), the program size gets quite large. Thus, the first obstacle in this project was an error message after linking stating that the program size became greater than the allowed maximum of 1, 310, 720 bytes.
Linking .pio\build\m5stick-c\firmware.elf
Retrieving maximum program size .pio\build\m5stick-c\firmware.elf
Checking size .pio\build\m5stick-c\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
Error: The program size (1786551 bytes) is greater than maximum allowed (1310720 bytes)
RAM: [== ] 16.8% (used 55024 bytes from 327680 bytes)
*** [checkprogsize] Explicit exit, status 1
Flash: [==========] 136.3% (used 1786551 bytes from 1310720 bytes)
Now, it comes into play that the M5StickC plus is based on the ESP32-PICO-D4 module which has 4 MB (4*2^20 bytes) of SPI flash memory. How does it come then that the allowed program size is considerably smaller, i.e. 1, 310, 720 bytes?
The answer lies in the partition table which divides the flash memory into sections (partitions) for different types data and one or several applications. The default partition table layout is defined in the defaul.csv file of Arduino-ESP32 package. Therein, two application partitions, app0
and app1
, are defined, each of which has a size of 0x140000
bytes (decimal: 1, 310, 720).
To overcome the error stated above, a different partitioning of the flash memory can be applied such as the huge_app.csv
which defines an app partition of 0x300000
bytes size (decimal: 3, 145, 728). This can be accomplished in PlatformIO using the board_build.partitions option in the platformio.ini file. With that option set, the build ouput changes to:
RAM: [== ] 16.8% (used 55024 bytes from 327680 bytes)
Flash: [====== ] 56.8% (used 1786551 bytes from 3145728 bytes)
Building .pio\build\m5stick-c\firmware.bin
More Trouble with Memory SizeMy first (and probably naive) idea was to run both libraries, ESP32-audioI2S and ESP32-A2DP, along with each other. Yet, this resulted in a non-functioning application that was producing varying exception traces most of the time such as this one:
Guru Meditation Error: Core 0 panic'ed (StoreProhibited). Exception was unhandled.
Core 0 register dump:
PC : 0x40161fd0 PS : 0x00060630 A0 : 0x80164b4d A1 : 0x3ffee980
A2 : 0x3fff9e00 A3 : 0x3ffee9c0 A4 : 0x00000001 A5 : 0x00000001
A6 : 0x00060820 A7 : 0x00000000 A8 : 0x00000100 A9 : 0x00006c42
A10 : 0x00000000 A11 : 0x00001209 A12 : 0x00060820 A13 : 0x3ffee9d0
A14 : 0x00001400 A15 : 0x00001800 SAR : 0x00000020 EXCCAUSE: 0x0000001d
EXCVADDR: 0x0000018f LBEG : 0x4000c2e0 LEND : 0x4000c2f6 LCOUNT : 0x00000000
ELF file SHA256: 0000000000000000
Backtrace: 0x40161fd0:0x3ffee980 0x40164b4a:0x3ffee9a0 0x40139586:0x3ffee9c0 0x400908c6:0x3ffee9f0
#0 0x40161fd0:0x3ffee980 in bta_av_hdl_event at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/bt/bluedroid/bta/av/bta_av_main.c:1248
#1 0x40164b4a:0x3ffee9a0 in bta_sys_event at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/bt/bluedroid/bta/sys/bta_sys_main.c:496
#2 0x40139586:0x3ffee9c0 in btu_task_thread_handler at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/bt/bluedroid/stack/btu/btu_task.c:233
#3 0x400908c6:0x3ffee9f0 in vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port.c:355 (discriminator 1)
Although I am not completely sure about the definite cause for the exceptions, I think that the most probable reason is insufficient RAM size.
After trying a few trial and error, I found a satisfying solution by deciding at startup of the application which library is initialized and started. The choice of the application mode is stored in non-volatile memory using the EEPROM library:
uint8_t mode = EEPROM.readByte(0);
if (mode == 2) {
startA2dp();
}
else {
startRadio();
}
The mode is changed on pressing Button B
of the M5StickC. The new mode is stored in non-volatile memory followed by a reboot of the device:
if (M5.BtnB.wasPressed()) {
if (deviceMode_ == RADIO) {
EEPROM.writeByte(0, 2); // Enter A2DP mode after restart
EEPROM.commit();
stopRadio(); // Close connections and clean up
}
else {
EEPROM.writeByte(0, 1); // Enter internet radio mode after restart
EEPROM.commit();
}
ESP.restart();
}
The Audio
class is instantiated dynamically and only if needed:
Audio *pAudio_ = nullptr;
pAudio_ = new Audio(false); // Use external DAC
If the BluetoothA2DPSink
is instantiated dynamically, it causes exception traces when being used later on. However, the instance itself consumes only a few hundred bytes. The allocation of resources such as the bluetooth stack does not happen until calling the start
method. Therefore, I instantiate it statically and call start
only when the application boots in "A2DP" mode.
BluetoothA2DPSink a2dp_ = BluetoothA2DPSink();
a2dp_.start(kDeviceName);
Displaying Artist and Song TitleThe following callback routine is used for processing song meta data provided by the bluetooth source. When the routine receives the title or the artist attribute, it updates the string that is shown on the TFT display:
void avrc_metadata_callback(uint8_t id, const uint8_t *text) {
switch (id) {
case ESP_AVRC_MD_ATTR_TITLE:
titleStr_ = (char*) text;
break;
case ESP_AVRC_MD_ATTR_ARTIST:
artistStr_ = (char*) text;
break;
}
if ( artistStr_.isEmpty() ) {
infoStr_ = titleStr_;
}
else {
if ( titleStr_.isEmpty() ) {
infoStr_ = artistStr_;
}
else {
infoStr_ = artistStr_ + " - " + titleStr_;
}
}
infoUpdatedFlag_ = true; // Raise flag for the display update routine
}
Outlook on Part IIIIn Part III of this project, we will send the artist and title of the current radio song to an IFTTT webhook in order to store it in a list of favourite songs.
Comments