Music with a Piezoelectric Buzzer

Music with a Piezoelectric Buzzer

Introduction

In this project, we will be generating sound using a piezoelectric buzzer. We will initially explore how to generate different frequencies by cycling a digital pin from HIGH to LOW at different rates. We will then examine some of the fundamentals of music theory and how they can help us play music with our buzzer.

Inventory:

Qty Description Typical Image Schematic Symbol Breadboard Image
1 Piezoelectric buzzer

How a Piezoelectric Buzzer Works

When generating sound using electronics, the first component that comes to mind is a speaker. Most speakers consist of two electromagnetic coils, called “voice coils”. One of the coils is attached to a flexible material called the speaker cone. When current is applied to the voice coils, an electromagnetic field is generated. The force generated between the fields moves coils which in turn makes the speaker cone vibrate. We perceive these vibrations in the air as sound.

The piezoelectric buzzer works very similarly, except it does not use electromagnets. Instead, it makes use of a material that has piezoelectric properties. When a material is piezoelectric, it means that applying mechanical force (i.e., bending or squeezing it) will cause it to gain charge. Inversely, applying an electric charge will cause the material to stretch or expand a very small amount. By pulsing our buzzer on and off we make the piezoelectric material inside quickly expand and contract. This produces the vibrations we hear in the air.

Piezoelectric materials found in most devices usually consist of crystals or special ceramics. Some biological materials like bone, DNA, and certain proteins also exhibit the same behavior. Typically, crystals and ceramics are used in most piezoelectric devices. The piezoelectric properties of quartz crystal is often taken advantage of to make the sparkers found in gas stoves. Quartz is also used in crystal oscillators, which aid in controlling the timing of computers and some wrist watches. Piezoelectric materials can even be used to build sensors like microphones or guitar pickups. In this project, we will be using a piezoelectric buzzer as a rudimentary speaker.

Setting up the Circuit

The circuit setup in this project will be very simple. The only component we will need is the buzzer, which we will hook directly to the chipKIT™ board. The only catch is that the buzzer has a polarity. Figure 1 illustrates the polarity convention for the buzzer where the anode is closest to the square sound port. Figure 2 depicts the schematic diagram of the circuit. To construct the circuit, follow the steps illustrated in Fig. 3.

 
Figure 1. Buzzer polarity.
Figure 2. Schematic representation of buzzer circuit.

  1. Place the buzzer in the breadboard. Be sure to keep track of what node each leg is in. The legs are not visible when the buzzer is securely in place. If you forget, you can tilt the buzzer without removing it to see what node each leg occupies.
  2. Connect the buzzer's cathode to ground.
  3. Connect the buzzer's anode to pin 4.
Figure 3. Buzzer circuit.

Frequency and Types of Waves

To be able to generate music, we need to be able to play different notes. A specific note is determined by the frequency at which it is played. We will write a function to play various notes, but first we must understand what determines a wave's frequency.

The frequency of a wave tells us how often it repeats within a specific unit of time. One repetition of a wave is referred to as a “cycle” The number of cycles in one second is equivalent to a wave's frequency in hertz (Hz). Measuring waves in hertz makes sense since it gives us a good idea of how quickly the wave repeats. For example, a 1 Hz wave will repeat once every second, which seems fast to us. On the other hand, a 500 Hz wave will repeat five hundred times in one second, which is much faster in comparison. The time it takes for one cycle of a wave to occur is referred to as the “period.” We will be controlling the frequency of the sound waves we generate by changing their periods. The following equation will come in handy for relating the period of a wave to its frequency. Note that f is the frequency and T is the period.

$\large f = \frac{1}{T} $

With the equation above, we can calculate the period needed to produce a wave. How the voltage changes during each period, whether it is a smooth curve or an abrupt edge, determines what type of wave it is. Figure 4 illustrates how a wave's shape, cycle, period, and frequency all relate to produce specific waveforms and frequencies.

Figure 4. Period and frequency of different waveforms.

Up until now we have primarily been using “square waves” with the chipKIT because they are the easiest to generate. Generating a square wave is simply a matter of switching a pin from HIGH to LOW with the proper timing. This is unlike most sound waves which consist of smooth sinusoids, such as cosine and sine waves. Due to their curves, sinusoids are difficult for digital devices to generate, so for this project we will stick to using a square wave. As a consequence, the tones we generate will sound a bit different than tones played using a sinusoidal wave. In fact, each waveform shown in Fig. 4. sounds slightly different. The sound of each waveform becomes increasingly “buzzier” going from top to bottom. The buzz of a waveform becomes very apparent at low frequencies, and is more noticeable because our brains have time to pick out the sharp edges. This becomes clear when you play a square wave at a very low frequency since it sounds more like repetitive clicking than a buzz. If you were to increase the frequency of the square wave, your brain would not be able to keep up. This causes the clicking sounds to blend together into a buzzy tone. We will explore this phenomenon more in the next section when we program the chipKIT to play different frequencies.

Writing the Frequency Control Sketch

The code to play a desired frequency is fairly straightforward. We will write a function playFreq() that plays a frequency in hertz for a specified number of milliseconds. Our playFreq() function will take three arguments, int buzzerPin, double freqHz, and int durationMs . The input frequency will be a double type because it allows us to play exact decimal frequencies.

The first operation our function needs to do is to convert the argument freqHz into the correct period. If you recall, the equation introduced in the previous section tells us the frequency given the period. Doing some simple algebra gives us a new equation that tells us the period given the frequency.

$\large T = \frac{1}{f} $

