Buzzer advanced operation

Piezoelectric music [VIDEO]

Platform Test Tools SAM3U2 Firmware nRF51422 Firmware Software
MPG1
MPG2
USB RS-232 Converter MPG User Code
Heart and Soul
AP2 Emulator TeraTerm

Prerequisite Modules

Description

In this module we build an application that will let us encode notes from sheet music to an array. The purpose is to show how to incorporate both audio and visual output in a more complicated program. Though you need to know very little about music for this exercise, the mathematical and sensory beauty of music might drive you to at least look at the basics of reading sheet music HERE. What you must understand is that music boils down to frequencies (notes) and the combination and duration of those notes.

To enable a reasonable solution to coding music, the application must allow us to define a note and the duration for which that note is played. These will be captured in an array and then sequenced out to the buzzer. We can code one channel for the dot matrix LCD board and two channels for the ASCII LCD board. Our goal is to write “Heart and Soul.” Since the dual buzzers on the ASCII development board make this project substantially more interesting this module will focus just on that board. Our implementation is based on a simple version of the song shown here:

HeartAndSoulMusicLowRes

Coding the Sound

All code is written in UserAppSM_Idle(). Start with the latest Master branch and take a moment to open and examine music.h in the “Application>Include” section of the project workspace. This file contains several octaves of note definitions that you will use in your song. It also has note duration and some other definitions that are needed to create a song. Each note must have a frequency, a duration, and a type. There are short-hand notations for all of the values to minimize the size of the array text and also make it easy to line up note values with their corresponding duration. A sample including 1 octave is shown below. “Middle C” is the de-facto reference on a piano keyboard. It happens to have a frequency of 261.6Hz which we round up to 262Hz.

/* Note lengths */
#define MEASURE_TIME              (u16)2000  /* Time in ms for 1 measure (1 full note) - should be divisible by 16 */
#define FULL_NOTE                 (u16)(MEASURE_TIME)
#define HALF_NOTE                 (u16)(MEASURE_TIME / 2)
#define QUARTER_NOTE              (u16)(MEASURE_TIME / 4)
#define EIGHTH_NOTE               (u16)(MEASURE_TIME / 8)
#define SIXTEENTH_NOTE            (u16)(MEASURE_TIME / 16)

#define FN                        FULL_NOTE                 
#define HN                        HALF_NOTE                 
#define QN                        QUARTER_NOTE              
#define EN                        EIGHTH_NOTE               
#define SN                        SIXTEENTH_NOTE            

/* Note length adjustments */
#define REGULAR_NOTE_ADJUSTMENT   (u16)50
#define STACCATO_NOTE_TIME        (u16)75
#define HOLD_NOTE_ADJUSTMENT      (u16)0

#define RT                        REGULAR_NOTE_ADJUSTMENT
#define ST                        STACCATO_NOTE_TIME        
#define HT                        HOLD_NOTE_ADJUSTMENT            

/* Musical note definitions */
#define NOTE_C3                   (u16)131
#define NOTE_C3_SHARP             (u16)139
#define NOTE_D3_FLAT              (u16)139
#define NOTE_D3                   (u16)147
#define NOTE_D3_SHARP             (u16)156
#define NOTE_E3_FLAT              (u16)156
#define NOTE_E3                   (u16)165
#define NOTE_F3                   (u16)175
#define NOTE_F3_SHARP             (u16)185
#define NOTE_G3_FLAT              (u16)185
#define NOTE_G3                   (u16)196
#define NOTE_G3_SHARP             (u16)208
#define NOTE_A3                   (u16)220
#define NOTE_A3_SHARP             (u16)233
#define NOTE_B3                   (u16)245
#define NOTE_C4                   (u16)262  /* Middle C */
...
/* Musical note definitions - short hand */
#define C3                   (u32)NOTE_C3
#define C3S                  (u32)NOTE_C3_SHARP
#define D3                   (u32)NOTE_D3
#define D3S                  (u32)NOTE_D3_SHARP
#define E3                   (u32)NOTE_E3
#define F3                   (u32)NOTE_F3
#define F3S                  (u32)NOTE_F3_SHARP
#define G3                   (u32)NOTE_G3
#define G3S                  (u32)NOTE_G3_SHARP
#define A3                   (u32)NOTE_A3
#define A3S                  (u32)NOTE_A3_SHARP
#define B3                   (u32)NOTE_B3
#define C4                   (u32)NOTE_C4  /* Middle C */

