ESP32: Como Reproduzir 4 Mensagens de Voz com o DAC Interno e Timer de Hardware
ESP32: Reproduzindo Áudio com Timer de Hardware e Arquivo audios.h
Quando começamos a reproduzir áudio no ESP32, normalmente utilizamos um laço for juntamente com a função delayMicroseconds() para enviar cada amostra ao DAC interno. Essa abordagem funciona muito bem para projetos simples e ajuda a entender como o DAC converte valores digitais em sinais analógicos.
Porém, existe uma maneira mais eficiente e profissional de fazer isso: utilizando um Timer de Hardware.
Neste artigo você vai entender por que utilizamos um Timer de Hardware, como ele funciona e qual a vantagem de armazenar os áudios em um arquivo separado chamado audios.h.
O que é um Timer de Hardware?
O ESP32 possui temporizadores internos (Hardware Timers) capazes de gerar interrupções em intervalos extremamente precisos.
Ao contrário da função delayMicroseconds(), que faz o processador ficar esperando para executar a próxima instrução, o Timer funciona de forma independente da CPU.
Isso significa que ele pode avisar o ESP32 exatamente quando é o momento de enviar a próxima amostra de áudio.
Em outras palavras:
- o Timer controla o tempo;
- o processador continua livre para executar outras tarefas.
Essa é exatamente a técnica utilizada em diversos equipamentos eletrônicos que reproduzem áudio.
Como funciona na prática?
Imagine que seu áudio foi convertido para:
- 8 bits
- Mono
- 16.000 Hz
Isso significa que o DAC precisa receber exatamente:
16.000 amostras por segundo.
Ou seja,
- uma amostra,
- depois outra,
- depois outra...
sempre em intervalos extremamente precisos.
O Timer é responsável por esse controle.
Sempre que ele dispara uma interrupção, o ESP32 envia apenas uma única amostra para o DAC.
Depois aguarda o próximo disparo do Timer.
Esse processo acontece milhares de vezes por segundo.
O que acontece sem o Timer?
Uma maneira simples seria utilizar:
for (uint32_t i = 0; i < tamanhoAudio; i++)
{
dac_output_voltage(DAC_CHANNEL_1, audio[i]);
delayMicroseconds(62);
}Esse código funciona.
Porém existem algumas limitações.
Durante esse laço, praticamente toda a atenção do processador fica voltada para a reprodução do áudio.
Se houver outras tarefas importantes acontecendo, pequenas variações de tempo podem ocorrer.
Em projetos simples isso geralmente não é um problema.
Mas em aplicações maiores, utilizar um Timer de Hardware é uma solução muito mais robusta.
Como funciona com o Timer?
Quando utilizamos um Timer de Hardware, a lógica muda completamente.
O programa principal apenas informa:
"Quero reproduzir este áudio."
Quem passa a controlar a velocidade é o próprio Timer.
A cada interrupção ele executa algo semelhante a:
dac_output_voltage(DAC_CHANNEL_1, audioAtual[indice]);
indice++;Quando chega ao final do vetor:
if (indice >= tamanhoAtual)
{
reproduzindo = false;
}O áudio termina automaticamente.
Enquanto isso, o restante do programa continua funcionando normalmente.
Vantagens do Timer de Hardware
Utilizar um Timer oferece diversas vantagens.
Precisão
A taxa de amostragem permanece constante.
No nosso projeto utilizamos:
16.000 amostras por segundo (16 kHz).
Melhor qualidade
Como todas as amostras são enviadas exatamente no instante correto, o áudio fica mais estável.
Processador livre
Enquanto o Timer envia as amostras, o programa principal pode:
- ler sensores;
- atualizar um display OLED;
- comunicar via Wi-Fi;
- utilizar ESP-NOW;
- executar outras tarefas.
Código mais organizado
O programa principal fica responsável apenas por decidir:
"Qual áudio deve tocar?"
Todo o restante é controlado pelo Timer.
O arquivo audios.h
Outro detalhe importante deste projeto é a utilização de um arquivo separado chamado:
audios.hNesse arquivo ficam armazenados todos os vetores de áudio.
Por exemplo:
const unsigned char audio1[] = {
0x80,
0x82,
0x85,
...
};
const uint32_t tamanhoAudio1 = sizeof(audio1);Depois:
const unsigned char audio2[] = {
...
};
const uint32_t tamanhoAudio2 = sizeof(audio2);E assim sucessivamente.
Código completo
Por que separar os áudios do programa principal?
Essa é uma prática muito utilizada em projetos maiores.
Imagine colocar milhares de linhas de dados diretamente dentro do arquivo principal.
O código ficaria enorme.
Muito mais difícil de entender.
Separando tudo em um arquivo próprio, o programa fica muito mais organizado.
Código arquivo audios.h
#ifndef AUDIOS_H#define AUDIOS_H
//========================================================// ÁUDIO 1// Cole aqui os bytes do primeiro áudio//========================================================const unsigned char audio1[] = { // ... cole o restante do áudio 1 ...};
const uint32_t tamanhoAudio1 = sizeof(audio1);
//========================================================// ÁUDIO 2// Cole aqui os bytes do segundo áudio//========================================================const unsigned char audio2[] = { // ... cole o restante do áudio 2 ...};
const uint32_t tamanhoAudio2 = sizeof(audio2);
//========================================================// ÁUDIO 3// Cole aqui os bytes do terceiro áudio//========================================================const unsigned char audio3[] = { // ... cole o restante do áudio 3 ...};
const uint32_t tamanhoAudio3 = sizeof(audio3);
//========================================================// ÁUDIO 4// Cole aqui os bytes do quarto áudio//========================================================const unsigned char audio4[] = { // ... cole o restante do áudio 4 ...};
const uint32_t tamanhoAudio4 = sizeof(audio4);
#endif
Outras vantagens
Mais fácil de localizar
Todos os áudios ficam concentrados em um único lugar.
Código principal menor
O arquivo .ino passa a conter apenas a lógica do programa.
Mais fácil de substituir mensagens
Se desejar trocar apenas um áudio, basta editar o arquivo audios.h.
Não é necessário alterar o restante do código.
Organização
Projetos profissionais costumam separar:
- bibliotecas;
- imagens;
- fontes;
- sons;
- configurações.
Fazer isso desde os primeiros projetos cria um bom hábito de programação.
Como o programa principal utiliza o arquivo audios.h?
Logo no início do código encontramos:
#include "audios.h"Essa linha informa ao compilador que todos os vetores definidos em audios.h estarão disponíveis no programa.
Assim podemos utilizar:
tocarAudio(audio1, tamanhoAudio1);ou
tocarAudio(audio2, tamanhoAudio2);Observe que o programa principal não precisa conhecer os dados do áudio.
Ele apenas informa qual vetor será reproduzido.
Quem realmente envia cada amostra para o DAC é o Timer de Hardware.
Ferramentas utilizadas
Gerar a voz (Narakeet)
Converter MP3 para WAV
https://www.online-convert.com/pt
Converter WAV para Array (FileToCArray)
https://notisrac.github.io/FileToCArray/
Código-fonte no GitHub
https://github.com/RodRobot/ESP32-DAC-Timer-Hardware
Vídeo completo no YouTube:
https://www.youtube.com/watch?v=mTrsVereq1o
Conclusão
Neste projeto vimos duas técnicas importantes para reprodução de áudio no ESP32.
A primeira foi utilizar um Timer de Hardware, responsável por enviar cada amostra ao DAC com uma frequência precisa de 16 kHz, garantindo uma reprodução mais estável e liberando o processador para outras tarefas.
A segunda foi organizar os arquivos de áudio no audios.h, mantendo o código principal limpo, organizado e muito mais fácil de manter.
Essa estrutura é utilizada em diversos projetos profissionais e serve como uma excelente base para evoluir para aplicações mais avançadas.
No próximo passo, podemos utilizar exatamente os mesmos arquivos de áudio com um DAC externo I2S, como o UDA1334A, obtendo uma qualidade sonora ainda melhor sem alterar a forma como os áudios são armazenados.
🔧 Acompanhe o RodRobot
Projetos maker com ESP32, Arduino, eletrônica e programação.
RodRobot – Projetos maker, eletrônica e programação na prática.


Comentários
Postar um comentário