Unfortunately, this equation gives us the period in seconds while the chipKIT only measures in either milliseconds or microseconds. We will want to measure our wave's period in microseconds since a period measured in milliseconds will not be precise enough to accurately represent decimal frequencies. This inaccuracy is due to the fact that the delay functions only accept int values, causing some precision to be lost.

For example, to find the period of a 2.3 Hz frequency in milliseconds, you would simply multiply the above equation by 1000 to convert to milliseconds, like so:

$\frac{1}{2.3 \ Hz}$ × 1000 = 434.782 ms

Now type casting the value to an int, you can see the value is truncated to 434 ms.

int( 434.782) = 434 ms

Similarly, to find the period in microseconds, you would multiply by 1,000,000.

  • $\frac{1}{2.3 \ Hz}$ × 1000000 = 434782.608µs
  • int( = 434782.608) = 434782 µs
  • ($ 434782 \mu s \Leftrightarrow 434.783 ms $)

Ultimately, this means we will lose negligible number precision if we measure our period in microseconds. The frequency to period conversion code will look like the following:

                  int periodMicro = int((1/freqHz)*1000000);
                

With the correct period calculations done, all we need to do is repeatedly drive a pin HIGH and LOW with a bit of delay in between. Looking back at Fig. 4, it is easy to see that the square wave is HIGH for half of its period and LOW for the other half. This means we will delay for half of the calculated period in between each pin transition.

Next we need to figure out how to repetitively change the pin value while keeping track of the time. A while() loop comparing the proper values will accomplish this quite nicely. The elapsed playtime can be calculated by storing the start time (just before the while() loop is called), and subtracting the current time from it. This can then be compared to the desired play duration.

Now that we are finished writing our playFreq() function, we will test it by playing 6 frequencies (from 1 to 10,000 Hz) for a duration of two seconds with a half second pause between each. The play and pause durations are controlled by the playTimeMs and pauseTimeMs variables and can be easily adjusted if need be. Overall, the final frequency control test sketch will look as follows:

                  int buzzerPin = 4;
                  void setup() 
                  {
                    pinMode(buzzerPin,OUTPUT);
                    int playTimeMs = 2000;//time to play frequency in milliseconds
                    int pauseTimeMs = 500;//time to pause between each frequency in milliseconds
  
                    playFreq(buzzerPin,1, playTimeMs); 
                    delay(pauseTimeMs);
                    
                    playFreq(buzzerPin,50, playTimeMs); 
                    delay(pauseTimeMs);
                    
                    playFreq(buzzerPin,100, playTimeMs); 
                    delay(pauseTimeMs);
                    
                    playFreq(buzzerPin,1000, playTimeMs); 
                    delay(pauseTimeMs);
                    
                    playFreq(buzzerPin,5000, playTimeMs); 
                    delay(pauseTimeMs);
                    
                    playFreq(buzzerPin,10000, playTimeMs); 
                    delay(pauseTimeMs);
                  }
  
                  void playFreq(double freqHz, int durationMs)
                  {
                      //Calculate the period in microseconds
                      int periodMicro = int((1/freqHz)*1000000);
                      int halfPeriod = periodMicro/2;
                      
                      //store start time
                      int startTime = millis();
                      
                      //(millis() - startTime) is elapsed play time
                      while ((millis() - startTime) < durationMs)    
                      {
                          digitalWrite(buzzerPin, HIGH);
                          delayMicroseconds(halfPeriod);
                          digitalWrite(buzzerPin, LOW);
                          delayMicroseconds(halfPeriod);
                      }
                   
                  }
  
                  void loop() 
                  {
                    
                  }

Playing Basic Music

In this section, we will begin to delve into how to make our chipKIT play simple songs. This portion of the project requires that you understand some basic music theory (i.e., musical notes, octaves, sharp notes). You must also be familiar with how to use arrays in your code. If you are unfamiliar with either of these topics, read through the related material by clicking the appropriate tab on the right. The song we will be playing with our buzzer is the 1985 Super Mario Bros® main theme. Specifically, we will only play the melody since playing multiple notes simultaneously is complicated.

The first operation our function needs to do is to convert the argument freqHz into the correct period. If you recall, the equation introduced in the previous section tells us the frequency given the period. Doing some simple algebra gives us a new equation that tells us the period given the frequency.

Storing Note Frequencies

