Projetos

Tocando Músicas no Arduino com dois Tones ao Mesmo Tempo

Eletrogate 5 de setembro de 2024

Introdução

Neste artigo nós vamos criar um projeto capaz de tocar uma música composta por uma melodia principal e um acompanhamento. Diferente de muitos exemplos que apenas tocam músicas simples com uma única melodia, aqui nós vamos tocar duas notas musicais ao mesmo tempo.

Nós vamos criar também uma versão modificada do exemplo ToneMelody do Arduino, que passará a utilizar o comando Millis, ao invés do comando Delay, para determinar o momento exato em que cada uma das notas musicais deverá ser tocada de forma independente.


Melodia Principal e Acompanhamento

Aqui em nosso projeto o sinal de áudio que será enviado para o mini alto-falante / fone de ouvido vai ser composto pela junção de dois sinais distintos: A Melodia Principal e o Acompanhamento:

1. Melodia principal
uma nota de cada vez sendo tocada pelo exemplo toneMelody, usando apenas o comando Tone.

2. Acompanhamento
uma nota de cada vez sendo tocada pelo exemplo toneMelody, usando apenas o comando Tone.

3. Música final  =  ( melodia principal + acompanhamento )
duas notas ao mesmo tempo sendo tocadas pela nova versão do exemplo toneMelody, usando o comando Tone e o novo comando Tone1.

Materiais Necessários:
Para este tutorial nós vamos disponibilizar duas montagens diferentes: uma para conectar a um fone de ouvido/headphone, e outra para conectar a um mini alto-falante.

Atenção: Não há necessidade de construir as duas versões, você pode escolher e montar a que melhor lhe atender.


Montagem 1 - Materiais Necessários

Esta primeira montagem utiliza componentes que permitem conectar a saída de áudio a um fone de ouvido ou um headphone.

ATENÇÃO:
Aqui indicamos um modelo de potenciômetro Linear por ser mais fácil de encontrar, mas lembramos que, para ajuste de volume, o ideal é utilizar um potenciômetro Logarítmico (A50K).

Indicamos também a utilização de resistores de 1KΩ, no entanto, podem ser utilizados quaisquer valores entre 1KΩ e 10KΩ, lembrando que quanto maior o valor do resistor menor será o volume.


Montagem 1 - Hardware

A placa Arduino envia os sinais digitais das portas 8 e 9 através de dois resistores para reduzir a intensidade da corrente elétrica pois tanto o fone de ouvido quanto o headphone precisam de uma intensidade de corrente muito baixa para funcionar.
Em uma de suas pontas estes dois resistores se unem e enviam este sinal para o potenciômetro para que o usuário possa regular a intensidade do volume.

Resistores e Potenciômetro

O sinal que sai logo após o ponto de união dos dois resistores já fornece ao fone de ouvido o que consideramos ser o volume máximo adequado. Utilizamos então o potenciômetro para que o usuário possa realizar uma redução deste volume máximo a um nível intermediário ou baixo, conforme a sua necessidade.

Módulo adaptador

O sinal já regulado é então enviado ao módulo adaptador, passando por mais um resistor (opcional), enviando o mesmo sinal de áudio para os pinos que correspondem aos speakers esquerdo e direito do fone de ouvido/headphone.


Montagem 2 - Materiais Necessários

Esta segunda montagem é mais simples e utiliza componentes que permitem conectar a saída de áudio a um mini Alto-Falante.

ATENÇÃO:
Aqui indicamos resistores de 220 Ω, mas podem ser utilizados resistores de valores próximos a este. Lembrando que quanto maior o valor do resistor, menor o volume e vice versa.
Procurando sempre utilizar valores acima de 200 Ω para manter a intensidade da corrente elétrica dentro de níveis seguros para a placa Arduino.


Montagem 2 - Hardware

Conectar o mini alto-falante diretamente às portas digitais da placa pode causar uma intensidade de corrente elétrica maior do que o Arduino pode suportar, portanto, devemos reduzir um pouco a intensidade da corrente elétrica para que a mesma fique dentro dos limites permitidos.
Em uma das pontas dos dois resistores eles se unem e enviam este sinal para o mini Alto-Falante.

