Índice:
Introdução
Você já imaginou construir um rádio capaz de reproduzir emissoras do mundo inteiro utilizando apenas um ESP32, um display OLED e uma conexão Wi-Fi?
Neste projeto desenvolvemos um Rádio Web inteligente, capaz de reproduzir transmissões MP3 diretamente da Internet. Além da reprodução de áudio, o sistema exibe no display OLED o nome da estação, o título da música em reprodução e a intensidade do sinal Wi-Fi.
Para tornar a experiência ainda mais agradável, adicionaremos um botão físico que permite alternar rapidamente entre várias emissoras previamente cadastradas.
Ao final deste tutorial você terá um equipamento totalmente funcional e aprenderá diversos conceitos importantes de programação embarcada, comunicação Wi-Fi, áudio digital via I2S e desenvolvimento de interfaces gráficas.
Como funciona o projeto?
O funcionamento é relativamente simples, mas envolve diversas tecnologias trabalhando em conjunto.
- O ESP32 conecta-se à rede Wi-Fi.
- É estabelecida uma conexão com o servidor da rádio online.
- O áudio MP3 é recebido continuamente pela Internet.
- A biblioteca AudioTools realiza a decodificação do fluxo MP3.
- O áudio PCM é enviado pela interface I2S.
- O módulo MAX98357A converte o áudio digital em sinal analógico amplificado.
- O alto-falante reproduz o som.
- O display OLED exibe informações da estação.
- Quando o botão é pressionado, o ESP32 encerra a conexão atual e inicia automaticamente a próxima emissora.
Componentes utilizados
- ESP32 DevKit V1
- Display OLED SSD1306 128×64 (I²C)
- MAX98357A (DAC + Amplificador I2S)
- Alto-falante 4 Ω ou 8 Ω
- Botão de pressão
- Jumpers
- Cabo USB
Bibliotecas utilizadas
Instale as seguintes bibliotecas na Arduino IDE:
- WiFi (ESP32)
- Wire
- Adafruit GFX Library
- Adafruit SSD1306
- ESP8266Audio (compatível com ESP32)
A biblioteca ESP8266Audio fornece as classes utilizadas no projeto:
- AudioGeneratorMP3
- AudioOutputI2S
- AudioFileSourceICYStream
- AudioFileSourceBuffer
Ela é responsável por realizar toda a reprodução do streaming MP3.
Ligações do circuito
Display OLED SSD1306
| OLED | ESP32 |
| VCC | 3.3 V |
| GND | GND |
| SDA | GPIO 21 |
| SCL | GPIO 22 |
MAX98357A
| MAX98357A | ESP32 |
| VIN | 5 V |
| GND | GND |
| BCLK | GPIO 18 |
| LRC | GPIO 19 |
| DIN | GPIO 23 |
Conecte o alto-falante diretamente nas saídas SPK+ e SPK− do módulo.
Botão
| Botão | ESP32 |
| Terminal 1 | GPIO 5 |
| Terminal 2 | GND |
O código utiliza o resistor INPUT_PULLUP, portanto não é necessário resistor externo.
Configurando o Wi-Fi
Antes de gravar o programa altere as seguintes linhas:
const char* ssid = “SeuWiFi”;
const char* password = “SuaSenha”;
Após ligar o ESP32 ele conecta automaticamente à rede.
Configurando as rádios
Uma das partes mais interessantes do projeto é a lista de emissoras.
Todas elas são armazenadas em uma estrutura:
struct Estacao{
const char* nome;
const char* url;
};
Cada rádio possui apenas dois parâmetros:
- Nome
- URL do streaming MP3
Exemplo:
{
“Groove Salad”,
“http://ice2.somafm.com/groovesalad-128-mp3”
}
Para adicionar novas rádios basta incluir novos itens no vetor radios[].
O programa calcula automaticamente quantas emissoras existem.
Inicialização do display
Logo após ligar o equipamento, o OLED apresenta mensagens indicando cada etapa da inicialização:
- Inicializando sistema
- Conectado ao Wi-Fi
- Wi-Fi conectado
- Iniciando rádio
Depois disso é exibida a interface principal.
Interface gráfica
A tela principal mostra:
- Nome do projeto (SARA RÁDIO)
- Número da estação atual
- Nome da rádio
- Título da música (quando disponível)
- Status da reprodução
- Intensidade do sinal Wi-Fi em dBm
Sempre que uma nova música começa, o servidor envia metadados ICY e o display é atualizado automaticamente.
Reprodução de áudio
Após conectar-se ao servidor da rádio, o ESP32 cria três objetos principais:
- Stream HTTP
- Buffer de recepção
- Decodificador MP3
O áudio decodificado é enviado continuamente ao MAX98357A através da interface I2S.
O ganho do amplificador é configurado por software:
saída->SetGain(0.20);
Esse valor pode variar de 0.0 até 1.0.
Troca de emissoras
O botão conectado ao GPIO 5 permite navegar entre as rádios cadastradas.
Sempre que o botão é pressionado:
- O streaming atual é encerrado.
- Os buffers são liberados da memória.
- O índice da estação é incrementado.
- O display é atualizado.
- A nova rádio começa a tocar.
Foi implementado um debounce de 250 ms, evitando múltiplas leituras provocadas pelo contato mecânico do botão.
Reconexão automática
Um grande diferencial deste projeto é a robustez do software.
Caso a rádio pare de transmitir ou a conexão seja interrompida, o ESP32 tenta reconectar automaticamente após alguns segundos.
Se a rede Wi-Fi cair, o sistema também realiza uma nova tentativa de conexão sem necessidade de reinicialização.
Isso torna o rádio muito mais confiável para uso contínuo.
Atualização das informações da música
Muitas rádios online enviam metadados contendo o nome da música e do artista.
Sempre que essas informações chegam ao ESP32, a função de callback atualiza automaticamente o display, exibindo a faixa que está sendo reproduzida naquele momento.
Fluxograma de funcionamento
Liga o ESP32
│
▼
Inicializa OLED
│
▼
Conecta ao Wi-Fi
│
▼
Conecta à rádio
│
▼
Recebe Streaming MP3
│
▼
Decodifica o áudio
│
▼
Envia áudio via I2S
│
▼
MAX98357A
│
▼
Alto-falante
Em paralelo, o programa monitora o botão, atualiza o display e verifica continuamente o estado da conexão.
O que você aprende neste projeto?
Este projeto reúne diversos conceitos fundamentais para quem deseja evoluir na área de sistemas embarcados:
- Programação com ESP32
- Comunicação Wi-Fi
- Consumo de APIs e streaming de dados
- Reprodução de áudio MP3
- Comunicação I2S
- Utilização do MAX98357A
- Controle de displays OLED
- Manipulação de botões com debounce
- Tratamento de eventos e callbacks
- Desenvolvimento de interfaces gráficas
- Gerenciamento de memória dinâmica
- Reconexão automática de serviços
Possíveis melhorias
Depois de concluir este projeto, você pode implementar novas funcionalidades, como:
- Controle de volume por encoder rotativo
- Equalizador gráfico no display
- Exibição do logotipo da emissora
- Relógio sincronizado pela Internet (NTP)
- Previsão do tempo
- Lista de favoritos
- Controle por aplicativo Bluetooth
- Reprodução de arquivos MP3 do cartão microSD
- Caixa acústica impressa em 3D
- Interface touchscreen
Código-fonte completo
O código completo deste projeto está disponível abaixo e implementa todas as funcionalidades apresentadas neste tutorial, incluindo conexão Wi-Fi, reprodução de streaming MP3, exibição das informações da estação no display OLED, troca de rádios por botão e mecanismos automáticos de reconexão.
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <AudioFileSourceICYStream.h>
#include <AudioFileSourceBuffer.h>
#include <AudioGeneratorMP3.h>
#include <AudioOutputI2S.h>
// =====================================================
// WIFI
// =====================================================
const char* ssid = "SEU_SSID";
const char* password = "SUA_SENHA";
// =====================================================
// OLED
// =====================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDRESS 0x3C
#define OLED_SDA 21
#define OLED_SCL 22
Adafruit_SSD1306 display(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&Wire,
OLED_RESET
);
// =====================================================
// MAX98357A
// =====================================================
#define I2S_BCLK 18
#define I2S_LRC 19
#define I2S_DOUT 23
// =====================================================
// BOTÃO
// =====================================================
#define BTN_PROXIMA 5
bool estadoAnteriorBotao = HIGH;
unsigned long ultimoDebounce = 0;
const unsigned long TEMPO_DEBOUNCE = 250;
// =====================================================
// LISTA DE RÁDIOS
// =====================================================
struct Estacao {
const char* nome;
const char* url;
};
Estacao radios[] = {
{
"Groove Salad",
"http://ice2.somafm.com/groovesalad-128-mp3"
},
{
"Left Coast 70s",
"http://ice2.somafm.com/seventies-128-mp3"
},
{
"Drone Zone",
"http://ice2.somafm.com/dronezone-128-mp3"
},
{
"Secret Agent",
"http://ice2.somafm.com/secretagent-128-mp3"
}
};
const int TOTAL_RADIOS =
sizeof(radios) / sizeof(radios[0]);
int radioAtual = 0;
// =====================================================
// OBJETOS DE ÁUDIO
// =====================================================
AudioGeneratorMP3* mp3 = nullptr;
AudioFileSourceICYStream* stream = nullptr;
AudioFileSourceBuffer* buffer = nullptr;
AudioOutputI2S* saida = nullptr;
const int TAMANHO_BUFFER = 16 * 1024;
// =====================================================
// ESTADO DO SISTEMA
// =====================================================
String tituloMusica = "";
String estadoRadio = "Inicializando";
bool radioAtiva = false;
unsigned long ultimaAtualizacaoTela = 0;
unsigned long ultimaTentativaRadio = 0;
// =====================================================
// FUNÇÕES DO DISPLAY
// =====================================================
String cortarTexto(String texto, int maximo) {
texto.trim();
if (texto.length() <= maximo) {
return texto;
}
return texto.substring(0, maximo - 3) + "...";
}
void mostrarMensagem(
String linha1,
String linha2
) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
display.setTextSize(1);
display.setCursor(28, 0);
display.println("SARA RADIO");
display.drawLine(
0,
12,
127,
12,
SSD1306_WHITE
);
display.setCursor(0, 22);
display.println(cortarTexto(linha1, 21));
display.setCursor(0, 40);
display.println(cortarTexto(linha2, 21));
display.display();
}
void mostrarTelaRadio() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
display.setTextSize(1);
// Cabeçalho
display.setCursor(28, 0);
display.println("SARA RADIO");
display.drawLine(
0,
12,
127,
12,
SSD1306_WHITE
);
// Número e nome da rádio
display.setCursor(0, 17);
display.print(radioAtual + 1);
display.print("/");
display.print(TOTAL_RADIOS);
display.print(" ");
display.println(
cortarTexto(radios[radioAtual].nome, 16)
);
// Música ou estado
display.setCursor(0, 32);
if (tituloMusica.length() > 0) {
display.println(
cortarTexto(tituloMusica, 21)
);
} else {
display.println(
cortarTexto(estadoRadio, 21)
);
}
// Wi-Fi
display.setCursor(0, 49);
if (WiFi.status() == WL_CONNECTED) {
display.print("WiFi ");
display.print(WiFi.RSSI());
display.print(" dBm");
} else {
display.print("WiFi desconectado");
}
display.display();
}
// =====================================================
// CALLBACKS DO STREAM
// =====================================================
void callbackStatus(
void* dados,
int codigo,
const char* mensagem
) {
(void)dados;
Serial.print("STATUS ");
Serial.print(codigo);
Serial.print(": ");
Serial.println(mensagem);
}
void callbackMetadados(
void* dados,
const char* tipo,
bool unicode,
const char* valor
) {
(void)dados;
(void)unicode;
Serial.print("METADADO ");
Serial.print(tipo);
Serial.print(": ");
Serial.println(valor);
String tipoTexto = String(tipo);
String valorTexto = String(valor);
if (
tipoTexto.equalsIgnoreCase("StreamTitle") ||
tipoTexto.indexOf("Title") >= 0
) {
tituloMusica = valorTexto;
tituloMusica.trim();
mostrarTelaRadio();
}
}
// =====================================================
// CONEXÃO WI-FI
// =====================================================
bool conectarWiFi() {
mostrarMensagem(
"Conectando WiFi",
"Aguarde..."
);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Conectando ao WiFi");
unsigned long inicio = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (millis() - inicio > 30000) {
Serial.println();
Serial.println("Falha ao conectar ao WiFi.");
mostrarMensagem(
"Erro no WiFi",
"Verifique a rede"
);
return false;
}
}
Serial.println();
Serial.println("WiFi conectado!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
mostrarMensagem(
"WiFi conectado",
WiFi.localIP().toString()
);
delay(1500);
return true;
}
// =====================================================
// PARAR A RÁDIO ATUAL
// =====================================================
void pararRadio() {
radioAtiva = false;
if (mp3 != nullptr) {
if (mp3->isRunning()) {
mp3->stop();
}
delete mp3;
mp3 = nullptr;
}
if (buffer != nullptr) {
buffer->close();
delete buffer;
buffer = nullptr;
}
if (stream != nullptr) {
stream->close();
delete stream;
stream = nullptr;
}
if (saida != nullptr) {
delete saida;
saida = nullptr;
}
delay(100);
}
// =====================================================
// INICIAR RÁDIO
// =====================================================
bool iniciarRadio() {
pararRadio();
tituloMusica = "";
estadoRadio = "Conectando...";
mostrarTelaRadio();
Serial.println();
Serial.println("========================");
Serial.print("Radio: ");
Serial.println(radios[radioAtual].nome);
Serial.print("URL: ");
Serial.println(radios[radioAtual].url);
stream = new AudioFileSourceICYStream(
radios[radioAtual].url
);
if (stream == nullptr) {
estadoRadio = "Erro no stream";
mostrarTelaRadio();
return false;
}
stream->RegisterMetadataCB(
callbackMetadados,
nullptr
);
stream->RegisterStatusCB(
callbackStatus,
nullptr
);
buffer = new AudioFileSourceBuffer(
stream,
TAMANHO_BUFFER
);
if (buffer == nullptr) {
estadoRadio = "Erro no buffer";
pararRadio();
mostrarTelaRadio();
return false;
}
buffer->RegisterStatusCB(
callbackStatus,
nullptr
);
saida = new AudioOutputI2S();
if (saida == nullptr) {
estadoRadio = "Erro no audio";
pararRadio();
mostrarTelaRadio();
return false;
}
saida->SetPinout(
I2S_BCLK,
I2S_LRC,
I2S_DOUT
);
// Volume de 0.0 a 1.0
saida->SetGain(0.20);
mp3 = new AudioGeneratorMP3();
if (mp3 == nullptr) {
estadoRadio = "Erro no MP3";
pararRadio();
mostrarTelaRadio();
return false;
}
mp3->RegisterStatusCB(
callbackStatus,
nullptr
);
if (!mp3->begin(buffer, saida)) {
estadoRadio = "Falha na radio";
pararRadio();
mostrarTelaRadio();
return false;
}
radioAtiva = true;
estadoRadio = "TOCANDO";
mostrarTelaRadio();
Serial.println("Radio iniciada!");
return true;
}
// =====================================================
// PRÓXIMA RÁDIO
// =====================================================
void proximaRadio() {
radioAtual++;
if (radioAtual >= TOTAL_RADIOS) {
radioAtual = 0;
}
Serial.println();
Serial.println("Botao pressionado.");
Serial.println("Mudando para a proxima radio.");
iniciarRadio();
}
// =====================================================
// LEITURA DO BOTÃO
// =====================================================
void verificarBotao() {
bool leituraAtual = digitalRead(BTN_PROXIMA);
if (
leituraAtual == LOW &&
estadoAnteriorBotao == HIGH &&
millis() - ultimoDebounce >= TEMPO_DEBOUNCE
) {
ultimoDebounce = millis();
proximaRadio();
}
estadoAnteriorBotao = leituraAtual;
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
delay(1500);
pinMode(BTN_PROXIMA, INPUT_PULLUP);
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(
SSD1306_SWITCHCAPVCC,
OLED_ADDRESS
)) {
Serial.println("OLED nao encontrado.");
while (true) {
delay(1000);
}
}
mostrarMensagem(
"Inicializando",
"sistema..."
);
delay(1000);
if (!conectarWiFi()) {
while (true) {
delay(1000);
}
}
iniciarRadio();
}
// =====================================================
// LOOP
// =====================================================
void loop() {
verificarBotao();
if (
mp3 != nullptr &&
mp3->isRunning()
) {
if (!mp3->loop()) {
Serial.println("Stream interrompido.");
mp3->stop();
radioAtiva = false;
estadoRadio = "Desconectado";
mostrarTelaRadio();
}
}
// Tenta reconectar a estação atual
if (
!radioAtiva &&
millis() - ultimaTentativaRadio >= 5000
) {
ultimaTentativaRadio = millis();
if (WiFi.status() == WL_CONNECTED) {
iniciarRadio();
}
}
// Atualiza o sinal do Wi-Fi
if (
millis() - ultimaAtualizacaoTela >= 5000
) {
ultimaAtualizacaoTela = millis();
mostrarTelaRadio();
}
// Reconecta o Wi-Fi caso caia
if (WiFi.status() != WL_CONNECTED) {
estadoRadio = "Reconectando WiFi";
mostrarTelaRadio();
WiFi.disconnect();
WiFi.begin(ssid, password);
unsigned long inicio = millis();
while (
WiFi.status() != WL_CONNECTED &&
millis() - inicio < 15000
) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
iniciarRadio();
}
}
delay(1);
}
Conclusão
Neste projeto desenvolvemos um rádio web completo utilizando ESP32, explorando recursos modernos como streaming de áudio via Internet, comunicação I²S, exibição de informações em um display OLED e seleção de múltiplas emissoras por meio de um botão físico.
Além de ser um excelente exercício para quem está aprendendo eletrônica embarcada, este projeto demonstra como um único microcontrolador pode integrar conectividade Wi-Fi, processamento de áudio, interface gráfica e interação com o usuário em um equipamento compacto e funcional.
Com pequenas adaptações, essa mesma arquitetura pode ser utilizada para criar caixas de som inteligentes, sistemas de automação residencial, players multimídia e diversos outros dispositivos conectados.
Saiba mais sobre a parceria Saravati e Sara Educação
Este “Guia de Montagem” é uma colaboração especial entre a Saravati e a Sara Educação, criado pelo Professor Felipe Rosa. Nosso objetivo é enriquecer a comunidade de entusiastas da eletrônica, IoT e automação com recursos educacionais de alta qualidade. Através dessa parceria, buscamos inspirar e capacitar criadores em seus projetos, disponibilizando esses guias em nosso blog e nas redes sociais.