In order to play a song, we need to have access to any of the possible notes that will be in the song. Let's consider how we are going to store the frequencies of the notes we want to play. When referring to notes on a piano, there are 7 major octaves. In each octave, there are 12 total notes. That means there are 84 notes in all, which is a lot of frequencies to keep track of! To organize all of these frequencies, we will use two-dimensional arrays. Each row of the array (the vertical index) will correspond to one of the seven octaves. Each column of the array (the horizontal index) will correspond to each of the note frequencies contained in that octave. To help organize things further, we will store the regular notes of each octave in an array named octaves[7][7] and the sharp notes in an array named sharpOctaves[7][5]. Since there are only five sharp notes per octave, sharpOctaves[7][5] will only have five columns. Both of the arrays should look as follows.

				//Store all note frequencies for each octave in a 2 dimensional array
				 double octaves[7][7] = 
				 {  
				  //Octave1 Notes:      c1      d1      e1      f1      g1      a1      b1
				  /* 1st octave */   {32.703, 36.708, 41.203, 43.654, 48.990, 55.000, 61.735},

				  //Octave2 Notes:      c2      d2      e2      f2      g2       a2       b2
				  /* 2nd octave */   {65.406, 73.416, 82.407, 87.307, 97.999, 110.000, 123.470},
				  
				  //Octave3 Notes:       c3       d3       e3       f3       g3       a3       b3 
				  /* 3rd octave */   {130.810, 146.830, 164.810, 174.610, 196.000, 220.000, 246.940},
				   
				  //Octave4 Notes:       c4       d4       e4       f4       g4       a4       b4
				  /* 4th octave */   {261.630, 293.660, 329.630, 349.230, 392.000, 440.000, 493.880},
				  
				  //Octave5 Notes:       c5       d5       e5       f5       g5       a5       b5
				  /* 5th octave */   {523.250, 587.330, 659.250, 698.460, 783.990, 880.000, 987.770},

				  //Octave6 Notes:        c6        d6        e6        f6        g6        a6        b6  
				  /* 6th octave */   {1046.500, 1174.700, 1318.500, 1396.900, 1568.000, 1760.000, 1979.500},

				  //Octave7 Notes:        c7        d7        e7        f7        g7        a7        b7  
				  /* 7th octave */   {2093.000, 2349.300, 2637.000, 2793.800, 3136.000, 3520.000, 3951.100}
				 };
				 

				//Store the sharp note frequencies for each octave in a 2 dimensional array
				double sharpOctaves[7][5] =
				{
				 //Octave1 Sharps:     C1      D1      F1      G1      A1 
				 /* 1st octave */   {34.648, 38.891, 46.249, 51.913, 58.270},
				 
				 //Octave2 Sharps:     C2      D2      F2       G2       A2 
				 /* 2nd octave */   {69.296, 77.782, 92.499, 103.830, 116.540},
				 
				 //Octave3 Sharps:      C3       D3       F3       G3       A3 
				 /* 3rd octave */   {138.590, 155.560, 185.000, 207.650, 233.080},
				 
				 
				 //Octave4 Sharps:      C4       D4       F4       G4       A4 
				 /* 4th octave */   {277.184, 311.130, 369.990, 415.300, 466.160},
				 
				 //Octave5 Sharps:      C5       D5       F5       G5       A5 
				 /* 5th octave */   {554.370, 622.250, 739.990, 830.610, 932.330},
				 
				 //Octave6 Sharps:       C6        D6        F6        G6        A6 
				 /* 6th octave */   {1108.700, 1244.500, 1480.000, 1661.200, 1864.700},
				 
				 //Octave7 Sharps:       C7        D7        F7        G7        A7 
				 /* 7th octave */   {2217.500, 2489.000, 2960.000, 3322.400, 3729.300}  
				};

You may have noticed in the previous code box that each note frequency is labelled with its note letter followed by its octave number. You may have also noticed that the sharp notes are labelled with capital letters while the regular notes are labelled with lower case letters. This provides a quick and easy way to differentiate a sharp from a regular note when in character form. It also brings us to our next challenge: How to represent a melody within MPIDE.

Representing a Melody in MPIDE

Our next task is thinking of a way to conveniently represent a song within MPIDE. Specifically, what we need to do is determine how to organize and represent the individual musical components of a melody. To do so, let's take a moment and list out what these key components are.

  1. Regular notes
  2. Sharp notes
  3. Pauses in the song
  4. The duration of each note/pause.

Since most notes are referred to with letters and numbers, it makes sense to use combinations of characters to represent these four key components. As you may already know, the most convenient way of grouping characters in a program is with String object. If you are unfamiliar with how Strings work, follow the yellow tab on the right. To represent one line in a song we will use one span String. The String will contain character combinations that we will decode with other functions later on. The details of what these character combinations are, and how they should be handled are described in the following sub sections.

Regular Notes:

To represent a regular note, we will simply use the note's lower case letter followed by its octave number. This will match the convention we previously established when organizing and labelling our 2d note arrays.

Ex. c5, a2, b3

Sharp Notes:

To represent a sharp note, we will simply use the note's upper case letter followed by its octave number. Again, this will match the convention we previously established when organizing and labelling our 2d note arrays.

Ex. C5, A2, B3

Pauses:

To represent pauses in the song, we will use a dash followed by an equal symbol (“-=”). This will make pauses easy to type as well as easy to count. We will assume a pause lasts as long as a note, unless otherwise specified.

Ex. -=

Note Length and the Length Modifier:

Next we need a method for representing a note's length. To streamline our design, we will assume a note is a default length unless otherwise specified. This will work well since the majority of notes (and pauses) in a song typically have the same length. For this particular melody, most of the notes are quarter notes (this was determined from the sheet music). So in our scheme we will assume by default a note should be played as a quarter note.

If we want to change the play length of a note, we will use a length modifier. By convention, the modifier will appear before the note it is modifying. The modifier will consist of an exclamation point followed by a number (ex. “!1”). The number in the modifier will correspond to a location in an array which contains the alternative note length. In practice, the modifier will work as follows.

Consider the following code:

					  //the typical length notes in the song are quarter notes
					double typicalNoteLength = 0.25; 
      
					//store the modified note lengths for the song
					double newNoteLength[ ] = {0.5, 0.125};
					
					String songLine = "a1b2!0c3-=d5!1-=";

When playing songLine above, most notes will be read as a quarter note by default. This is why typicalNoteLength is set to 0.25. The note “c3”, however, will be played as a half note. This is because it's preceded by the length modifier “!0”. The zero in the modifier corresponds to the 0th element (0.5) in the array newNoteLength. The next modifier to appear in the String is “!1”. It is modifying the length of the last pause “-=”. The final pause will have the duration of an eighth note since the 1 in the modifier corresponds to the 1st element (0.125) in the newNoteLength array.