O sinal que sai logo após o ponto de união dos dois resistores já fornece ao mini Alto-Falante o que consideramos ser o volume máximo adequado.

Mini Alto-Falante

É importante frisar que o mini Alto-Falante deve ser de um modelo com especificações próximas de 8Ω/0.5W  ou de  8Ω/0.3W.  Alto-Falantes mais potentes exigem uma maior intensidade de corrente e podem não funcionar ou até mesmo forçar a placa Arduino.

Alto-Falante e Caixa de Som

Para enviar o sinal para Alto-falantes mais potentes ou caixa de som é necessário acoplar um módulo extra para amplificação de sinais ou então realizar a primeira montagem e utilizar um cabo P2 x P2 que conecte o módulo adaptador do nosso circuito à entrada auxiliar da caixa de som.


Funcionamento

O fornecimento de energia do nosso projeto deve ser de 5V.
Você pode conectar a placa Arduino UNO a um cabo USB e ligar este cabo à porta USB do computador ou a um carregador de celular.

Vamos dividir a explicação em 4 exemplos ou etapas:

Primeira Etapa

Vamos explicar sobre a fórmula da frequência e período e criar um exemplo demonstrando, da forma mais simples e básica possível, como gerar um sinal de áudio.

Segunda Etapa

Vamos explicar o básico sobre a configuração do Timer-1 e criar um exemplo demonstrando como realizar a geração de áudio através dele, com uma precisão bem maior.

Terceira Etapa

Vamos modularizar o funcionamento da segunda etapa, deixando todo o código em um arquivo .h separado. Neste novo arquivo iremos criar o comando Tone-1 para que possamos chamá-lo da mesma forma que fazemos com o comando Tone normal, passando os mesmos parâmetros.

Quarta Etapa

Vamos criar uma versão totalmente nova e modificada do já conhecido exemplo “ToneMelody” que se encontra nos exemplos da Arduino IDE.
Esta nova versão do ToneMelody passará a usar o sistema de temporização por comando Millis, ao invés de Delay, para calcular o tempo exato em que cada uma das notas musicais, da melodia principal e do acompanhamento, devem ser executadas em perfeita sincronia.


Frequência e Período

Frequência e período são dois conceitos da física que estão relacionados entre si.
O período é o tempo necessário para que ocorra uma repetição, e a frequência é a quantidade de vezes que um determinado evento  ocorre a cada unidade de tempo.

Na relação entre frequência e período temos que:
f = 1 / T  ,  logo    T = 1 / f  ,  o que nos leva a   periodo = 1(segundos) / frequência(Hertz)

Desta forma, se quisermos que um alto-falante gere uma frequência sonora de 440Hz, nós precisamos calcular:
periodo = 1 / 440 = 0.0022727272 segundos

Este é o tempo que corresponde ao ciclo completo em que o cone do alto-falante vai do estado de retraído ao estado expandido. É por isso que ainda precisamos calcular o valor do meio período:
meio periodo = periodo / 2 = 0.0011363636 segundos

Agora que temos o valor da metade do período nós só precisamos fazer o cone do alto-falante ficar contraído por meio período e depois ficar expandido por mais meio período. E repetindo este processo constantemente nós vamos obter uma frequência sonora de 440Hz.

Este processo será demonstrado no código a seguir.


Primeira Etapa - áudio básico com Delay

Abaixo temos o código do arquivo correspondente à primeira etapa.
Você pode fazer o download deste código clicando no link: codigo_primeira_etapa.zip

Este primeiro código exemplifica como podemos fazer a geração de áudio utilizando os comandos digitalWrite e delayMicroseconds.

Arquivo “primeira_etapa.ino”
 
int audioPin = 8;

float meioPeriodo_us;

void setup()
{
  pinMode(audioPin, OUTPUT);

  int freq = 440;
  
  float periodo = 1.0/freq;         // valor em segundos
  float meioPeriodo = periodo/2.0;  // valor em segundos

  meioPeriodo_us = meioPeriodo * 1000000; // valor em microssegundos
}

