ESP32: Como Reproduzir 4 Mensagens de Voz com o DAC Interno e Timer de Hardware

 

ESP32 reproduzindo quatro mensagens de voz com o DAC interno utilizando Timer de Hardware e quatro botões
Projeto com ESP32 reproduzindo quatro mensagens de voz ("Temperatura 30 graus", "Aguarde um momento", "Sensor garagem" e "Sensor porta") utilizando o DAC interno, Timer de Hardware a 16 kHz e quatro botões.

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.h

Nesse 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

Diagrama de ligação do ESP32 utilizando DAC interno (GPIO25), amplificador PAM8403, quatro sensores touch TTP223 e alto-falante de 4 Ω para reprodução de mensagens de voz.
Esquema completo de ligação do projeto ESP32 utilizando o DAC interno (GPIO25) para reproduzir quatro mensagens de voz. O áudio é amplificado pelo PAM8403 e reproduzido em um alto-falante de 4 Ω / 3 W. Os quatro sensores touch TTP223 acionam as mensagens armazenadas no arquivo audios.h, enquanto um Timer de Hardware garante uma taxa de amostragem estável de 16 kHz.


GitHub
Para facilitar o acompanhamento deste tutorial, você pode baixar todos os arquivos do projeto no GitHub.

https://github.com/RodRobot/ESP32-DAC-Timer-Hardware


/*****************************************************************************************
 * Projeto : ESP32 - Player de Voz com 4 Mensagens com Timer de Hardware - ESP32
 * Autor   : RodRobot
 * Site    : https://rodrobot.blogspot.com/p/projetos.html
 *
 * Descrição:
 * Este exemplo reproduz quatro áudios utilizando o DAC interno do ESP32.
 * As amostras são enviadas através de um Timer de Hardware configurado
 * para 16 kHz (16000 amostras por segundo), proporcionando um áudio
 * muito mais estável do que utilizando delayMicroseconds().
 *
 * Hardware:
 * - ESP32
 * - PAM8403
 * - Alto-falante 4 Ω 3W
 * - 4 Botões
 *
 * Ligações:
 * GPIO18 -> Botão 1
 * GPIO19 -> Botão 2
 * GPIO21 -> Botão 3
 * GPIO22 -> Botão 4
 * GPIO25 -> DAC
 *
 * Arquivo audios.h:
 * audio1, audio2, audio3, audio4
 * tamanhoAudio1, tamanhoAudio2, tamanhoAudio3, tamanhoAudio4
 *
 *****************************************************************************************/

#include <driver/dac.h>
#include "audios.h"

//==================== BOTÕES ====================

const int pinoBotao1 = 18;             // Botão Áudio 1
const int pinoBotao2 = 19;             // Botão Áudio 2
const int pinoBotao3 = 21;             // Botão Áudio 3
const int pinoBotao4 = 22;             // Botão Áudio 4

//==================== TIMER =====================

hw_timer_t *timer = NULL;              // Ponteiro para o Timer

volatile bool reproduzindo = false;    // Indica reprodução

volatile const unsigned char *audioAtual = NULL;   // Ponteiro do áudio
volatile uint32_t tamanhoAtual = 0;                // Tamanho do áudio
volatile uint32_t indice = 0;                      // Índice da amostra

//================================================

// Interrupção do Timer (16 kHz)

void IRAM_ATTR onTimer()
{
    if (!reproduzindo) return;                     // Não faz nada

    dac_output_voltage(DAC_CHANNEL_1, audioAtual[indice]); // Envia amostra

    indice++;                                     // Próxima amostra

    if (indice >= tamanhoAtual)
    {
        reproduzindo = false;                     // Finaliza áudio
        indice = 0;
        dac_output_voltage(DAC_CHANNEL_1, 0);     // Silencia DAC
    }
}

//================================================

void tocarAudio(const unsigned char *audio,
                uint32_t tamanho)
{
    if (reproduzindo) return;                     // Evita sobreposição

    audioAtual = audio;                           // Seleciona áudio
    tamanhoAtual = tamanho;                       // Define tamanho
    indice = 0;                                   // Reinicia índice
    reproduzindo = true;                          // Inicia reprodução
}

//================================================

void setup()
{
    pinMode(pinoBotao1, INPUT);                   // Botão 1
    pinMode(pinoBotao2, INPUT);                   // Botão 2
    pinMode(pinoBotao3, INPUT);                   // Botão 3
    pinMode(pinoBotao4, INPUT);                   // Botão 4

    dac_output_enable(DAC_CHANNEL_1);             // Habilita DAC

    timer = timerBegin(1000000);                  // Timer em 1 MHz

    timerAttachInterrupt(timer, &onTimer);        // Liga interrupção

    timerAlarm(timer, 62, true, 0);               // 62 µs ≈ 16 kHz

    timerStart(timer);                            // Inicia Timer
}

//================================================

void loop()
{
    if (digitalRead(pinoBotao1) == HIGH)
    {
        tocarAudio(audio1, tamanhoAudio1);        // Toca áudio 1
        delay(300);                               // Debounce
    }

    if (digitalRead(pinoBotao2) == HIGH)
    {
        tocarAudio(audio2, tamanhoAudio2);        // Toca áudio 2
        delay(300);                               // Debounce
    }

    if (digitalRead(pinoBotao3) == HIGH)
    {
        tocarAudio(audio3, tamanhoAudio3);        // Toca áudio 3
        delay(300);                               // Debounce
    }

    if (digitalRead(pinoBotao4) == HIGH)
    {
        tocarAudio(audio4, tamanhoAudio4);        // Toca áudio 4
        delay(300);                               // Debounce
    }
}


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)

https://www.narakeet.com/

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

Postagens mais visitadas deste blog

ESP-NOW com ESP32-C3 e ESP32: Comunicação Sem Wi-Fi na Prática

ESP32 + RoboEyes: Como Criar Olhos Animados para seu Robô com Display OLED

ESP32-C3 Super Mini: Características, Vantagens, Pinagem, Matter.