How it Works
The first part of the code defines what song will be played by saving the following to EEPROM:
- This DATA directive defines which notes will be played. Lower case letters denote flats while upper case letters denote notes that are played normally, and R denotes a rest.
- This DATA directive defines what octave the previously defined notes will be played in. Remember that we’ve increased the octave by two from the sheet music; so any note played in the 4th octave is now played in the 6th and any note played in the 5th octave is now played in the 7th.
- This DATA directive defines how long each note will be played. The value of the duration corresponds to the denominator of the note’s value. So a half note (1/2) would have a duration value of 2 and a quarter note (1/4) will have a duration value of 4.
- This DATA directive defines whether the note is dotted or not. A value of 1 denotes a dot and a value of 0 means the note is not dotted.
The code that follows the DATA directives can be used as a template for any song that you may want to play. Basically, each note is read from EEPROM and played at the proper frequency for the correct amount of time. Let’s take a more in-depth look at how the program accomplishes this.
The first part of the code reads the note stored in the Notes DATA directive and places it into a variable named noteLetter.
READ Notes + index, noteLetter
A lookup table is then used to store the corresponding frequency for each note relative to the eighth octave. The eighth octave in musical turns is technically simply the note “C.” However, for mathematical reasons and since the lower octaves don’t play clearly through the piezospeaker, we will define each note of the eighth octave with a frequency and use this octave as a baseline for calculating the frequencies of notes in other octaves.
LOOKDOWN noteLetter, [ "C", "d", "D", "e", "E", "F", "g", "G", "a", "A", "b", "B", "R", "Q" ], offset LOOKUP offset, [ 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902, 0, 0 ], noteFreq
Seeing as we don’t want every note to be played in the 8th octave, we need to calculate the frequency of each note in the octave we do want to play. Take another look at Figure 10 (previous page) and see if you can detect a pattern when dropping from the note C8 to C7 to C6. Do you notice how their values cut in half each time you drop an octave? For example, the frequency of C8 is 4186 Hz. 4186 divided by 2 is 2093 or the frequency of the note C7. 2093 divided by 2 is 1046.5 or the frequency of the note C6. This pattern continues all the way down the octave, and is how we were able to assign frequency values for notes in our 8th octave.
However, we would have some pretty tedious PBASIC code if we had to keep dividing by 2 in order to get the desired frequency value. There must be an easier way to drop multiple octaves, right? Right! Let’s take the C-note example again: say we wanted to drop from C8 to C6, isn’t that the same as dividing the frequency of C8 by 4? Or, if we wanted to drop from C8 to C5 (523.25 Hz), isn’t that the same as diving the frequency of C8 by 8? By George, I think there’s a pattern emerging here!
By using the 8th octave as a reference, we can divide a note’s frequency by 8, 4, 2 or 1 in order to calculate the frequency values for the 5th, 6th, 7th, and 8th octaves, respectively. To put it in even simpler terms: we could also divide by 23, 22, 21, or 20. To translate this code into PBASIC, we first read the octave from EEPROM and save it to a variable named noteOctave.
READ Octaves + index, noteOctave
Then, in order to calculate the correct power to raise 2 to, we subtract the value in noteOctave from 8. So if noteOctave was 7 now it’s 1. If noteOctave was 6 now it’s 2, etc.
noteOctave = 8 - noteOctave
Lastly, we need to divide the current frequency by 2 raised to the power of noteOctave. In PBASIC we can use the DCD operator, which is a 2n-power decoder of a 4-bit value. So if we divide noteFreq by DCD noteOctave, we are in turn diving by 1, 2, 4, or 8 which will, in turn, give us the correct noteFreq value.
noteFreq = noteFreq / (DCD noteOctave)
Now we need to calculate how long to play each note. First, we read the value stored in the Durations DATA directive and store it in a variable named noteDuration.
READ Durations + index, noteDuration
In order to calculate the millisecond duration for each note, we’ll use the theoretical duration of a whole note as a base. In this case, we’ll say a whole note would last one second. Therefore, a half note would last 500 ms, a quarter note would last 250 ms, and an eighth note would last 125 ms. Since we set the values in the Durations DATA directive to the number in the denominator of the notes value, we just need to divide 1000 by the value in noteDuration.
noteDuration = 1000 / noteDuration
To finish, we need to re-calculate the note’s duration if it is a dotted note. Remember, we set dotted notes equal to 1 in the Dots DATA directive, so if we read a 1 from the Dots DATA directive, we need to increase that note’s duration by one-half its value. Or, to simplify matters, multiply the note’s duration by 1.5.
READ Dots + index, noteDot IF noteDot = 1 THEN noteDuration = noteDuration * 3 / 2
Then, we play the note for the specified duration through the piezospeaker, add one to the EEPROM index and repeat the process until the value in the Notes DATA directive is “Q”.