void loop()
{
  // Coloca pino em HIGH e aguarda o tempo de meioPeriodo_us
  digitalWrite( audioPin, HIGH );
  delayMicroseconds( meioPeriodo_us );
  
  // Coloca pino em LOW e aguarda o tempo de meioPeriodo_us
  digitalWrite( audioPin, LOW );
  delayMicroseconds( meioPeriodo_us );
}
 

Começamos declarando duas variáveis globais: audioPin e meioPriodo_us.
Já deixamos a variável audioPin definida como 8 para indicar que o nosso programa vai usar a porta digital 8 para o envio do sinal de áudio.

Na função SETUP configuramos a porta de saída de áudio como sendo do tipo OUTPUT e criamos uma variável freq = 440 para determinar que o valor da frequência de áudio será de 440Hz.

Para calcular o período dividimos 1 pelo valor da frequência, então fazemos período = 1.0/freq.
Como o período corresponde a todo o percurso da onda sonora, desde seu nível mais baixo até o mais alto, neste caso, deixando o alto-falante desligado e depois ligado, então precisamos calcular o valor que corresponde à metade do período, para que fique metade do tempo desligado e metade ligado, constituindo assim um período completo, portanto, fazemos meioPeriodo = periodo/2.0.

Dentro do método LOOP nós vamos utilizar o comando delayMicroseconds, e precisamos passar um valor convertido para microssegundos, portanto precisamos converter o valor da variável meioPeriodo, que está em segundos, para um valor em microssegundos, por isso fazemos meioPeriodo_us = periodo * 1000000;
Agora, dentro do método LOOP, nós ativamos o sinal na porta de saída de áudio e aguardamos um tempo de meioPeriodo_us, logo em seguida nós desativamos o sinal na porta e aguardamos novamente o mesmo tempo de meioPeriodo_us.

E como este código que ativa e desativa o sinal da porta está dentro do método LOOP este processo irá se repetir constantemente gerando, assim, um áudio na frequência de 440Hz.

Observações:

O código é muito simples de programar e de entender como funciona, porém, existem dois pontos muito importantes que precisamos ressaltar:
primeiro: como utiliza o comando delayMicroseconds este gerador sonoro não serve para ser incluído dentro de um projeto que também precise executar outros processos.
segundo: o código que se repete constantemente está dentro do método LOOP e os comandos digitalWrite e delayMicroseconds têm um custo de execução, o que causa um pequeno atraso extra e faz com que o áudio emitido não seja exatamente de 440Hz.

Utilizando o processo de repetição dos Timers podemos gerar um áudio sem travamentos, com uma precisão muito maior na geração da frequência, e determinar o tempo de duração do áudio, o que nos leva ao próximo exemplo.


Utilizando o Timer-1 do Arduino

O que é um Timer?

Um Timer, ou Timer/Counter, é uma parte do hardware construído dentro do microcontrolador do Arduino e serve para realizar uma contagem de tempo e disparar interrupções. Dependendo de como você o programar ele pode disparar um evento quando ocorrer um Overflow  do contador ou quando ocorrer um Match entre o valor do contador e o valor do registro de comparação.

Modo de Funcionamento

Dentre os diversos modos de funcionamento que um Timer pode assumir, os que nos interessam são os modos Normal e CTC.

Modo Normal com interrupção por overflow
O modo Normal é o funcionamento padrão do Timer. Primeiro colocamos o valor do contador na posição em que queremos que ele inicie e então ele segue contando até que o valor do contador ultrapasse o valor máximo do timer, causando um overflow.
Quando ocorre este overflow ele chama a interrupção ISR(TIMERx_OVF_vect) e executa os códigos que estão dentro dela.
OBS: dentro da interrupção é necessário atribuir novamente o ponto de início ao contador.

MODO CTC com interrupção por comparação
No modo CTC (Clear Timer on Compare) nós temos que definir o valor do registrador de comparação OCRxA, então o Timer segue contando normalmente e, a todo momento, faz uma comparação entre o valor do contador e o valor do registrdador OCRxA.
Quando estes dois valores forem iguais ele reinicia o contador do Timer automaticamente e chama a interrupção ISR(TIMERx_COMPA_vect) para executar os códigos que estão dentro dela.