Start by defining three arrays corresponding to the frequency, duration and type of the notes to be played. The word “Right” refers to the “Right hand” that these notes would be played with if on a piano.

  static u16 au16NotesRight[]    = {};
  static u16 au16DurationRight[] = {};
  static u16 au16NoteTypeRight[] = {};

Copy in the three sets of data that make up the melody of the song.

Notes:

F5, F5, F5, F5, F5, E5, D5, E5, F5, G5, A5, A5, A5, A5, A5, G5, F5, G5, A5, A5S, C6, F5, F5, D6, C6, A5S, A5, G5, F5, NO, NO

Duration of the notes:

QN, QN, HN, EN, EN, EN, EN, EN, EN, QN, QN, QN, HN, EN, EN, EN, EN, EN, EN, QN,  HN, HN, EN, EN, EN, EN,  QN, QN, HN, HN, FN

Type of each note:

RT, RT, HT, RT, RT, RT, RT, RT, RT, RT, RT, RT, HT, RT, RT, RT, RT, RT, RT, RT,  RT, HT, RT, RT, RT, RT,  RT, RT, RT, HT, HT

You can see why it is important that we gave short symbol names to all of the elements so that they do not extend too far on the screen but also line up visually (notice the extra spaces added to keep things lined up).

The algorithm must index the arrays to set the frequency and then play that note for the duration corresponding to the note. Two additional variables will be used to track whether the note is playing given its current interval and the duration (if any) of silence. In most cases, there is a brief period of silence between notes — if not, the notes “run in to” each other. Sometimes we want this, other times we do not. This is determined by the NoteType. To check quickly if a note is currently active, a boolean is TRUE when the note is active. We will also need a working variable for the the index. Therefore, six variables are needed:

  static u8 u8IndexRight = 0;
  static u32 u32RightTimer = 0;
  static u16 u16CurrentDurationRight = 0;
  static u16 u16NoteSilentDurationRight = 0;
  static bool bNoteActiveRight = TRUE;

  u8 u8CurrentIndex;

The algorithm we will code is quite simple especially in pseudo code:

if ( the current note duration is over )
{
  Get the current time and note index;
  if ( the note is currently active )
  {
    check (RT, ST and HT notes)
    {
      Calculate the "on" and "silent" duration of the current note based on its full duration - the note type adjustment;
    }
    Set the note frequency and turn the audio on or handle the special case of a "NO" note
  } 
  else
  {
     Turn off the buzzer for this period;
     Set the CurrentDuration = SilentDuration;
     Flag note active.
  }
}

Implement the pseudo code piece by piece. First add in the basic structure which is the main if() frame that runs when a note has finished playing. The code must determine if it is time to start a new note or if its at the silent part of the current note. If a new note is to be played, the note type (RT, ST, or HT) must be handled and then the buzzer set up).

  if(IsTimeUp(&u32RightTimer, (u32)u16CurrentDurationRight))
  {
    u32RightTimer = G_u32SystemTime1ms;
    u8CurrentIndex = u8IndexRight;
    
    /* Set up to play current note */
    if(bNoteActiveNextRight)
    {
      if(au16NoteTypeRight[u8CurrentIndex] == RT)
      {
      } /* end RT case */
    
      else if(au16NoteTypeRight[u8CurrentIndex] == ST)
      {
      } /* end ST case */

      else if(au16NoteTypeRight[u8CurrentIndex] == HT)
      {
      } /* end HT case */

      /* Set the buzzer frequency for the note (handle NO special case) */
      if(au16NotesRight[u8CurrentIndex] != NO)
      {
        PWMAudioSetFrequency(BUZZER1, au16NotesRight[u8CurrentIndex]);
        PWMAudioOn(BUZZER1);
      }
      else
      {                
        PWMAudioOff(BUZZER1);
      }
    } /* end if(bNoteActiveNextRight) */
    else
    {
      /* No active note */
    }
  } /* end if(IsTimeUp(&u32RightTimer, (u32)u16CurrentDurationRight)) */