Now that we have established a structure and a character scheme for each song, we can begin to piece it together. The convenient way to represent an entire song or melody will be to use an array of Strings. Remember, think of each String in the array as a line for the song. To play the entire song, you simply have to loop through and grab each String. Once you have obtained the String, you can simply call the function that will decode and play the notes for that line. Overall, our representation for an entire melody will look as follows.

					  // the tempo is 100 whole notes per minute
					  int tempo = 100; 
						
					  //the typical length notes in the song are quarter notes
					  double typicalNoteLength = 0.25; 
						  
					  //store the modified note lengths for the song
					  double newNoteLength[ ] = {0.0167, 0.3333};
					  
					  
					  String fullMelody[ ] =
					  {
						"e5!0-=e5-=e5   -=c5e5-=  g5-=-=-= g4-=-=-=",
						"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
						"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
						"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
						"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
						"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
						"-=-=g5F5 f5D5-=e5 -=c6-=c6 c6-=-=-=",
						"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
						"-=-=D5-= -=d5-=-= c5-=-=-= -=-=-=-=",
						"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
						"-=-=g5F5 f5D5-=e5 -=c6-=c6 c6-=-=-=",
						"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
						"-=-=D5-= -=d5-=-= c5-=-=-= -=-=-=-=",
						"c5c5-=c5 -=c5d5-= e5c5-=a4 g4-=-=-=",
						"c5c5-=c5 -=c5d5e5 -=-=-=-= -=-=-=-=",
						"c5c5-=c5 -=c5d5-= e5c5-=a4 g4-=-=-=",
						"e5e5-=e5 -=c5e5-= g5-=-=-= g4-=-=-=",
						"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
						"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
						"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
						"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
						"e5c5-=g4 -=-=G4-= a4f5-=f5 a4-=-=-=",
						"!1b4!1a5!1a5 !1a5!1g5!1f5 e5c5-=a4 g4-=-=-=",
						"e5c5-=g4 -=-=G4-= a4f5-=f5 a4-=-=-=",
						"b4f5-=f5 !1f5!1e5!1d5 c5e4-=e4 c4-=-=-="
					  };

You may have noticed that a tempo variable has been added to our song representation. This is because it will be needed later on, along with the note length, to calculate the note's duration in milliseconds. The tempo was derived from the original sheet music and can be used to make the song play slower or faster without having to modify the various note lengths that have been stored. Another previously unmentioned addition to our melody is the white space between notes. This was added strictly for organizational purposes. The white space helps illustrate that each line contains exactly 4 groups of notes. Within each group there are only 4 notes, including pauses. Any ! modifiers do not count as a note, so some groups appear to be longer than others. Breaking the notes of a line into groups helps make the song more readable. It also makes it easier to translate sheet music to our scheme. Eventually the organizational white space will be stripped from each song line so it is easily decoded.

Writing the Functions to Play a Melody

With an idea of how our melody will be structured, we can begin writing the functions necessary to decode and play it. While writing these functions, we are going to use a top down coding approach. This means as we run into challenges we will imagine up new sub-functions to solve these problems. We will not be concerned with writing these sub-functions until later. With this approach you can divide up the work. Once you have written all of your necessary functions, you can start testing and fixing each function from the bottom up. The bottommost functions (the ones that contain no other sub-functions) are typically small and easy to understand. Once you ensure these bottommost functions are working how you want, you can move up and test the function that calls them. Since you have already tested and verified the sub-function, you can rule them out as a potential cause for any problems and focus on fixing your current function.

playMelody()

To begin our top down approach, we will write a function called playMelody(). This function will simply use a for loop to loop through and decode/play each String stored in the fullMelody array. The details of decoding and playing the notes in each String will be handled by a sub-function called playLine(). We will go over the details of playLine() in a moment. For now, the complete playMelody() is shown below.

					void playMelody(String song[ ], double typicalNoteLength, int tempo, double newNoteLength[ ], int numLines)
					{
						//Loop through and play each line of song
						 for(int i = 0; i < numLines; i++)
						 {
						   playLine(song[i], typicalNoteLength, tempo, newNoteLength);
						 }
					}	

playLine()

Our next task is implementing the sub-function playLine(). This function can be a bit difficult to understand, so the completed function is shown below for reference. Referring back to the completed function as you read the description may make it easier to follow.

				void playLine(String songLine, double typicalNoteLength, int tempo, double newNoteLength[])
				{
				  //remove spaces from song
				  String lineNotes = removeWhiteSpace(songLine);
					  
				   
				  for( int i = 0; i < lineNotes.length(); i+=2)
				  {    
					 //get the current note to be played
					  char noteLetter = lineNotes[i];
					   
					  //get octave number of note to be played
					  char noteNum = lineNotes[i+1];
				 
					 //convert noteDigit from char to int so it can be used as an  array index
					 int noteOctaveNum = charToDigit(noteNum);
					 
					 //declare noteLengthMs before if statement 
					 int noteLengthMs;
					  
					  
					 //Check to see if the typical note length or a modified note length should be used to play the next note
					 if(noteLetter == '!')//use modified note length 
					 {
						
					   //Calculate the notes length in milliseconds (for modified note lengths)
					   noteLengthMs = noteLengthToMs(newNoteLength[noteOctaveNum], tempo);
					   
					   i+=2;//increment index to grab the note following the "!0" length modifier
					   noteLetter = lineNotes[i]; 
						
					  //get octave number of note to be played after "!0" length modifier
					  noteNum = lineNotes[i+1];
					  //convert noteDigit from char to int so it can be used as an  array index
					  noteOctaveNum = charToDigit(noteNum);
					  
					 }
					 else//use typical note length 
					 {
					  //Calculate the notes length in milliseconds (for typical note lengths)
					  noteLengthMs = noteLengthToMs(typicalNoteLength, tempo);
					 }
				   
					 playNote(noteLetter, noteOctaveNum ,noteLengthMs);
				  }
				}