Sobre o Cálculo do Período de Tempo
Tanto no modo NORMAL quanto no modo CTC nós precisamos definir por quanto tempo este Timer fará sua contagem:
Primeiro nós precisamos calcular um valor de Período de Tempo levando em conta a frequência de atuação que desejamos, a velocidade de processamento do microcontrolador e o valor do prescaler que está sendo utilizado.

Vamos utilizar o seguinte cálculo:
Interrupt Frequency = ( 1 / Frequency ) / 2
posicao_timer = ( CPU Frequency / prescaler x Interrupt Frequency ) – 1

Exemplo:
Prescaler = 8
Frequency
= 440 Hz
Interrupt Frequency =
( 1 / 440 ) / 2  =  0.0011363636
posicao_timer =
( 16.000.000 / 8 x 0.0011363636 ) – 1  = 2271

Modo Normal: Se o Timer estiver programado para funcionar em modo NORMAL, nós vamos colocar a posição do seu contador sendo o valor máximo de contagem menos o valor do posicao_timer. Desta forma o contador irá contar exatamente por este período de tempo até que alcance seu valor máximo, causando o overflow e disparando a interrupção.

Modo CTC: Se o Timer estiver programado para funcionar em modo CTC, nós vamos atribuir este valor de posicao_timer ao registrador de comparação OCRxA. Desta forma o contador irá contar a partir do valor inicial zero e, quando seu valor for igual ao do OCRxA, ele vai reiniciar o contador automaticamente e disparar a interrupção.

Resolução, Clock e Prescaler

O Arduino UNO possui três timers: Timer0, Timer1 e Timer2.
O Timer0 e o Timer2 são timers com resolução de 8-bits enquanto que o Timer1 é um timer com resolução de 16-bits.
Timers de 8-bits podem contar até 255, enquanto que timers de 16-bits podem contar até 65.535.


Se precisarmos fazer o timer contar valores que estejam dentro destas limitações nós podemos manter o sistema de contagem da forma normal como ele está programado, mas se for preciso contar até valores que estejam acima destes valores então precisamos determinar um valor de prescaler para o contador do timer.

Importante: Os valores de prescalers que podem ser determinados podem variar de um timer para o outro, portanto é preciso conferir no datasheet do microcontrolador quais valores de prescalers estão disponíveis para o timer que você pretende utilizar, lembrando que quanto menor o valor do prescaler maior a precisão da contagem do timer.


Segunda Etapa - áudio avançado com Timer-1

Abaixo temos o código do arquivo correspondente à segunda etapa.
Você pode fazer o download deste código clicando no link: codigo_segunda_etapa.zip

Este segundo código exemplifica como podemos fazer a geração de áudio utilizando um dos timers do Arduino. Neste caso vamos usar o Timer-1 em modo CTC.

Arquivo “segunda_etapa.ino”
 
int audioPin = 8;

void setup()
{
  pinMode(audioPin, OUTPUT);

  int freq = 440;

  float periodo = 1.0/freq;
  float meioPeriodo = periodo/2.0;

  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  OCR1A  = (16000000 / 8.0 * meioPeriodo) - 1;
  
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS11);
  TIMSK1 |= (1 << OCIE1A);
  interrupts();
}

ISR(TIMER1_COMPA_vect)
{ 
  digitalWrite(audioPin, digitalRead(audioPin) ^ 1);
}

void loop() {

}
 

Neste exemplo nós vamos incluir praticamente tudo o que programamos e aprendemos no código do exemplo anterior:
audioPin = 8;
pinMode (audioPin, OUTPUT);
freq = 440;
periodo = 1 / freq;
meioPeriodo = periodo / 2;
– definir o uso da porta digital 8 e configurá-la como OUTPUT,
– definir o valor da frequência de áudio,
– calcular os valores do período e do meio-período.

Mas, ao invés de ativar/desativar o alto-falante dentro do método LOOP, nós vamos utilizar a interrupção ISR(TIMER1_COMPA_vect) do Timer-1.

Logo após o cálculo da variável meioPeriodo nós vamos realizar a configuração do Timer-1, conforme a explicação que se segue:

noInterrupts();
Primeiro nós vamos desativar o funcionamento de todas as interrupções para podermos fazer as mudanças nos registradores do timer sem que ele cause nenhum comportamento indesejado durante sua configuração.

 

TCCR1A = 0;  TCCR1B = 0;
Zeramos todos os bits dos registradores TCCR1A e TCCR1B, colocando o Timer-1 em sua configuração padrão, com os contatores e interrupções desativados.

 

TCNT1 = 0;
Zeramos o contador TCNT1 para garantir que o timer vai iniciar sua contagem a partir do valor inicial.

OCR1A = (16000000 / 8 * meioPeriodo) – 1;
Definimos o valor do registrador de comparação OCR1A para determnar até que valor o timer deve fazer a sua contagem.
A fórmula utilizada é:  OCR1A = ( CPU Frequency / prescaler x Interrupt Frequency ) – 1

 


TCCR1B |= (1 << WGM12);
Colocamos o timer em modo CTC, aplicando a máscara de bits WGM12 ao registrador TCCR1B.

 


TCCR1B |= (1 << CS11);

O contador do Timer-1 não consegue realizar todas as contagens correspondentes às frequências das notas musicais que vamos usar mesmo ele sendo um timer de 16-bits, portanto, precisamos configurar o clock do timer para o Prescaler 8, aplicando a máscara de bits CS11 ao registrador TCCR1B.

 


TIMSK1 |= (1 << OCIE1A);
E por último, habilitamos o uso da interrupção de comparação ISR(TIMER1_COMPA_vect), aplicando a máscara de bits OCIE1A ao registrador TIMSK1.

 

interrupts();
Agora que o timer está configurado da forma que desejamos, nós habilitamos novamente o funcionamento de todas as interrupções.

ISR(TIMER1_COMPA_vect) {
   digitalWrite(audioPin, digitalRead(audioPin) ^ 1);
}
Agora o nosso timer passa a funcionar da forma que o configuramos, e sempre que o valor do contador se igualar ao valor do registrador OCR1A ele vai disparar a interrupção ISR(TIMER1_COMPA_vect) e executar o comando digitalWrite(audioPin, digitalRead(audioPin) ^ 1);

Neste comando digitalWrite nós passamos o valor do segundo parâmetro como “digitalRead(audioPin) ^ 1” para que ele sempre faça a inversão do valor de seu último estado. Poderíamos também ter escrito como “! digitalRead(audioPin)“; que iria funcionar da mesma forma.


Terceira Etapa - criando o comando Tone1

Abaixo temos o código do arquivo correspondente à terceira etapa.
Você pode fazer o download deste código clicando no link: codigo_terceira_etapa.zip

Agora nós temos um exemplo separado em dois arquivos: “terceira_etapa.ino” e “tone1.h”, modularizando, assim, o conteúdo responsável pela geração do áudio, que vamos poder ativar através da chamada “tone1(<pino>, <frequência>, <duração>);” e terminar seu funcionamento com o comando noTone1();

Arquivo “terceira_etapa.ino”
 
#include "tone1.h"

void setup()
{
  tone(8, 440, 1000);
  delay(1200);
  tone1(8, 440, 1000);
}

void loop() {

}
 

No arquivo “terceira_etapa.ino” nós precisamos incluir o arquivo “tone1.h” para termos acesso às suas funções.
Na função SETUP colocamos uma chamada ao comando tone original, depois damos um delay de tempo e, logo em seguida, fazemos uma chamada ao nosso novo comando tone1.
Desta forma podemos ouvir o áudio de ambos para podermos comparar se estão funcionando da mesma forma.

Arquivo “tone1.h”
 
int _audioPin;
int _audioFreq;
unsigned long _audioDur;
unsigned long _timeCnt;
float _timeMax;

void tone1(int pin, int frequency, unsigned long duration = 0)
{
  _audioPin  = pin;
  _audioFreq = frequency; // Hz
  _audioDur  = duration;  // Segundos

  pinMode(_audioPin, OUTPUT);

  float periodo = 1.0/_audioFreq;
  float meioPeriodo = periodo/2.0;

  _timeMax = (1.0/meioPeriodo) * (_audioDur/1000.0);
  _timeCnt = 0;
  
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  OCR1A  = (16000000 / 8.0 * meioPeriodo) - 1;
  
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS11);
  TIMSK1 |= (1 << OCIE1A);
  interrupts();
}