The note adjustments are coded as follows:

  • RT: Regular notes have a short silent period (REGULAR_NOTE_ADJUSTMENT). So u16CurrentDurationRight is au16DurationRight[u8CurrentIndex] – REGULAR_NOTE_ADJUSTMENT and u16NoteSilentDurationRight is REGULAR_NOTE_ADJUSTMENT. Since there is a silent period after the note is played, bNoteActiveNextRight is FALSE.
  • ST: Staccatto notes are brief so they have a fixed duration of STACCATO_NOTE_TIME. That leaves au16DurationRight[u8CurrentIndex] – STACCATO_NOTE_TIME for u16NoteSilentDurationRight. Since there is a silent period after the note is played, bNoteActiveNextRight is FALSE.
  • HT: Hold notes do not have any silent period, so u16CurrentDurationRight is the full duration, u16NoteSilentDurationRight is 0 and the next event is the next note so bNoteActiveNextRight is TRUE. We must therefore increment the note index here, too.
      if(au16NoteTypeRight[u8CurrentIndex] == RT)
      {
        u16CurrentDurationRight = au16DurationRight[u8CurrentIndex] - REGULAR_NOTE_ADJUSTMENT;
        u16NoteSilentDurationRight = REGULAR_NOTE_ADJUSTMENT;
        bNoteActiveNextRight = FALSE;
      } /* end RT case */
    
      else if(au16NoteTypeRight[u8CurrentIndex] == ST)
      {
        u16CurrentDurationRight = STACCATO_NOTE_TIME;
        u16NoteSilentDurationRight = au16DurationRight[u8CurrentIndex] - STACCATO_NOTE_TIME;
        bNoteActiveNextRight = FALSE;
      } /* end ST case */

      else if(au16NoteTypeRight[u8CurrentIndex] == HT)
      {
        u16CurrentDurationRight = au16DurationRight[u8CurrentIndex];
        u16NoteSilentDurationRight = 0;
        bNoteActiveNextRight = TRUE;

        u8IndexRight++;
        if(u8IndexRight == sizeof(au16NotesRight) / sizeof(u16) )
        {
          u8IndexRight = 0;
        }
      } /* end HT case */

Once the duration values have been set, we can make the tone active by setting the frequency and activating the buzzer. A “NO” note (i.e. no sound is played) is a special case so it is handled separately and just turns off the buzzer. It still must be treated like any other note to fit properly in the algorithm.

      /* Set the buzzer frequency and LED for the note (handle NO special case) */
      if(au16NotesRight[u8CurrentIndex] != NO)
      {
        PWMAudioSetFrequency(BUZZER1, au16NotesRight[u8CurrentIndex]);
        PWMAudioOn(BUZZER1);
      }
      else
      {                
        PWMAudioOff(BUZZER1);
      }
    } /* end if(bNoteActiveNextRight) */

We still have to handle the “non-active” note case which is the silent part of the duration. The code above was conditioned on bNoteActiveNextRight being TRUE, so if it is FALSE we have to update the parameters to produce the silent period and get ready for the next note. If you are copying this code, the final closing brace for the if(IsTimeUp) loop is included here.

    else
    {
      /* No active note */
      PWMAudioOff(BUZZER1);
      u32RightTimer = G_u32SystemTime1ms;
      u16CurrentDurationRight = u16NoteSilentDurationRight;
      bNoteActiveNextRight = TRUE;
 
      u8IndexRight++;
      if(u8IndexRight == sizeof(au16NotesRight) / sizeof(u16) )
      {
        u8IndexRight = 0;
      }
    } /* end else if(bNoteActiveNextRight) */
  } /* end if(IsTimeUp(&u32RightTimer, (u32)u16CurrentDurationRight)) */

