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 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:
- 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)
- The note name in English notation as an uppercase letter, or lowercase if sharp (R has a special meaning and indicates a rest)
- 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!
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.