void noTone1() {
  TIMSK1 = 0;
  _timeCnt = 0; 
}

ISR(TIMER1_COMPA_vect)
{ 
  digitalWrite(_audioPin, digitalRead(_audioPin) ^ 1);

  if((_audioDur > 0) && (_timeCnt++ >= _timeMax)) {
    noTone1();
  }
}
 

No arquivo “tone1.h” nós colocamos todo o conteúdo que fizemos no arquivo “segunda_etapa.ino” só que de uma forma modularizada.
Primeiro nós definimos algumas variáveis globais para armazenar os valores de contagem de tempo, tempo máximo de duração e os valores que vamos receber através da chamada da função tone1.

Nesta função tone1 nós vamos receber os valores dos parâmetros pin, frequency e duration, e estes valores serão armazenados nas variáveis _audioPin, _audioFreq e _audioDur, respectivamente. Vamos configurar como OUTPUT a porta digital definida em _audioPin.

Da mesma forma como fizemos no exemplo anterior, “segunda_etapa.ino”, nós vamos definir o valor do período e do meio período. Desta vez nós vamos definir também o valor do tempo máximo de duração da nota (_timeMax) para sabermos o momento exato em que ela deverá ser interrompida automaticamente. Ao final da função tone1 nós vamos fazer a configuração do timer exatamente como fizemos no exemplo anterior.

Desta vez nós temos também a função noTone1, que reseta o registrador TIMSK1 e reinicia o contador de tempo máximo de duração _timeCnt, fazendo com que o timer pare de funcionar interrompendo, assim, a geração de áudio do nosso comando tone1.

O código da interrupção por comparação ISR(TIMER1_COMPA_vect) também foi modificado, pois incluímos nele uma comparação que verifica se o nosso contador de tempo máximo já alcançou o valor de tempo de duração. Se sim, então ele chama a função noTone1, interrompendo o funcionamento do timer.


Quarta Etapa - o novo exemplo "ToneMelody"

Abaixo temos o código do arquivo correspondente à quarta e última etapa.
Você pode fazer o download deste código clicando no link: codigo_quarta_etapa.zip

E Agora, por último, nós temos o exemplo da quarta etapa, dividido em quatro arquivos: “quarta_etapa.ino”, “musica.h”, “pitches.h” e “tone1.h”.

O arquivo “tone1.h” é o mesmo “tone1.h” que construímos no exemplo anterior.

O arquivo “pitches.h” é o mesmo que já vem no exemplo “toneMelody” do Arduino, e ele contém a lista de frequências de áudio com seus respectivos nomes de constantes; o que mudou neste arquivo é que incluímos a constante MUTE para quando quisermos apenas aguardar um período de tempo sem que nenhum som seja emitido.

O arquivo “musica.h” contem os arrays das notas musicais e os arrays de seus respectivos tempos de duração. É aqui que colocamos as notas musicais e os tempos de cada nota da música.

E o arquivo “quarta_etapa.ino” é o arquivo principal, que utiliza os códigos destes outros arquivos .h para ler e executar cada nota musical em seus respectivos tempos e canais de áudio.

Arquivo “pitches.h”
/*************************************************
   Public Constants
 *************************************************/

#define MUTE     -1   // foi incluído para adicionarmos um 
                      // período de silêncio quando necessário.
#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35

No arquivo “pitches.h” nós incluímos a constante MUTE, com o valor -1, para quando quisermos aguardar um período de tempo sem que nenhum som seja emitido.

Arquivo “quarta_etapa.ino”
 
#include "pitches.h"
#include "tone1.h"
#include "musica.h"

int audioPin0 = 8;  // canal 0
int audioPin1 = 9;  // canal 1

unsigned long ultimoTempo0 = 0;
unsigned long ultimotempo1 = 0;

int qteNotas0, qteNotas1;