Build the code and run it — you should hear the melody of the song playing. Adding in the “left hand” music is identical to the right hand with all the variables named “left” instead of “right.” The variables and music are provided here:

  static u16 au16NotesLeft[]    = {F4, F4, A4, A4, D4, D4, F4, F4, A3S, A3S, D4, D4, C4, C4, E4, E4};
  static u16 au16DurationLeft[] = {EN, EN, EN, EN, EN, EN, EN, EN, EN,  EN,  EN, EN, EN, EN, EN, EN};
  static u16 au16NoteTypeLeft[] = {RT, RT, RT, RT, RT, RT, RT, RT, RT,  RT,  RT, RT, RT, RT, RT, RT};
  static u8 u8IndexLeft = 0;
  static u32 u32LeftTimer = 0;
  static u16 u16CurrentDurationLeft = 0;
  static u16 u16NoteSilentDurationLeft = 0;
  static bool bNoteActiveNextLeft = TRUE;

The music is shorter because it is a repetative sequence. We do not need to code anything that repeats exactly, though care must be taken to ensure that timing still works out precisely so that the right hand and left hand do not get out of sync. The code is shown here without any explanation since it is identical to what was detailed above.

 /* Left Hand */
  if(IsTimeUp(&u32LeftTimer, (u32)u16CurrentDurationLeft))
  {
    u32LeftTimer = G_u32SystemTime1ms;
    u8CurrentIndex = u8IndexLeft;
    
    /* Set up to play current note */
    if(bNoteActiveNextLeft)
    {
      if(au16NoteTypeLeft[u8CurrentIndex] == RT)
      {
        u16CurrentDurationLeft = au16DurationLeft[u8CurrentIndex] - REGULAR_NOTE_ADJUSTMENT;
        u16NoteSilentDurationLeft = REGULAR_NOTE_ADJUSTMENT;
        bNoteActiveNextLeft = FALSE;
      }
    
      else if(au16NoteTypeLeft[u8CurrentIndex] == ST)
      {
        u16CurrentDurationLeft = STACCATO_NOTE_TIME;
        u16NoteSilentDurationLeft = au16DurationLeft[u8CurrentIndex] - STACCATO_NOTE_TIME;
        bNoteActiveNextLeft = FALSE;
      }

      else if(au16NoteTypeLeft[u8CurrentIndex] == HT)
      {
        u16CurrentDurationLeft = au16DurationLeft[u8CurrentIndex];
        u16NoteSilentDurationLeft = 0;
        bNoteActiveNextLeft = TRUE;

        u8IndexLeft++;
        if(u8IndexLeft == sizeof(au16NotesLeft) / sizeof(u16) )
        {
          u8IndexLeft = 0;
        }
      }

      /* Set the buzzer frequency for the note (handle NO special case) */
      if(au16NotesLeft[u8CurrentIndex] != NO)
      {
        PWMAudioSetFrequency(BUZZER2, au16NotesLeft[u8CurrentIndex]);
        PWMAudioOn(BUZZER2);
      }
      else
      {                
        PWMAudioOff(BUZZER2);
      }
    }
    else
    {
      PWMAudioOff(BUZZER2);
      u32LeftTimer = G_u32SystemTime1ms;
      u16CurrentDurationLeft = u16NoteSilentDurationLeft;
      bNoteActiveNextLeft = TRUE;
      
      u8IndexLeft++;
      if(u8IndexLeft == sizeof(au16NotesLeft) / sizeof(u16) )
      {
        u8IndexLeft = 0;
      }
    } /* end else if(bNoteActiveNextLeft) */
  } /* end if(IsTimeUp(&u32LeftTimer, (u32)u16CurrentDurationLeft)) */

Build the code and notice how much better the song sounds with the two different channels working together. Your ear is the mixer that combines the sounds. This is what happens on a regular speaker, but mixing audio on a piezoelectric buzzer where we only have a single driving amplitude signal is simply not possible. A great project would be an eight-channel piezoelectric board so you could add up to six additional channels with harmonies, beats, and anything else you could imagine! Or, take the next step to run analog audio as we will do in a future module.

Coding the Lights

While the sound is cool, we can also add some visual feedback to the system by turning on LEDs in sequence to the music. It so happens we have just enough lights for the number of notes used in the song. We will code them like this:

/* LED mapping for right hand:
D5  WHITE
E5  PURPLE
F5  BLUE
G5  CYAN
A5  GREEN
A5S YELLOW
C6  ORANGE
D6  RED

This allows us to use a simple switch statement to select the LED for the note that is playing, rather than coding another array to hold LED selections that correspond to the notes. While there is nothing stopping you from doing that, we will use the switch statement to add some variety to the example. Update the section of code on the right hand where the buzzer is turned on or off depending on the note. The switch statement is conditioned on au16NotesRight[u8CurrentIndex] to set the correct LED on. Of course code is required to turn the LED off when no note is playing. A neat effect would be to have the LED fade out.

      /* Set the buzzer frequency and LED for the note (handle NO special case) */
      if(au16NotesRight[u8CurrentIndex] != NO)
      {
        PWMAudioSetFrequency(BUZZER1, au16NotesRight[u8CurrentIndex]);
        PWMAudioOn(BUZZER1);
        
        /* LED control */
        switch(au16NotesRight[u8CurrentIndex])
        {
          case D5:
            LedOn(WHITE);
            break;
            
          case E5:
            LedOn(PURPLE);
            break;
            
          case F5:
            LedOn(BLUE);
            break;
            
          case G5:
            LedOn(CYAN);
            break;
            
          case A5:
            LedOn(GREEN);
            break;
            
          case A5S:
            LedOn(YELLOW);
            break;
            
          case C6:
            LedOn(ORANGE);
            break;
            
          case D6:
            LedOn(RED);
            break;
            
          default:
            break;
            
        } /* end switch */
      }
      else
      {                
        PWMAudioOff(BUZZER1);
        LedOff(WHITE);
        LedOff(PURPLE);
        LedOff(BLUE);
        LedOff(CYAN);
        LedOff(GREEN);
        LedOff(YELLOW);
        LedOff(ORANGE);
        LedOff(RED);
      }

Turning off the LEDs must also be done in the main “silent” state.

    else
    {
      /* No active note */
      PWMAudioOff(BUZZER1);
      u32RightTimer = G_u32SystemTime1ms;
      u16CurrentDurationRight = u16NoteSilentDurationRight;
      bNoteActiveNextRight = TRUE;
 
      LedOff(WHITE);
      LedOff(PURPLE);
      LedOff(BLUE);
      LedOff(CYAN);
      LedOff(GREEN);
      LedOff(YELLOW);
      LedOff(ORANGE);
      LedOff(RED);

      u8IndexRight++;
      if(u8IndexRight == sizeof(au16NotesRight) / sizeof(u16) )
      {
        u8IndexRight = 0;
      }
    } /* end else if(bNoteActiveNextRight) */

In the example code online we added backlight control that changes the LED backlight with the cycles of the left hand. You can find that code in the solution on GitHub. The last thing we will show here is adding the title to the LCD in UserAppInitialize()

void UserAppInitialize(void)
{
  u8 au8SongTitle[] = "Heart and Soul";

  LCDCommand(LCD_CLEAR_CMD);
  LCDMessage(LINE1_START_ADDR, au8SongTitle);

One of the things to be very careful with in a program like this is that the overall timing does not skew over time. Even a delay of a few microseconds every note will add up to be a noticeable delay over time. During development we had a slight timing error between the right and left algorithms that was noticeable even after just five repetitions of the song. The key is to sequence everything to a stable reference and then of course make sure that all calculations are balanced. Using the system tick to control timing is essential because regardless of what else is happening in the system, the system tick period is always accurate. Even though processing each note takes slightly different amounts of time depending on what type of note it is, the error created by that processing is zeroed every loop iteration so it is effectively 0.

As you code more embedded firmware, understanding and managing timing issues will be paramount to a successful system. The consequences of not doing so could be much worse than a song sounding bad!

[STATUS: RELEASED. LAST UPDATE: 2016-MAR-07]

*** ENGENUICS WILL NOT BE ABLE TO SHIP PRODUCTS BETWEEN MAY 26 AND JUNE 26, 2017 ***