Inside of playLine(), the first thing we want to do is remove the spaces we've added to the song lines. To do so, let's assume we have a sub-function called removeWhiteSpace() that removes all the spaces in a string. It will do as its name implies and store the resulting String in the variable String lineNotes.

Next, we will grab the letter of each note from lineNotes using a for loop. The for loop's counter will act as the index of the String lineNotes. This works because the characters in a String object can be accessed the same way as the elements of an array. The note character obtained from lineNotes will be stored in the variable char noteLetter for later use. Similarly, the note's numerical character will be obtained from lineNotes by using the index i+1. This numerical character will be stored in the variable char noteNum. Remember that the numeric character in our scheme corresponds to the octave of the note in question. Later on when we want to select the correct row in our 2d octave arrays, we will need to use an integer as the index. So we will assume we have a sub-function called charToDigit() that converts a numerical character to is integer equivalent. In this case, charToDigit() will store its resulting value in a variable called noteOctaveNum.

Now that we have the character and octave number of the note we want to play, we will check and make sure that the note wasn't actually a “!” modifier. Using noteLetter == '!' as the argument for an if statement will accomplish this. Now there are two possible outcomes for the if statement.

  1. noteLetter is not a '!'.

    This means we should use the default note length to calculate the notes duration in milliseconds.

  2. noteLetter is an '!'.

    This means we must calculate the notes duration in milliseconds based one of the alternative note lengths stored in the newNoteLengtharray.

Assuming the 1st possible outcome occurs, we will use a sub-function called noteLengthToMs() to calculate the note's duration in milliseconds. It will be called as follows: noteLengthMs = noteLengthToMs(typicalNoteLength, tempo) Assuming the 2nd possible outcome occurs, then we will call noteLengthToMs() as follows: noteLengthMs = noteLengthToMs(newNoteLength[noteOctaveNum], tempo). Note that newNoteLength[noteOctaveNum] has replaced typicalNoteLength in the arguments. Recall newNoteLength is an array which stores alternative note lengths. After calculating the modified note duration, we must grab the characters of the note that is being modified. This is done by incrementing the for loops counter by i+=2 and storing the next characters in noteLetter and noteNum. After the proper note duration has sorted out with either case of the if statement, we can actually play the note. To do so, we will use a sub-function called playNote that will play the correct frequency for the correct period of time.

removeWhiteSpace()

Now we will start defining the sub-functions of playLine(). The first of these is removeWhiteSpace(). The code for this function will simply consist of a for loop that loops through every character of the String. Any character that is not a space will be concatenated in a temporary String. The function will then return the temporary String. Here is what removeWhiteSpace() looks like:

                  String removeWhiteSpace(String input)
                  {
                    String temp = "";
                    for(int i = 0; i < input.length(); i++)
                    {
                      if(input[i] != ' ')
                     {
                      temp = temp+input[i];
                     }
                    }
                    return temp;
                  }
                

charToDigit()

The second sub-function of playLine() we will define is charToDigit(). This function is very short but requires some explanation. The function in its entirety is shown below.

                  int charToDigit(char character)
                  {
                    return character - '0';
                  }	
                

Although the function is very short, the operation it performs is odd. Subtracting two characters doesn't really make sense to a person, but it makes perfect sense to a computer. This is because every character has an associated numeric value. In most programming languages characters are assigned these values based on the ASCII standard. When you tell a computer to subtract two characters it is actually subtracting the ASCII value of those characters.

For example:

  • The ASCII value of 'A' is 65
  • The ASCII value of 'B' is 66
  • So 'A' - 'B'= 1

In our sketch the whole point of subtracting characters is to convert the char digits '0' through '9' to their integer equivalent. Unfortunately, their ASCII values do not match the number represented. Instead, ASCII maps the characters '0' through '9' to the values 48 through 57. This is the reason we must subtract '0' and cannot convert the digits value directly.

For example:

  • The ASCII value of '0' is 48
  • The ASCII value of '1' is 49
  • The ASCII value of '8' is 56
  • The ASCII value of '9' is 57

So:

  • '1' - '0' = 1
  • '8' - '0' = 8
  • '9' - '0' = 9

In terms of ASCII values that is:

  • 49 - 48 = 1
  • 56 - 48 = 8
  • 57 - 48 = 9

noteLengthToMs()

The third sub-function of playLine() we will define is noteLengthToMs(). The function essentially calculates the duration of a whole note in milliseconds based on the tempo, and then multiplies it by the note length. With the comments, this function is fairly self-explanatory.

					/*Converts tempo based note length (full note, 1/2 note, 1/4 note ect..) 
					to the notes play length in milliseconds*/
					int noteLengthToMs(double noteLen ,int tempo)
					{ 
					  //Convert tempo from notes per minute to notes per second
					  double notesPerSecond = tempo/60.0;
						
					  //Calculate the length of one whole note in milliseconds
					  int wholeNoteMs = 1000/notesPerSecond;
						
					 //Calculate the length of note (whole, 1/2, 1/4 ect) in milliseconds
					 return wholeNoteMs*noteLen;    
					} 
                

playNote()