int id0, id1;        // índice da nota
int nota0, nota1;    // nota
int tempo0, tempo1;  // tempo de duração, em milissegundos

void setup()
{
  pinMode(audioPin0, OUTPUT);
  pinMode(audioPin1, OUTPUT);
  qteNotas0 = sizeof(melody0)/sizeof(int);
  qteNotas1 = sizeof(melody1)/sizeof(int);

  id0 = -1;
  nota0 = 0;
  tempo0 = 0;
  
  id1 = -1;
  nota1 = 0;
  tempo1 = 0;
}

void loop()
{
  unsigned long tempoAtualMillis = millis();

  // Canal 0
  if(id0 < qteNotas0 - 1) if(tempoAtualMillis - ultimoTempo0 >= (tempo0 * 1.2))
    {
      ultimoTempo0 = tempoAtualMillis;
      id0++;
      nota0 = melody0[id0];
      tempo0 = noteDurations0[id0] * tempoMinimoNota;
      if(nota0 > MUTE) {
        tone(audioPin0, nota0, tempo0);
      }
    }

  // Canal 1
  if(id1 < qteNotas1 - 1) if(tempoAtualMillis - ultimotempo1 >= (tempo1 * 1.2))
    {
      ultimotempo1 = tempoAtualMillis;
      id1++;
      nota1 = melody1[id1];
      tempo1 = noteDurations1[id1] * tempoMinimoNota;
      if(nota1 > MUTE) {
        tone1(audioPin1, nota1, tempo1);
      }
    }
}
 

Neste quarto e último exemplo nós vamos incluir os arquivos “pitches.h”, “tone1.h” e “musica.h” dentro de “quarta_etapa.ino” para utilizarmos suas funcionalidades e valores de constantes.

Você vai perceber que ao longo do código existem variáveis que se repetem, alterando somente o valor final: 0(zero) para se referir à melodia principal e 1(um) para se referir ao acompanhamento.

Vamos definir as variáveis audioPin0 e audioPin1 para as portas digitais que queremos utilizar como saída de áudio. As variáveis ultimoTempo0 e ultimoTempo1 serão utilizadas para armazenar os valores do tempo corrente quando a última nota foi tocada, para auxiliar nas contagens de tempo MILLIS dentro da função LOOP, que veremos mais adiante.

Nas variáveis qteNotas0 e qteNotas1 nós vamos armazenar as quantidades de notas musicais presentes em cada um dos dois arrays (melodia principal e acompanhamento). Nas variáveis id0, id1, nota0, nota1, tempo0 e tempo1 nós vamos armazenar os índices, os valores de frequência e os tempos de duração das notas.

Dentro da função SETUP nós vamos definir as duas portas digitais audioPin0 e audioPin1 como OUTPUT, e vamos armazenar as quantidades de notas musicais dos arrays dentro das variáveis qteNotas0 e qteNotas1.

E, ao final da função SETUP, nós vamos fazer a inicialização de algumas das variáveis (id0, id1, nota0, nota1, tempo0 e tempo 1). As variáveis id0 e id1 devem começar como -1 para que, quando forem incrementadas pela primeira vez, assumam a posição inicial do array, que é 0(zero), que vamos entender melhor através do funcionamento da função LOOP.

Dentro da função LOOP nós vamos ler o tempo de contagem atual do Arduino através do comando millis() e vamos armazenar este valor na variável tempoAtualMillis.

Logo abaixo nós temos dois blocos de código que executam exatamente o mesmo processo, porém, um dos blocos faz a leitura e execução das notas da melodia principal enquanto que o outro bloco faz a leitura e execução das notas do acompanhamento.
Como você pode observar nós temos um bloco escrito “Canal 0” e outro escrito “Canal 1“. Cada um destes blocos faz a leitura da nota musical e do tempo de duração da nota, em seus respectivos arrays e tocam a nota musical pelo tempo de duração que foi determinado.

Exemplificando pelo bloco do Canal 0, estes blocos funcionam da seguinte forma:

if (id0 < qteNotas0 – 1)
Primeiro ele verifica se o id da nota que deve ser tocada é menor do que a quantidade de notas do array menos 1, isso garante que ele fará a leitura e interpretação das notas somente até a quantidade máxima de notas do array.
Se o id da nota estiver dentro deste intervalo então ele passa para a próxima condição.

