Playing music on Arduino

Provided you connect a piezo speaker to your Arduino board, the tone Arduino function allows to play tones given their frequencies. Let's use it to play entire melodies!

The Arduino board of my robotic teapot with a piezo sounder

The Arduino board of my robotic teapot with a piezo sounder

The melody encoding format I'll use is compact and pretty straightforward, but I admit it isn't the easiest one to read. The melody is represented as a null-terminated string of chars, in which each note is described by 3 consecutive characters:

  1. The note duration in sixteenth notes as an hexadecimal digit between 0 and F (0 has a special meaning and is interpreted as a whole note)
  2. The note name in English notation as an uppercase letter, or lowercase if sharp (R has a special meaning and indicates a rest)
  3. The octave number as a decimal digit, between 0 and 8 (for a rest, the value is ignored)

So for instance, a quarter-note C from the 5th octave is 4C5, and a eighth-note D sharp from the 4th octave is 2d4.

With this notation, the Tetris theme is:

4E52B42C54D52C52B44A42A42C54E52D52C56B42C54D54E54C54A42A42A42B42C56D52F54A52G52F56E52C54E52D52C54B42B42C54D54E54C54A44A44R0

In this example, a piezo passive sounder is connected on pin 3 of the Arduino board. The code to play a melody stored in the format defined previously can be written as follows:

// Melody player implementation
class Player
{
public:
  // Create the player for specified speaker pin
  Player(int speakerPin)
  {
    pin = speakerPin;
    current = NULL;
    next = NULL;
    left = 0;
  }

  // Start playing melody
  void play(const char *melody, bool looped = false)
  {
    current = melody;
    if(looped) next = melody;
    else next = NULL;
    left = 0;
  }

  // Stop melody
  void stop(void)
  {
    current = NULL;
    left = 0;
  }

  // Update melody, must be called on each quarter of a beat
  void sync(long bpm)
  {
    // Check if no melody is set
    if(!current) return;

    // Check if a note is playing
    if(left)
    {
      left--;
      if(left) return;
    }

    // Check for end of the melody string
    if(!current[0] || !current[1] || !current[2])
    {
      current = next;
      if(!current) return;
    }

    // Read note in the melody string
    unsigned char d = (unsigned char)current[0];
    unsigned int duration = (d >= 0x41 ? d - 0x41 + 10 : d - 0x30);
    unsigned int octave = (unsigned char)current[2] - 0x30;
    char note = current[1];

    // 0 is interpreted as a whole note, or semibreve
    if(duration == 0) duration = 16;

    // Update status
    left = duration;
    current+= 3;

    // Play note
    unsigned int frequency = pitch(octave, note);
    if(frequency)
    {
      unsigned long unit = 60000L/(bpm*4);
      noTone(pin);
      tone(pin, frequency, unit*duration - unit/2);
    }
  }

private:
  int pin;
  const char *current;
  const char *next;
  unsigned int left;  // sync calls left for current note
};

After setting the melody, the function syncMelody() must be called on each quarter of a beat to play it at the chosen tempo. It does not wait by itself to allow some other processing between calls.

The helper function pitch(unsigned int octave, unsigned char note) returns the frequency given an octave number and a note name. Lowercase note name is interpreted as sharp, and 0 is returned on a rest or on error. Note A from the 4th octave returns 440 Hz.

// Return frequency given octave number and note name
unsigned int pitch(unsigned int octave, char note)
{
  // Note names array
  static const unsigned int countNotes = 12;
  static const char Notes[] = { 'C', 'c', 'D', 'd', 'E', 'F', 'f', 'G', 'g', 'A', 'a', 'B' };

  // Frequencies array
  static const unsigned int countFreqs = countNotes*7 + 4;
  static const unsigned int Freqs[] =
    {   33,   35,   37,   39,   41,   44,   46,   49,   52,   55,   58,   62,
        65,   69,   73,   78,   82,   87,   93,   98,  104,  110,  117,  123,
       131,  139,  147,  156,  165,  175,  185,  196,  208,  220,  233,  247,
       262,  277,  294,  311,  330,  349,  370,  392,  415,  440,  466,  494,
       523,  554,  587,  622,  659,  698,  740,  784,  831,  880,  932,  988,
      1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
      2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
      4186, 4435, 4699, 4978 };

  // Rest
  if(note == 'R' || note == 'r')
    return 0;

  // Find note index
  unsigned int n = 0;
  while(Notes[n] != note)
    if(++n == countNotes)
      return 0;

  // Frenquency index
  if(octave == 0) return 0;
  unsigned int p = countNotes*(octave-1) + n;

  if(p < countFreqs) return Freqs[p];
  else return 0;
}

For instance, here is a simple example playing the Tetris theme!

// Tetris melody
const char melody[] = "4E52B42C54D52C52B44A42A42C54E52D52C56B42C54D54E54C54A42A42A42B42C56D52F54A52G52F56E52C54E52D52C54B42B42C54D54E54C54A44A44R0";
const int speakerPin = 3;  // speaker on pin 3

Player player(speakerPin);
long bpm = 120L;

setup()
{
  player.play(melody, true);
}

loop()
{
  player.sync(bpm);
  delay(60000L/(bpm*4));    // a quarter of a beat
}

I can now upgrade my dancing teapot to have it play a melody while dancing!

The Arduino board of the robotic teapot with a piezo sounder

The Arduino board of the robotic teapot with a piezo sounder

After adding the piezo speaker and including the previous code, I simply modified the main functions as follows:

[...]
Player player(speakerPin);
long bpm = 120L;

void sync()
{
  // Given set BPM, delay until the next quarter of a beat
  static unsigned long lastMillis = 0;
  unsigned long duration = 60000L/(bpm*4);
  long leftMillis = lastMillis + duration - millis();
  if(leftMillis > 0) delay(leftMillis);
  lastMillis = millis();

  // Update player
  player.sync(bpm);
}

void nsync(unsigned int count)
{
  // Call count times sync
  while(count--)
    sync();
}

void loop()
{
  // Set current melody
  player.play(melody);

  // Now do some choregraphy on the melody
  int cycles = 32;
  while(cycles--)
  {
    left(-20, 0);
    right(-20, 0);
    nsync(4);      // 1 beat
    left(20, 0);
    right(20, 0);
    nsync(4);      // 1 beat
  }
}

As a result, the teapot now dances and sings!

You can find the upgraded source code under GPLv3 on my repository on GitHub.

Categories
Tags
Feeds