The final sub-function of playLine() we must define is playNote(). The core of playNote() will be an ifelse tree. The ifelse will be used to determine whether the note being played is a pause, a sharp, or a regular note.

If the note is a pause, then we will simply delay for the pause's length. If the note is not a pause, then we will check to see if the note is a sharp using the predefined function isupper(). The function isupper() takes one char as an argument and returns true when that character is upper case. Once we have determined that we are dealing with a sharp note, we must obtain the corresponding frequency from the sharpOctaves array. To do so, we will need to calculate the index that corresponds to that note's frequency.

To get the array index for the sharp notes letter, let's assume we have a sub-function sharpToIndex() that solves our problem. sharpToIndex() will store the value it returns to the variable noteFreqIndex. Next, we need to calculate the index for the sharp note's octave. The index for a given position in an array is X-1, so to access the proper octave we must subtract one from the number we are given. The resulting number will be stored in the variable octaveIndex. Once we have both of the indices, we will use freq = sharpOctaves[octaveIndex][noteFreqIndex] to obtain the note's frequency. Now that the frequency has been obtained, we will use the old playFreq() to play the actual note.

The final possibility in the aforementioned ifelse tree is that a regular note needs to be played. In this case, the code will look very similar to the code used to play a sharp note. The primary difference will be that we will use the sub-function noteToIndex() to store the result to noteFreqIndex. Also, once we have both of the indices, we will use freq = octaves[octaveIndex][noteFreqIndex] to obtain the note's frequency. Overall, this function should look as follows.

					void playNote(char note, int noteOctaveNum ,int noteLengthMs)
					{  
					 int noteFreqIndex;
					 int octaveIndex;
					 double freq;
					  if(note == '-')
					  {
						//Play nothing
						 delay(noteLengthMs);
					  }
					  else if(isupper(note))
					  {
						//Convert the sharp note letter to an array index
						 noteFreqIndex = sharpToIndex(note);
						   
						//Convert octave number to an array index
						octaveIndex = noteOctaveNum - 1;
						  
						//Use indices to retrieve  corresponding frequency from the array
						freq = sharpOctaves[octaveIndex][noteFreqIndex];
						playFreq(freq, noteLengthMs);  
					  }
					  else
					  {
						//Convert the note letter to an array index
						noteFreqIndex = noteToIndex(note);
						  
						//Convert octave number to an array index
						octaveIndex = noteOctaveNum - 1;
							
						//Obtain note frequency
						freq = octaves[octaveIndex][noteFreqIndex]; 
						playFreq(freq, noteLengthMs); 
					  }
					}	

sharpToIndex()

With playNote() complete, we can begin writing its sub-functions. The first is going to be sharpToIndex(). This function uses a switch statement to convert the correct capital note letter (C,D,F,G, or A) to the correct array index (1,2,3,4). The switch statement's default case will play an error tone for 225 milliseconds if the note's character doesn't match any of the cases. This will be helpful for catching typos made when translating the sheet music of the song.

					int sharpToIndex(char note)
					{
					  switch(note)
					  {
						case 'C':
						  return 0;
						  break; 
						case 'D':
						  return 1;
						  break; 
						case 'F':
						  return 2;
						  break; 
						case 'G':
						  return 3;
						  break; 
						case 'A':
						  return 4;
						  break; 
						default:
						//Plays a error tone if none of the note letters match
						playFreq(6000, 225); 
					  }
					} 

noteToIndex()

The last function we need to write is noteToIndex(). It will be very similar to sharpToIndex() except it will check for lower case letters and have 6 possible indices it can return. Just as in sharpToIndex(), the default case will play an error tone for 225 milliseconds to catch any typos in the song.

					int sharpToIndex(char note)
					{
					  switch(note)
					  {
						case 'C':
						  return 0;
						  break; 
						case 'D':
						  return 1;
						  break; 
						case 'F':
						  return 2;
						  break; 
						case 'G':
						  return 3;
						  break; 
						case 'A':
						  return 4;
						  break; 
						default:
						//Plays a error tone if none of the note letters match
						playFreq(6000, 225); 
					  }
					} 	

Putting it all Together

With all the functions written, we can finally combine them into one sketch, as shown below. Be aware that in the setup() function, just below the String fullMelody definition, there is an additional line.

					/*calculate the number of lines in melody array
					  THIS MUST BE DONE IN SAME SCOPE AS ARRAY DEFENITON*/
					  int numLines = sizeof(fullMelody)/sizeof(fullMelody[0]);	