if (tempoAtualMillis – ultimoTempo0 >= (tempo0 * 1.2))
Aqui o programa vai verificar se já se passou o período de tempo de duração da nota multiplicado por 1,2. Isso faz com que um pequeno espaço de tempo sem áudio  ocorra antes da rotina passar a ler e tocar a próxima nota musical.

ultimoTempo0 = tempoAtualMillis;
Assim que ele entra na rotina ele atribui o valor atual da contagem de tempo à variável ultimoTempo para que o tempo de espera possa ser resetado e ele possa contar quanto tempo se passou até poder passar para a próxima nota a ser tocada.

id0++;
Neste ponto o programa já incrementa a variável id para que possa ler a próxima nota do array. É por isso que lá na função SETUP nós inicializamos as variávels id0 e id1 com o valor -1 para que, ao chegar neste ponto, elas sejam incrementadas e comecem a ler a partir da posição zero do array.

nota0 = melody0[id0];
A variável nota recebe o valor da nota musical que deve ser tocada, do seu respectivo índice atual.

tempo0 = noteDurations0[id0] * tempoMinimoNota;
A variável tempo recebe o valor do tempo de duração da nota, do seu respectivo índice, e o multiplica pela variável tempoMinimoNota, que é um valor de tempo ajustável onde podemos deixar a música mais rápida ou mais devagar apenas ajustando este valor.

if (nota0 > MUTE) {
  tone(audioPin0, nota0, tempo0);
}
E, ao final do bloco do Canal 0, nós verificamos se o valor da nota musical é diferente do valor da nota MUTE, que acrescentamos à lista do arquivo “pitches.h”, pois só faz sentido tocar o som de uma nota se ela não for uma nota MUTE (sem áudio).

A mesma explicação acima se aplica ao bloco do Canal 1, só que este faz a leitura das notas e dos tempos de duração correspondentes à melodia de acompanhamento.

O Canal 0 utiliza o comando Tone e o Canal 1, o comando Tone1.
As notas são executadas dentro de blocos que se ativam por contagem de tempo através da leitura do tempo atual decorrido (Millis) ao invés de travar o tempo com Delay. Desta forma nós podemos tocar as notas da melodia principal e as notas da melodia de acompanhamento ao mesmo tempo e sem causar travamentos em nenhuma parte do programa.


Conclusão

Neste artigo aprendemos como criar um novo comando Tone e uma versão modificada do exemplo ToneMelody que nos permite tocar duas notas ao mesmo tempo. Criamos também um circuito que utiliza somente uma única saída (fone de ouvido ou mini alto-falante) para tocar as duas notas que são mixadas em um único sinal.

Nós utilizamos o Timer1 do Arduino UNO para criar o nosso comando Tone1 pois os timers 0 e 2 já são utilizados pelos comandos Tone e Millis que são necessários dentro do nosso novo exemplo do ToneMelody.

É possível utilizar um Arduino Mega 2560 e aproveitar os seus timers 3, 4 e 5 para criar novos comandos Tones, pois também são timers de 16-bits assim como o Timer 1.

Já que o nosso novo exemplo ToneMelody executa as notas musicais através da contagem com o comando Millis ao invés do comando Delay nós podemos aproveitá-lo em diversos tipos de projetos eletrônicos, tais como:

  • brinquedo infantil com vários botões, onde cada um executa uma música diferente
  • música de fundo para jogos eletrônicos simples (jogo da memória ou jogos para TV utilizando a biblioteca TVout)
  • projeto musical para árvore de natal ou para caixinhas de música
  • música de fundo dos times para jogos de futebol de botão
  • música para teclado infantil, em que as notas da música vão tocando à medida em que a criança aperta qualquer uma das teclas

E a lista de projetos segue até onde sua imaginação quiser…

Espero que tenha gostado deste projeto e que tenha várias ideias de omo utilizá-lo em novos projetos ou incluí-lo em alguns de seus projetos já existentes.


Eletrogate

5 de setembro de 2024

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!