ESP32: Zvuk z MP3 na BT reproduktor
V předchozích článcích jsme se věnovali různým možnostem reprodukce zvuku pomocí modulu ESP32. Ve všech uvedených případech byl audio výstup realizován pomocí vestavěného digitálně-analogového převodníku (DAC), na který navazoval externí audio zesilovač určený pro připojení reproduktoru nebo sluchátek.
K dnešnímu zapojení nás tak trochu inspiroval sám autor knihovny Arduino Audio Tools, Phil Schatzmann. Ten kromě této knihovny na svém GitHubu prezentuje také projekt ESP32-A2DP, jenž umožňuje nakonfigurovat modul ESP32 jako Bluetooth audio zařízení – konkrétně jako přijímač (sink) nebo vysílač (source). Odtud už byl jen krok k myšlence zkusit přehrávat zvukový soubor uložený v paměti ESP32 a odesílat jej bezdrátově do spárovaného Bluetooth reproduktoru.
Cíl dnešního článku je tedy poměrně jasný: přehrát jeden nebo více MP3 souborů uložených v paměti modulu ESP32 a přenést jejich audio data přes Bluetooth do externího BT reproduktoru. V praxi to znamená projít celým řetězcem kroků – od spárování zařízení, přes načtení a dekódování MP3 souboru, až po vytvoření plynulého audio datového proudu kompatibilního s profilem Bluetooth A2DP, který reproduktor očekává.
Některé dílčí kroky, jako je uložení MP3 souboru do paměti ESP32, jeho načtení a samotné dekódování, jsme již podrobně řešili v článku Zvukový výstup (MP3) na ESP32. Tyto části zde proto nebudeme zbytečně opakovat. Čtenářům, kteří s knihovnou Arduino Audio Tools zatím nemají zkušenosti, doporučujeme zejména prostudovat druhou část zmíněného článku, na niž budeme v dalším textu přímo navazovat a často se na ni odkazovat.
Co budeme potřebovat?
Pro dnešní „BT hrátky“ budeme potřebovat vývojový kit s modulem ESP32 (my použijeme DOIT DEVKIT s modulem ESP32-WROOM) a libovolný Bluetooth reproduktor nebo jiné audio zařízení (soundbar, Hi-Fi systém apod.), které umožňuje bezdrátové přehrávání hudby. Funkčnost reproduktoru je vhodné hned na začátku ověřit například připojením k mobilnímu telefonu.
Spárování výstupního Bluetooth audio zařízení s mobilním telefonem nám poslouží nejen pro základní kontrolu funkčnosti (případně i zjištění stavu baterie), ale především k tomu, abychom zjistili přesné označení zařízení, pod kterým se reproduktor hlásí. Toto označení budeme později potřebovat přímo v programu běžícím na modulu ESP32.
Obrázek č. 1 – Připojení mobilu k BT reproduktoru nám zobrazí jméno
Je však potřeba dát pozor na jednu častou záludnost: pokud jste si Bluetooth zařízení v minulosti v telefonu ručně přejmenovali, může se zobrazovaný název lišit od skutečného názvu zařízení. Zatímco samotné BT zařízení se stále identifikuje svým originálním názvem (např. EC713), telefon k jeho MAC adrese přiřadí vámi zvolený popis typu „běhací sluchátka“, „moje Hi-Fi“ nebo třeba „BT ňufínek“. V takovém případě je často jednodušší použít telefon, ke kterému daný Bluetooth reproduktor nikdy nebyl připojen ani spárován, a znovu jej vyhledat.
Jakmile známe správný „volací znak“ našeho Bluetooth reproduktoru, můžeme jej od mobilního telefonu opět odpojit a postoupit k dalším krokům.
Kód programu
Náš první ukázkový program bude mít poměrně jednoduchý úkol: po stisknutí vestavěného tlačítka na vývojovém kitu ESP32 (tlačítko označené IO0 nebo BOOT) se přehraje první MP3 soubor, při dalším stisknutí druhý. Oba audio soubory budou uloženy ve flash paměti modulu ESP32.
Nejprve si ukážeme celý program jako celek, poté si jej postupně rozebereme a nakonec jej prakticky vyzkoušíme.
#include "AudioTools.h" // Hlavní knihovna AudioTools – poskytuje AudioStream, AudioPlayer atd.
#include "AudioTools/Disk/AudioSourceLittleFS.h" // Podpora čtení MP3 z LittleFS
#include "AudioTools/AudioCodecs/CodecMP3Helix.h" // MP3 dekodér Helix
#include <LittleFS.h> // Souborový systém LittleFS pro ESP32
#include "BluetoothA2DPSource.h" // Bluetooth A2DP source pro ESP32
#define BT_SPEAKER_NAME "EC713" // nazev BT reproduktoru
#define MP3_FILE_NAME_1 "plihal-TEST-1.mp3" // 1. soubor
#define MP3_FILE_NAME_2 "plihal-TEST-2.mp3" // 2. soubor
#define BUTTON_PIN 0 // tlacitko
bool currentFile = false; // false = 1, true = 2
bool lastButtonState = HIGH; // stav tlacitka
bool playbackSTOP = true;
// AUDIO SOURCE – MP3 soubory z LittleFS
// Zdroj audio dat, zde MP3 soubory z root adresáře LittleFS
AudioSourceLittleFS source("/", "mp3"); // "/" = root, "mp3" = filtrování jen MP3 souborů - bere je postupne
MP3DecoderHelix decoder; // Dekodér pro převod MP3 na PCM (raw audio)
// BUFFER – kruhový buffer pro přenos PCM dat
// Buffer slouží jako mezivrstva mezi dekodérem a BT stackem
const size_t BUFFER_SIZE = 32 * 1024; // 32 kB buffer (velikost lze upravit podle RAM)
RingBuffer<uint8_t> ringBuffer(BUFFER_SIZE); // deklarace kruhového bufferu
bool btConnected = false; // Flag, zda je BT reproduktor připojen
// CUSTOM STREAM – přepis MP3 dekodéru do bufferu
// Třída implementující AudioStream pro zápis dekódovaných dat do ringBufferu
class BTBufferStream : public AudioStream {
public:
// AudioPlayer bude volat write() po dekódování každého bloku MP3
size_t write(const uint8_t *data, size_t len) override {
// Zapíšeme dekódovaná PCM data do kruhového bufferu
return ringBuffer.writeArray(data, len);
}
};
BTBufferStream btStream; // instance custom streamu
// AUDIO PLAYER – přehrávač MP3 -> buffer
// Player spojuje zdroj (MP3) s dekodérem a výstupem (náš buffer)
AudioPlayer player(source, btStream, decoder);
// BLUETOOTH A2DP
BluetoothA2DPSource a2dp_source; // instance BT A2DP stacku
// CALLBACK – dodání dat pro Bluetooth
// Tato funkce je volána BT stackem při potřebě dalších audio rámců
int32_t get_data_frames(Frame *frame, int32_t frame_count) {
if (playbackSTOP) {
memset(frame, 0, frame_count * sizeof(Frame));
return frame_count;
}
int needed = frame_count * sizeof(Frame); // Kolik byteů potřebujeme vyplnit
// Čteme data z ringBufferu
int read = ringBuffer.readArray((uint8_t*)frame, needed);
// Pokud není dost dat, doplníme ticho (PCM = 0)
if (read < needed) {
memset(((uint8_t*)frame) + read, 0, needed - read);
}
return frame_count; // Vracíme počet rámců připravených pro BT stack
}
// CALLBACK – změna stavu Bluetooth
// Tato funkce se volá při změně stavu připojení BT reproduktoru
void bt_state_changed(esp_a2d_connection_state_t state, void *ptr) {
Serial.print("[i] BT stav: ");
Serial.println(state); // vypíše číselný stav
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) { // připojeno
Serial.println("[*] Bluetooth připojeno!");
btConnected = true;
// Volná RAM pro debugging
Serial.print("Volná RAM před startem playeru: ");
Serial.println(ESP.getFreeHeap());
// Spuštění AudioPlayeru až po připojení BT
// player.begin(); // začne číst MP3 a zapisovat do bufferu
} else { // odpojeno
btConnected = false;
Serial.println("[!] Bluetooth odpojeno!");
}
}
// CALLBACK – metadata z MP3 (volitelné)
// Při dekódování MP3 se některé tagy (artist, title, album) mohou vypsat
void printMetaData(MetaDataType type, const char* str, int len){
Serial.print("==> ");
Serial.print(toStr(type)); // typ metadata
Serial.print(": ");
Serial.println(str); // hodnota metadata
}
void switchTrack() {
Serial.println("== PREPINAM SKLADBU ==");
// zastavit player
player.end();
playbackSTOP = true;
// vyčistit buffer
ringBuffer.reset();
// přepnout soubor
currentFile = !currentFile;
if (currentFile) {
Serial.println("Hraju soubor 2");
source.setFileFilter(MP3_FILE_NAME_2);
} else {
Serial.println("Hraju soubor 1");
source.setFileFilter(MP3_FILE_NAME_1);
}
// znovu spustit (jen pokud BT připojeno)
if (btConnected) {
player.begin();
playbackSTOP = false;
}
}
// SETUP – inicializace
void setup() {
Serial.begin(115200); // Inicializace Serial pro debug
pinMode(BUTTON_PIN, INPUT_PULLUP); // nastaveni tlacitka
// Inicializace souborového systému LittleFS
if(!LittleFS.begin(true)) {
Serial.println("Chyba: LittleFS nenacteno.");
while(true); // zastavení, pokud FS nefunguje
}
// ===== INFO O LITTLEFS =====
size_t totalBytes = LittleFS.totalBytes();
size_t usedBytes = LittleFS.usedBytes();
size_t freeBytes = totalBytes - usedBytes;
Serial.println("=== LITTLEFS INFO ===");
Serial.print("Celkem: "); Serial.print(totalBytes); Serial.println(" B");
Serial.print("Využito: "); Serial.print(usedBytes); Serial.println(" B");
Serial.print("Volné: "); Serial.print(freeBytes); Serial.println(" B");
Serial.println("=====================");
// Výpis souborů v LittleFS
Serial.println("Soubory v LittleFS:");
File root = LittleFS.open("/");
File file = root.openNextFile();
while(file){
Serial.println(file.name()); // jméno souboru
file = root.openNextFile();
}
source.setAutoNext(false); // automaticky neprehravej dalsi
source.setFileFilter(MP3_FILE_NAME_1); // vyfiltruj konkretni soubor
// ===== AUDIO PLAYER SETUP =====
player.setMetadataCallback(printMetaData); // nastavíme callback pro metadata
// ===== BLUETOOTH SETUP =====
a2dp_source.set_auto_reconnect(false); // nevynucovat automatické reconnect
a2dp_source.set_data_callback_in_frames(get_data_frames); // callback pro audio data
a2dp_source.set_on_connection_state_changed(bt_state_changed); // callback pro změnu stavu
a2dp_source.set_volume(100); // nastavíme maximální hlasitost (0–100)
// Spuštění BT připojení
a2dp_source.start(BT_SPEAKER_NAME); // název reproduktoru
Serial.println("Čekám na připojení BT...");
}
// LOOP – hlavní smyčka
void loop() {
if (btConnected) { // jen pokud je BT připojeno
if (!playbackSTOP && !player.copy()) {
Serial.println("=== KONEC SKLADBY ===");
playbackSTOP = true;
}
delay(5); // malé zpomalení – stabilita BT stacku
bool buttonState = digitalRead(BUTTON_PIN);
// detekce hrany (kliknutí)
if (lastButtonState == HIGH && buttonState == LOW) {
switchTrack();
delay(300); // jednoduchý debounce
}
lastButtonState = buttonState;
}
delay(1);
}
Použité knihovny a makra
Na začátku programu připojíme všechny potřebné knihovny a nadefinujeme si používaná makra. Knihovny AudioTools.h, AudioSourceLittleFS.h a CodecMP3Helix.h slouží k načítání MP3 souboru z paměti, jeho dekódování a přípravě audio dat. O samotnou správu paměti pomocí souborového systému LittleFS se pak stará knihovna LittleFS.h. Pro Bluetooth přenos audia využijeme knihovnu BluetoothA2DPSource.h.
Dále definujeme několik maker. Makro BT_SPEAKER_NAME obsahuje název Bluetooth reproduktoru, ke kterému se bude ESP32 připojovat. Sem tedy dáme název svého BT reproduktoru, který jsme se dozvěděli na mobilním telefonu.
Makra MP3_FILE_NAME_1 a MP3_FILE_NAME_2 pak určují názvy MP3 souborů, které budeme postupně zařazovat do přehrávání. Makro BUTTON_PIN
udává číslo GPIO pinu, na který je připojeno vestavěné tlačítko BOOT vývojového kitu.
#include "AudioTools.h"
#include "AudioTools/Disk/AudioSourceLittleFS.h"
#include "AudioTools/AudioCodecs/CodecMP3Helix.h"
#include <LittleFS.h>
#include "BluetoothA2DPSource.h"
#define BT_SPEAKER_NAME "EC713"
#define MP3_FILE_NAME_1 "plihal-TEST-1.mp3"
#define MP3_FILE_NAME_2 "plihal-TEST-2.mp3"
#define BUTTON_PIN 0
Stavové proměnné
V další části deklarujeme trojici logických proměnných, do nichž budeme ukládat aktuální stav přehrávání, stav tlačítka a informaci o tom, zda má být přehrávání aktivní či zastavené. Konkrétní využití těchto proměnných si ukážeme později přímo v kódu.
bool currentFile = false;
bool lastButtonState = HIGH;
bool playbackSTOP = true;
Audio a Bluetooth objekty
Pro samotné načítání MP3 souborů a jejich přípravu k odeslání přes Bluetooth nás bude zajímat především následující část, ve které deklarujeme objekty pro práci s audiem:
AudioSourceLittleFS source("/", "mp3");
MP3DecoderHelix decoder;
Objekt source reprezentuje zdroj všech souborů s příponou .mp3 uložených v kořenovém adresáři / souborového systému LittleFS. Objekt decoder pak slouží jako dekodér, který provádí převod komprimovaných MP3 dat na nekomprimovaná PCM audio data.
Tyto objekty už pravděpodobně známe z článku věnovaného klasickému přehrávání MP3 souborů na ESP32. V tehdejším řešení jsme si vytvořili přehrávač, který načítal data z MP3 souboru, dekódoval je a následně je odesílal na analogový výstup (interní DAC). Tentokrát je však situace odlišná – místo analogového výstupu potřebujeme dekódovaná audio data předat Bluetooth knihovně, která je odešle bezdrátově do BT reproduktoru.
K tomu už nestačí přímo propojit zdroj a výstup. Potřebujeme vytvořit určitou „mezivrstvu“, která se bude z pohledu dekodéru chovat podobně jako běžný audio výstup a z pohledu Bluetooth knihovny naopak jako zdroj dat pro správný přenos pomocí profilu A2DP.
Jednou z možností by teoreticky bylo dekódovat celý MP3 soubor do paměti a teprve poté jej začít odesílat přes Bluetooth. Toto řešení má však hned několik zásadních nevýhod. Nejenže bychom museli čekat na dokončení celého dekódování, čímž by vznikla nežádoucí prodleva, ale především bychom čelili obrovským nárokům na paměť – velikost nekomprimovaných audio dat je totiž několikanásobně větší než samotný MP3 soubor. Takový přístup je proto v praxi nepoužitelný.
Naštěstí existuje mnohem elegantnější řešení. Vytvoříme si malou mezipaměť (například 32 kB), která bude sloužit jako buffer mezi dekodérem a Bluetooth přenosem.
Je vlastně docela úsměvné označovat 32 kB paměť za „malou“, pokud si uvědomíme, že u 8bitových počítačů typu Atari nebo Commodore to představovalo kompletní využitelnou paměť celého stroje. 😊
Tato mezipaměť bude fungovat jako tzv. kruhový buffer. Dekodér do něj bude postupně zapisovat audio data a Bluetooth knihovna je bude současně číst a odesílat do reproduktoru. Jakmile zapisovací ukazatel dosáhne konce bufferu, vrátí se opět na jeho začátek, a zápis tak probíhá stále dokola, jako bychom měli k dispozici mnohem větší paměťový prostor.
Samozřejmě je nutné zajistit, aby zápis nikdy „neutekl“ čtení a nezačal přepisovat data, která ještě nebyla odeslána. To je v našem případě řešeno vhodnou volbou rychlostí zpracování a velikosti bufferu, což si prakticky ověříme při samotném běhu programu.
Následující řádky ukazují deklaraci zmíněného kruhového bufferu pro PCM audio data o velikosti 32 kB:
const size_t BUFFER_SIZE = 32 * 1024;
RingBuffer<uint8_t> ringBuffer(BUFFER_SIZE);
Tento buffer bude sloužit jako mezivrstva mezi MP3 dekodérem a Bluetooth A2DP zásobníkem. Jeho úkolem je plynule přijímat dekódovaná audio data a současně je poskytovat Bluetooth knihovně k odeslání do reproduktoru.
Vlastní stream pro zápis do bufferu
Abychom mohli kruhový buffer pohodlně plnit přímo z audio přehrávače, vytvoříme si vlastní třídu, kterou nazveme BTBufferStream. Tato třída bude dědit z obecné třídy AudioStream a implementuje metodu write(), kterou třída AudioPlayer automaticky volá po dekódování každého bloku MP3 dat.
Tímto postupem vlastně napodobíme chování třídy AnalogAudioStream, kterou jsme v minulém článku používali pro odesílání dat na analogový výstup (DAC). V našem případě však data neputují na výstupní převodník, ale pouze do kruhového bufferu. Při návrhu této „odlehčené“ třídy nám pomohla analytická práce umělé inteligence, která prošla strukturu původní třídy AnalogAudioStream a navrhla tuto minimalistickou variantu obsahující pouze nezbytný zápis do bufferu.
class BTBufferStream : public AudioStream {
public:
size_t write(const uint8_t *data, size_t len) override {
return ringBuffer.writeArray(data, len);
}
};
Pro samotné použití tohoto streamu si následně vytvoříme objekt btStream, který bude představovat náš vlastní „custom“ audio výstup. Jeho úloha je přímo
analogická objektu out z dřívějšího řešení s DAC – jen místo fyzického výstupu zapisuje data do bufferu.
BTBufferStream btStream;
Vytvoření audio přehrávače
Máme-li nyní:
- definovaný zdroj MP3 souborů (
source), - výstup do bufferu (
btStream), - MP3 dekodér (
decoder),
můžeme vše propojit vytvořením objektu přehrávače player (třída AudioPlayer). Tento objekt zajišťuje kompletní tok dat: od načítání MP3 souboru, přes dekódování, až po zápis výsledných PCM dat do našeho kruhového bufferu.
AudioPlayer player(source, btStream, decoder);
V tuto chvíli se kruhový buffer postupně plní dekódovanými audio daty. Dalším krokem je jejich odesílání do světa prostřednictvím Bluetooth.
A2DP zdroj a callback pro data
Nejprve vytvoříme instanci A2DP zásobníku, který se postará o přenos audio dat do Bluetooth reproduktoru:
BluetoothA2DPSource a2dp_source;
Bluetooth A2DP stack očekává, že mu aplikace poskytne callback funkci, kterou bude volat pokaždé, když bude potřebovat další audio rámce k odeslání. Tuto funkci nazveme get_data_frames a její návrh vychází z ukázkových příkladů knihovny BluetoothA2DPSource.
int32_t get_data_frames(Frame *frame, int32_t frame_count) {
if (playbackSTOP) {
memset(frame, 0, frame_count * sizeof(Frame));
return frame_count;
}
int needed = frame_count * sizeof(Frame);
int read = ringBuffer.readArray((uint8_t*)frame, needed);
if (read < needed) {
memset(((uint8_t*)frame) + read, 0, needed - read);
}
return frame_count;
}
Na začátku funkce je kontrola stavové proměnné playbackSTOP. Pokud je přehrávání zastaveno, jsou všechna data v rámci vynulována a do Bluetooth
připojeného zařízení je odesíláno ticho.
V opačném případě funkce čte potřebné množství dat z kruhového bufferu, kde jsou uložena dekódovaná PCM data z MP3 souboru. Pokud by v bufferu nebylo k dispozici dostatek dat (například při dohrání skladby), je zbytek rámce doplněn nulami, aby byl zachován správný formát audio datového toku.
Callback změny stavu Bluetooth
Další důležitou callback funkcí A2DP zásobníku je funkce bt_state_changed, která reaguje na změny stavu Bluetooth připojení. Tato funkce je prakticky beze změn
převzata z ukázkového programu knihovny BluetoothA2DPSource a slouží k detekci navázání a ukončení spojení s BT reproduktorem.
void bt_state_changed(esp_a2d_connection_state_t state, void *ptr) {
Serial.print("[i] BT stav: ");
Serial.println(state);
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
Serial.println("[*] Bluetooth připojeno!");
btConnected = true;
Serial.print("Volná RAM před startem playeru: ");
Serial.println(ESP.getFreeHeap());
} else {
btConnected = false;
Serial.println("[!] Bluetooth odpojeno!");
}
}
Pomocné funkce přehrávače
V další části programu jsou definovány pomocné funkce využívané přehrávačem. Jednou z nich je funkce printMetaData, která vypisuje do okna sériového monitoru metadata aktuálně přehrávaného MP3 souboru (např. název skladby, interpret apod.).
void printMetaData(MetaDataType type, const char* str, int len){
Serial.print("==> ");
Serial.print(toStr(type));
Serial.print(": ");
Serial.println(str);
}
Přepínání skladeb
Funkci switchTrack využíváme pro přepínání jednotlivých skladeb. Na jejím začátku se nejprve zastaví aktuální přehrávání, vyčistí se kruhový buffer a následně se přepne proměnná currentFile na opačný logický stav. Na základě této hodnoty je poté v podmínce vybrán konkrétní MP3 soubor, který má být dále přehráván.
Je třeba si uvědomit, že toto řešení není obecně ideální a v této podobě prakticky funguje pouze pro dva soubory. Pro potřeby ukázkového projektu to však zcela postačuje. Čtenář, který se rozhodne pustit do vlastního projektu s odesíláním MP3 souborů přes Bluetooth do BT reproduktoru, si tuto část kódu jistě dokáže snadno upravit podle svých potřeb (například pomocí pole názvů souborů nebo indexu aktuální skladby).
Za pozornost ale rozhodně stojí způsob, jakým vybíráme konkrétní soubor ze seznamu načtených MP3 souborů v objektu source. V některých tutoriálech, které popisují starší verze knihovny AudioTools.h, se objevují různé metody pro přímé spuštění konkrétního souboru. Naše zkušenost však ukazuje, že mnoho z těchto metod již v aktuální verzi knihovny neexistuje a překlad programu často končí chybou.
Použití metody setFileFilter( se tak v současnosti ukazuje jako nejspolehlivější funkční způsob, jak přehrávači AudioPlayer
jednoznačně předat správný MP3 soubor.
void switchTrack() {
Serial.println("== PREPINAM SKLADBU ==");
// zastavit player
player.end();
playbackSTOP = true;
// vyčistit buffer
ringBuffer.reset();
// přepnout soubor
currentFile = !currentFile;
if (currentFile) {
Serial.println("Hraju soubor 2");
source.setFileFilter(MP3_FILE_NAME_2);
} else {
Serial.println("Hraju soubor 1");
source.setFileFilter(MP3_FILE_NAME_1);
}
// znovu spustit (jen pokud BT připojeno)
if (btConnected) {
player.begin();
playbackSTOP = false;
}
}
Nastavení přehrávače ve funkci setup
V části setup() je nejprve provedeno nastavení souborového systému LittleFS, včetně výpisu jeho základních parametrů na sériový port. Tuto část zde nebudeme detailně rozebírat, protože se nyní zaměřujeme především na samotnou funkčnost přehrávání MP3 souborů přes Bluetooth.
Pozornost tedy věnujme až následujícím nastavením:
source.setAutoNext(false);
Nastavení hodnoty false zajistí, že po dohrání skladby nebude automaticky spuštěn další soubor.
source.setFileFilter(MP3_FILE_NAME_1);
Pomocí filtru vybereme první skladbu, která se má přehrát po startu programu. Tento mechanismus jsme již viděli ve funkci switchTrack.
player.setMetadataCallback(printMetaData);
Přiřazením funkce printMetaData k metodě setMetadata zajistíme, že se metadata právě přehrávané skladby budou vypisovat do sériového monitoru. Tím máme základní část MP3 přehrávače hotovou.
Nastavení Bluetooth části
Nyní přichází na řadu konfigurace Bluetooth. Nejprve musíme objektu a2dp_source přiřadit callback funkci, která bude poskytovat audio rámce k odeslání – tedy naši funkci get_data_frames. Zároveň nastavíme callback funkci bt_state_changed, která zpracovává změny stavu Bluetooth připojení.
a2dp_source.set_data_callback_in_frames(get_data_frames);
a2dp_source.set_on_connection_state_changed(bt_state_changed);
V této části programu je dále možné nastavit například vynucení automatického reconnectu nebo výstupní hlasitost. Následuje samotné zahájení pokusu o připojení k Bluetooth reproduktoru se zadaným názvem:
a2dp_source.start(BT_SPEAKER_NAME);
Serial.println("Čekám na připojení BT...");
Hlavní smyčka loop
V nekonečné smyčce loop() hlavního programu se především testuje, zda již došlo k úspěšnému připojení Bluetooth zařízení – jinak nemá smysl
jakkoli posílat audio data.
if (btConnected) { // jen pokud je BT připojeno
...
Uvnitř této podmínky se zároveň testuje stisknutí tlačítka. Je zde implementován jednoduchý pokus o potlačení zákmitů mechanického kontaktu (debouncing), a to porovnáním aktuálního a předchozího stavu tlačítka spolu s krátkou prodlevou 300 ms. Při detekci stisknutí tlačítka je zavolána funkce switchTrack, která přepne skladbu.
bool buttonState = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH && buttonState == LOW) {
switchTrack();
delay(300);
}
lastButtonState = buttonState;
Dále je nutné ošetřit stav, kdy skladba doběhne až do konce. Pokud je playbackSTOP nastaveno na false (tedy skladba se přehrává), ale metoda player.copy() již vrací hodnotu 0, znamená to, že přehrávač nemá žádná další data ke zpracování. V takovém případě je nutné přepnout stav přehrávače do režimu „zastaveno“ (playbackSTOP = true).
if (!playbackSTOP && !player.copy()) {
Serial.println("=== KONEC SKLADBY ===");
playbackSTOP = true;
}
Praktické vyzkoušení programu
Nakonec už nezbývá než náš program prakticky vyzkoušet. Do flash paměti spravované souborovým systémem LittleFS (viz článek Kam (zase) se soubory? Aneb LittleFS…) nahrajeme dvojici MP3 souborů. V našem případě se musí jmenovat plihal-TEST-1.mp3 a plihal-TEST-2.mp3, nicméně tyto názvy lze snadno změnit v makrech definovaných na začátku programu.
Je dobré připomenout, že standardní velikost paměti vyhrazené pro LittleFS je zhruba 1,5 MB, což není příliš mnoho. Pro naše dvě básničky Karla Plíhala to však bohatě postačí.
Obrázek č. 2 – Modul ESP32 a Bluetooth reproduktor – test kódu může začít
Před přeložením programu je vhodné ještě jednou zkontrolovat, zda je v úvodu správně nastaven název našeho Bluetooth reproduktoru. Pokud je vše v pořádku, zapneme BT reproduktor a nahrajeme program do modulu ESP32. Po úspěšném nahrání je dobré program pro jistotu znovu spustit stisknutím resetovacího tlačítka EN na vývojovém kitu.
Následně už nezbývá než chvíli počkat a sledovat výstup v okně Serial Monitoru. Proces připojování Bluetooth zařízení nebývá úplně nejrychlejší, takže je potřeba mít trochu trpělivosti. Úspěšné navázání spojení by nám měla signalizovat nejen hláška vypsaná v sériovém monitoru, ale obvykle také zvukový signál samotného BT reproduktoru, který oznámí, že byl úspěšně připojen nový zdroj zvuku.
V této chvíli můžeme stisknout tlačítko BOOT na vývojovém kitu ESP32. Z Bluetooth reproduktoru by se měla ozvat první MP3 skladba. Při dalším stisknutí tlačítka se přehraje skladba druhá. Protože je program v této ukázkové podobě připraven pouze pro tyto dva soubory, budou se při každém dalším stisknutí
tlačítka oba MP3 soubory cyklicky střídat.
Obrázek č. 3 – Výstup v okně Serial Monitoru
Upřímně řečeno – jako základní ukázka přehrávání MP3 souborů z ESP32 přes Bluetooth do externího reproduktoru to zatím bohatě stačilo.
Jenže když už používáme Bluetooth reproduktor, nabízí se otázka: proč jej neovládat způsobem, na který jsme zvyklí?
Většina Bluetooth reproduktorů má vlastní tlačítka Play, Pause, Next a Previous. Místo ovládání přehrávání
tlačítkem BOOT na vývojovém kitu by tedy bylo mnohem praktičtější reagovat přímo na tato tlačítka. Právě tím se celý projekt posune blíže chování běžného Bluetooth audio přehrávače – s tím rozdílem, že veškerá logika stále běží v našem vlastním programu na modulu ESP32.
Ovládání přehrávání pomocí tlačítek Bluetooth reproduktoru (AVRCP)
Abychom toho dosáhli, musíme využít další Bluetooth profil, konkrétně AVRCP (Audio/Video Remote Control Profile). Zatímco profil A2DP
zajišťuje samotný přenos audio dat, AVRCP slouží výhradně k dálkovému ovládání přehrávání. Přenáší tedy pouze povely typu:
- PLAY/PAUSE – spuštění nebo pozastavení přehrávání,
- NEXT – přechod na další skladbu,
- PREVIOUS – návrat na předchozí skladbu.
V našem zapojení funguje Bluetooth reproduktor jako ovládací zařízení a ESP32 jako řízený přehrávač. Při stisku tlačítka na reproduktoru je tedy přes Bluetooth odeslán odpovídající AVRCP příkaz, na který můžeme v programu na ESP32 reagovat.
Dobrou zprávou je, že knihovna BluetoothA2DPSource, kterou již používáme pro přenos audia, má podporu AVRCP zabudovanou. Stačí tedy vhodným callbackem
zachytit příchozí povely a namapovat je na existující funkce přehrávače – například přepnutí skladby, pauzu nebo opětovné spuštění přehrávání.
V následující části se proto podíváme na to, jak AVRCP příkazy v programu zachytit a zpracovat a jak jednoduše nahradit původní ovládání tlačítkem na
kitu ovládáním přímo z Bluetooth reproduktoru.
Místo GPIO vstupu AVRCP callback
Největší rozdíl mezi předchozím kódem a kódem následujícím spočívá v tom, odkud přichází povely k ovládání přehrávání.
V předešlém kódu jsme v hlavní smyčce loop() testovali stav pinu GPIO0 a podle jeho změny volali funkci switchTrack(),
tak tato část zcela zmizí. Fyzické tlačítko na ESP32 už nepotřebujeme – místo toho budeme reagovat na AVRCP příkazy, které přicházejí z Bluetooth
reproduktoru.
Knihovna BluetoothA2DPSource nám k tomu nabízí speciální callback funkci:
a2dp_source.set_avrc_passthru_command_callback(button_handler);
Tímto voláním říkáme Bluetooth stacku, že chceme být informováni o každém stisku tlačítka na připojeném BT zařízení.
Callback pro tlačítka BT reproduktoru
Samotné zpracování tlačítek probíhá ve funkci button_handler. Ta dostane dva parametry:
- kód příkazu (
cmd), - informaci, zda bylo tlačítko právě uvolněno (
released).
void button_handler(uint8_t cmd, bool released) {
if (!released) return;
Reakce až na uvolnění tlačítka je praktická – zabrání opakovanému vyvolání akce při delším stisku.
Dále už jen rozlišujeme jednotlivé AVRCP povely podle jejich kódů:
switch(cmd) {
case 0x44: // PLAY
case 0x46: // PAUSE
...
break;
case 0x4B: // NEXT
...
break;
case 0x4C: // PREVIOUS
...
break;
}
Play/Pause: řízení přehrávání bez zastavení dekodéru
Velmi důležité je, jak je řešena dvojice příkazů Play/Pause. Tyto povely nezastavují ani nespouštějí Bluetooth přenos, ale pouze ovlivňují, zda se mají dekódovaná data odesílat, nebo ne.
case 0x44: // PLAY
case 0x46: // PAUSE
if (playbackSTOP) {
Serial.println("=== PLAY ===");
ringBuffer.reset();
player.begin();
playbackSTOP = false;
} else {
Serial.println("=== STOP ===");
playbackSTOP = true;
}
break;
Zásadní rozdíl oproti původnímu switchTrack() je v tom, že při pauze nevoláme player.end(). Dekodér i přehrávač zůstávají připravené a pouze se přeruší tok dat do Bluetooth bufferu. Ticho je zajištěno přímo v callbacku get_data_frames().
Tento přístup odpovídá chování běžných Bluetooth přehrávačů a je výrazně stabilnější než opakované ukončování a spouštění dekodéru.
Next/Previous: přepnutí skladeb z reproduktoru
Tlačítka Next a Previous jsou mapována přímo na dříve vytvořenou funkci switchTrack(), kterou už známe z předešlého kódu:
case 0x4B: // NEXT
Serial.println("=== NEXT ===");
switchTrack();
break;
case 0x4C: // PREVIOUS
Serial.println("=== PREVIOUS ===");
switchTrack();
break;
To není opět zcela elegantní řešení, ale pro naši ukázku dostačuje – a hlavně logika přepínání skladeb se nemusela vůbec měnit, pouze se změnil zdroj povelu. Místo tlačítka na GPIO nyní přepnutí vyvolává AVRCP příkaz z Bluetooth reproduktoru.
Zjednodušená hlavní smyčka loop()
Díky přesunutí veškerého ovládání do callbacků Bluetooth se výrazně zjednodušila i hlavní smyčka programu:
void loop() {
if (btConnected && !playbackSTOP) {
if (!player.copy()) {
Serial.println("=== KONEC SKLADBY ===");
playbackSTOP = true;
}
}
delay(5);
}
V loop() už neřešíme žádný vstup od uživatele. Smyčka pouze:
- kopíruje data z MP3 přehrávače do bufferu,
- detekuje konec skladby.
Celé ovládání přehrávání je tak nyní plně řízené dálkově pomocí tlačítek Bluetooth reproduktoru.
Nyní se tedy podíváme na kód programu, který nám umožní přehrávat MP3 soubory pomocí tlačítka na BT reproduktoru.
Kód programu:
#include "AudioTools.h"
#include "AudioTools/Disk/AudioSourceLittleFS.h"
#include "AudioTools/AudioCodecs/CodecMP3Helix.h"
#include <LittleFS.h>
#include "BluetoothA2DPSource.h"
#define BT_SPEAKER_NAME "EC713"
#define MP3_FILE_NAME_1 "plihal-TEST-1.mp3"
#define MP3_FILE_NAME_2 "plihal-TEST-2.mp3"
bool currentFile = false;
bool playbackSTOP = true;
bool btConnected = false;
// ===== AUDIO SOURCE =====
AudioSourceLittleFS source("/", "mp3");
MP3DecoderHelix decoder;
// ===== RING BUFFER =====
const size_t BUFFER_SIZE = 32 * 1024;
RingBuffer<uint8_t> ringBuffer(BUFFER_SIZE);
// ===== CUSTOM STREAM PRO BT =====
class BTBufferStream : public AudioStream {
public:
size_t write(const uint8_t *data, size_t len) override {
return ringBuffer.writeArray(data, len);
}
};
BTBufferStream btStream;
// ===== AUDIO PLAYER =====
AudioPlayer player(source, btStream, decoder);
// ===== BLUETOOTH =====
BluetoothA2DPSource a2dp_source;
// ============================
// CALLBACK – DODÁNÍ PCM DAT BT STACKU
// ============================
int32_t get_data_frames(Frame *frame, int32_t frame_count) {
if (playbackSTOP) {
memset(frame, 0, frame_count * sizeof(Frame));
return frame_count;
}
int needed = frame_count * sizeof(Frame);
int read = ringBuffer.readArray((uint8_t*)frame, needed);
if (read < needed) {
memset(((uint8_t*)frame) + read, 0, needed - read);
}
return frame_count;
}
// PŘEPÍNAČ SOUBORŮ
void switchTrack() {
Serial.println("== PREPINAM SKLADBU ==");
player.end();
playbackSTOP = true;
ringBuffer.reset();
currentFile = !currentFile;
if (currentFile) {
Serial.println("Hraju soubor 2");
source.setFileFilter(MP3_FILE_NAME_2);
} else {
Serial.println("Hraju soubor 1");
source.setFileFilter(MP3_FILE_NAME_1);
}
if (btConnected) {
player.begin();
playbackSTOP = false;
}
}
// CALLBACK – TLAČÍTKA BT REPRODUKTORU
void button_handler(uint8_t cmd, bool released) {
if (!released) return; // reagujeme až po puštění tlačítka
Serial.print("[BT BUTTON] command 0x");
Serial.println(cmd, HEX);
switch(cmd) {
case 0x44: // PLAY
case 0x46: // PAUSE
if (playbackSTOP) {
Serial.println("=== PLAY ===");
// pokud player nikdy neběžel, musíme ho spustit
ringBuffer.reset();
player.begin();
playbackSTOP = false;
} else {
Serial.println("=== STOP ===");
// jen pauza – NEZASTAVUJEME PLAYER!
playbackSTOP = true;
}
break;
case 0x4B: // NEXT
Serial.println("=== NEXT ===");
switchTrack();
break;
case 0x4C: // PREVIOUS
Serial.println("=== PREVIOUS ===");
switchTrack();
break;
default:
Serial.println("Tlačítko není mapováno.");
}
}
// CALLBACK – BT STAV
void bt_state_changed(esp_a2d_connection_state_t state, void *ptr) {
Serial.print("[i] BT stav: ");
Serial.println(state);
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
Serial.println("[*] Bluetooth připojeno!");
btConnected = true;
} else {
btConnected = false;
Serial.println("[!] Bluetooth odpojeno!");
}
}
// SETUP
void setup() {
Serial.begin(115200);
if (!LittleFS.begin(true)) {
Serial.println("Chyba: LittleFS nelze spustit");
while(true);
}
// nastavení prvního souboru
source.setAutoNext(false);
source.setFileFilter(MP3_FILE_NAME_1);
// audio metadata (nepovinné)
player.setMetadataCallback([](MetaDataType t, const char* s, int l){
Serial.print("META: ");
Serial.println(s);
});
// BLUETOOTH SETUP
a2dp_source.set_data_callback_in_frames(get_data_frames);
a2dp_source.set_on_connection_state_changed(bt_state_changed);
a2dp_source.set_avrc_passthru_command_callback(button_handler);
a2dp_source.set_volume(100);
a2dp_source.start(BT_SPEAKER_NAME);
}
// LOOP
void loop() {
if (btConnected && !playbackSTOP) {
if (!player.copy()) {
Serial.println("=== KONEC SKLADBY ===");
playbackSTOP = true;
}
}
delay(5);
}
Následující obrázek ukazuje výstup programu v okně Serial Monitoru.
Obrázek č. 4 – Výstup v okně Serial Monitoru
Závěr
V tomto článku jsme si krok za krokem ukázali, jak lze z modulu ESP32 vytvořit jednoduchý, ale docela funkční Bluetooth audio přehrávač, který dokáže přehrávat MP3 soubory uložené ve vlastní paměti a bezdrátově je odesílat do běžného BT reproduktoru.
Klíčovým prvkem celého řešení se stal kruhový buffer, který plní roli mezipaměti mezi MP3 dekodérem a Bluetooth stackem. Díky tomu není nutné dekódovat celý soubor dopředu a přehrávání může probíhat plynule, s minimálními nároky na paměť a bez zbytečné latence.
V další fázi jsme si ukázali, jak lze přehrávání ovládat nejprve pomocí tlačítka na vývojovém kitu a následně celý projekt posunout o úroveň výš využitím profilu AVRCP. Díky němu je možné reagovat na tlačítka přímo na Bluetooth reproduktoru (Play, Pause, Next, Previous) a udělat z ESP32 zařízení, které se chová jako skutečný Bluetooth audio přehrávač – bez nutnosti jakékoliv fyzické obsluhy na samotném modulu.
Výsledkem je univerzální architektura, kterou lze dále snadno rozšiřovat a přizpůsobovat konkrétním aplikacím.
Možností využití přehrávání MP3 souborů přes Bluetooth z ESP32 je překvapivě hodně, zejména tam, kde:
- nechceme řešit klasický audio zesilovač,
- chceme využít hotový BT reproduktor,
Namátkově mě nyní napadají takové tři možné projekty, které by tuto dovednost mohly využít:
- Mluvící hodiny nebo budík
ESP32 může v pravidelných intervalech přehrávat hlasová hlášení času, upozornění nebo alarmy, přičemž uživatel vše ovládá přímo tlačítky na reproduktoru. - Stopky, časovače a tréninkové intervaly
Například při sportovním tréninku může zařízení každých 15 nebo 60 sekund přehrát zvukovou informaci o čase, kole nebo změně režimu, aniž by bylo třeba sledovat displej. - Zvuková signalizace v chytré domácnosti
Oznámení událostí (otevřené dveře, alarm, stav systému) hlasem nebo zvukem přehrávaným do libovolného BT reproduktoru v místnosti.
Jak je vidět, kombinace ESP32 + MP3 + Bluetooth otevírá poměrně široké možnosti pro jednoduché (nejen) domácí projekty. A co je důležité – většina složité práce je již vyřešena knihovnami, takže se můžeš soustředit hlavně na vlastní logiku aplikace a nápady, jak ji využít.
A právě o tom bastlení má být! 🙂