This bit of code is used to automatically calculate the number of lines (elements) in our fullMelody array. For details about calculating the number of elements in an array, follow the orange tab near the top of the page.

				//Store all note frequencies for each octave in a 2 dimensional array
				 double octaves[7][7] = 
				 {  
				  //Octave1 Notes:      c1      d1      e1      f1      g1      a1      b1
				  /* 1st octave */   {32.703, 36.708, 41.203, 43.654, 48.990, 55.000, 61.735},
				  
				  //Octave2 Notes:      c2      d2      e2      f2      g2       a2       b2
				  /* 2nd octave */   {65.406, 73.416, 82.407, 87.307, 97.999, 110.000, 123.470},
					
				  //Octave3 Notes:       c3       d3       e3       f3       g3       a3       b3 
				  /* 3rd octave */   {130.810, 146.830, 164.810, 174.610, 196.000, 220.000, 246.940},
					 
				  //Octave4 Notes:       c4       d4       e4       f4       g4       a4       b4
				  /* 4th octave */   {261.630, 293.660, 329.630, 349.230, 392.000, 440.000, 493.880},
					
				  //Octave5 Notes:       c5       d5       e5       f5       g5       a5       b5
				  /* 5th octave */   {523.250, 587.330, 659.250, 698.460, 783.990, 880.000, 987.770},
				  
				  //Octave6 Notes:        c6        d6        e6        f6        g6        a6        b6  
				  /* 6th octave */   {1046.500, 1174.700, 1318.500, 1396.900, 1568.000, 1760.000, 1979.500},
				  
				  //Octave7 Notes:        c7        d7        e7        f7        g7        a7        b7  
				  /* 7th octave */   {2093.000, 2349.300, 2637.000, 2793.800, 3136.000, 3520.000, 3951.100}
				 };
				   
				  
				//Store the sharp note frequencies for each octave in a 2 dimensional array
				double sharpOctaves[7][5] =
				{
				 //Octave1 Sharps:     C1      D1      F1      G1      A1 
				 /* 1st octave */   {34.648, 38.891, 46.249, 51.913, 58.270},
				   
				 //Octave2 Sharps:     C2      D2      F2       G2       A2 
				 /* 2nd octave */   {69.296, 77.782, 92.499, 103.830, 116.540},
				   
				 //Octave3 Sharps:      C3       D3       F3       G3       A3 
				 /* 3rd octave */   {138.590, 155.560, 185.000, 207.650, 233.080},
				   
				 //Octave4 Sharps:      C4       D4       F4       G4       A4 
				 /* 4th octave */   {227.180, 311.130, 369.990, 415.300, 466.160},
				   
				 //Octave5 Sharps:      C5       D5       F5       G5       A5 
				 /* 5th octave */   {554.370, 622.250, 739.990, 830.610, 932.330},
				   
				 //Octave6 Sharps:       C6        D6        F6        G6        A6 
				 /* 6th octave */   {1108.700, 1244.500, 1480.000, 1661.200, 1864.700},
				   
				 //Octave7 Sharps:       C7        D7        F7        G7        A7 
				 /* 7th octave */   {2217.500, 2489.000, 2960.000, 3322.400, 3729.300}  
				};               
				  
				int buzzerPin = 4;
				 
				void setup()
				{
				  pinMode(buzzerPin,OUTPUT);
					
				  // the tempo is 100 whole notes per minute
				  int tempo = 100; 
					 
				  //the typical length notes in the song are quarter notes
				  double typicalNoteLength = 0.25; 
					   
				  //store the modified note lengths for the song
				  double newNoteLength[ ] = {0.0167, 0.3333};
				   
				   
				  String fullMelody[ ] =
				  {
					"e5!0-=e5-=e5   -=c5e5-=  g5-=-=-= g4-=-=-=",
					"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
					"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
					"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
					"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
					"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
					"-=-=g5F5 f5D5-=e5 -=c6-=c6 c6-=-=-=",
					"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
					"-=-=D5-= -=d5-=-= c5-=-=-= -=-=-=-=",
					"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
					"-=-=g5F5 f5D5-=e5 -=c6-=c6 c6-=-=-=",
					"-=-=g5F5 f5D5-=e5 -=G4a4c5 -=a4c5d5",
					"-=-=D5-= -=d5-=-= c5-=-=-= -=-=-=-=",
					"c5c5-=c5 -=c5d5-= e5c5-=a4 g4-=-=-=",
					"c5c5-=c5 -=c5d5e5 -=-=-=-= -=-=-=-=",
					"c5c5-=c5 -=c5d5-= e5c5-=a4 g4-=-=-=",
					"e5e5-=e5 -=c5e5-= g5-=-=-= g4-=-=-=",
					"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
					"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
					"c5-=-=g4 -=-=e4-= -=a4-=b4 -=A4a4-=",
					"!1g4!1e5!1g5 a5-=f5g5 -=e5-=c5 d5b4-=-=",
					"e5c5-=g4 -=-=G4-= a4f5-=f5 a4-=-=-=",
					"!1b4!1a5!1a5 !1a5!1g5!1f5 e5c5-=a4 g4-=-=-=",
					"e5c5-=g4 -=-=G4-= a4f5-=f5 a4-=-=-=",
					"b4f5-=f5 !1f5!1e5!1d5 c5e4-=e4 c4-=-=-="
				  };
				   
				  /*calculate the number of lines in melody array
				  THIS MUST BE DONE IN SAME SCOPE AS ARRAY DEFENITON*/
				  int numLines = sizeof(fullMelody)/sizeof(fullMelody[0]);
				  
				 playMelody(fullMelody, typicalNoteLength, tempo, newNoteLength, numLines);
				  
				}
				  
				void playMelody(String song[ ], double typicalNoteLength, int tempo, double newNoteLength[ ], int numLines)
				{
					//Loop through and play each line of song
					 for(int i = 0; i < numLines; i++)
					 {
					   playLine(song[i], typicalNoteLength, tempo, newNoteLength);
					 }
				}
				 
				void playLine(String songLine, double typicalNoteLength, int tempo, double newNoteLength[])
				{
				  //remove spaces from song
				  String lineNotes = removeWhiteSpace(songLine);
					  
				   
				  for( int i = 0; i < lineNotes.length(); i+=2)
				  {    
					 //get the current note to be played
					  char noteLetter = lineNotes[i];
					   
					  //get octave number of note to be played
					  char noteNum = lineNotes[i+1];
				 
					 //convert noteDigit from char to int so it can be used as an  array index
					 int noteOctaveNum = charToDigit(noteNum);
					 
					 //declare noteLengthMs before if statement 
					 int noteLengthMs;
					  
					  
					 //Check to see if the typical note length or a modified note length should be used to play the next note
					 if(noteLetter == '!')//use modified note length 
					 {
						
					   //Calculate the notes length in milliseconds (for modified note lengths)
					   noteLengthMs = noteLengthToMs(newNoteLength[noteOctaveNum], tempo);
					   
					   i+=2;//increment index to grab the note following the "!0" length modifier
					   noteLetter = lineNotes[i]; 
						
					  //get octave number of note to be played after "!0" length modifier
					  noteNum = lineNotes[i+1];
					  //convert noteDigit from char to int so it can be used as an  array index
					  noteOctaveNum = charToDigit(noteNum);
					  
					 }
					 else//use typical note length 
					 {
					  //Calculate the notes length in milliseconds (for typical note lengths)
					  noteLengthMs = noteLengthToMs(typicalNoteLength, tempo);
					 }
				   
					 playNote(noteLetter, noteOctaveNum ,noteLengthMs);
				  }
				}
				 
				String removeWhiteSpace(String input)
				{
				  String temp = "";
				  for(int i = 0; i < input.length(); i++)
				  {
					if(input[i] != ' ')
				   {
					temp = temp+input[i];
				   } 
				  }
				  return temp;
				}
				 
				int charToDigit(char character)
				{
				  return character - '0';
				}
				 
				/*Converts tempo based note length (full note, 1/2 note, 1/4 note, ect..) 
				to the notes play length in milliseconds*/
				int noteLengthToMs(double noteLen ,int tempo)
				{ 
				  //Convert tempo from notes per minute to notes per second
				  double notesPerSecond = tempo/60.0;
				   
				  //Calculate the length of one whole note in milliseconds
				  int wholeNoteMs = 1000/notesPerSecond;
				   
				 //Calculate the length of note (whole, 1/2, 1/4 ect) in milliseconds
				 return wholeNoteMs*noteLen;    
				} 
				 
				void playNote(char note, int noteOctaveNum ,int noteLengthMs)
				{  
				 int noteFreqIndex;
				 int octaveIndex;
				 double freq;
				  if(note == '-')
				  {
					//Play nothing
					 delay(noteLengthMs);
				  }
				  else if(isupper(note))
				  {
					//Convert the sharp note letter to an array index
					 noteFreqIndex = sharpToIndex(note);
					  
					//Convert octave number to an array index
					octaveIndex = noteOctaveNum - 1;
					 
					//Use indices to retrieve  corresponding frequency from the array
					freq = sharpOctaves[octaveIndex][noteFreqIndex];
					playFreq(freq, noteLengthMs);  
				  }
				  else
				  {
					//Convert the note letter to an array index
					noteFreqIndex = noteToIndex(note);
					 
					//Convert octave number to an array index
					octaveIndex = noteOctaveNum - 1;
					   
					//Obtain note frequency
					freq = octaves[octaveIndex][noteFreqIndex]; 
					playFreq(freq, noteLengthMs); 
				  }
				}
				 
				int sharpToIndex(char note)
				{
				  switch(note)
				  {
					case 'C':
					  return 0;
					  break; 
					case 'D':
					  return 1;
					  break; 
					case 'F':
					  return 2;
					  break; 
					case 'G':
					  return 3;
					  break; 
					case 'A':
					  return 4;
					  break; 
					default:
					//Plays a error tone if none of the note letters match
					playFreq(6000, 225); 
				  }
				} 
				 
				int noteToIndex(char note)
				{
				  switch(note)
				  {
					case 'c':
					  return 0;
					  break; 
					case 'd':
					  return 1;
					  break; 
					case 'e':
					  return 2;
					  break; 
					case 'f':
					  return 3;
					  break; 
					case 'g':
					  return 4;
					  break; 
					case 'a':
					  return 5;
					  break; 
					case 'b':
					  return 6;
					  break; 
					default:
					  //Plays a error tone if none of the note letters match
					  playFreq(6000, 225); 
				  }
				}

				void playFreq(double freqHz, int durationMs)
				{
					//Calculate the period in microseconds
					int periodMicro = int((1/freqHz)*1000000);
					int halfPeriod = periodMicro/2;
					   
					//store start time
					int startTime = millis();
					   
					//(millis() - startTime) is elapsed play time
					while ((millis() - startTime) < durationMs)    
					{
						digitalWrite(buzzerPin, HIGH);
						delayMicroseconds(halfPeriod);
						digitalWrite(buzzerPin, LOW);
						delayMicroseconds(halfPeriod);
					}
					
				}
				   
				void loop() 
				{
					 
				}	

Upon uploading the sketch, the melody should play correctly and last for about one minute.

Summary:

In this project, we generated sound using a piezoelectric buzzer. We explored how to generate different frequencies by cycling a digital pin from HIGH to LOW at different rates. We then examined some of the fundamentals of music theory and how they can help us play music with our buzzer.

Core Concepts:
  • Buzzers
  • Piezoelectric properties of materials
  • Period and frequency of waves
  • Common waveforms
  • Frequencies of musical notes
  • Musical octaves
  • Basics of music theory
  • Defining and manipulating simple arrays
  • Defining and manipulating multidimensional arrays
  • Calculating the number of elements in an array
  • Character ASCII values
Functions Introduced:
  • sizeof()
  • isupper()

  • Other product and company names mentioned herein are trademarks or trade names of their respective companies. © 2014 Digilent Inc. All rights reserved.
  • Circuit and breadboard images were created using Fritzing.