Theremin 2

In this section we are going to add several enhancements to our theremin. We’ll start with the LCD and push button controls. After that we’re going to calibrate the frequency servo, and then add the code to play a song in demo mode.

Let’s start with the LCD, there really isn’t much new here. I kept the LCD functionality as simple as possible. There are only two possible things to display on the screen. The first screen says ‘Press SEL for demo’, and displays a song name. The second simply displays ‘Playing’ and then the name of the song it is playing.

There are two buttons, ‘next’ and ‘select’. Pressing ‘next’ will allow you to cycle through the different songs that can be played as a demonstration. Pressing select will start the demo. This is pretty simple, the push buttons are used to trigger interrupts on digital input ports 50 and 51, just like was done with the simple dice game in the LCD tutorial. First we do some setup for the LCD and interrupts.

attachInterrupt(btnNextPin, BtnNext, CHANGE);
attachInterrupt(btnSelectPin, BtnSelect, CHANGE);
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
// Print a message to the LCD.
displayLCD("Press Sel 4Demo", Songs[iCurrSong].name );

If you recall the LCD tutorial, the first if statement is checking the time since the last interrupt to correct for switch bounce. When the next button is pressed, we need to increment iCurrSong which is a global variable (its an array index) that keeps track of the current song. The modulus operator (%) returns the remainder when iCurrSong is divided by numSongs, so if there are 4 songs, iCurrSong will cycle through the values 0, 1, 2, 3, .. 0, 1, 2, 3, .. 0, 1, 2 , 3.

In the last section we added some code that was for calibrating our volume level control. We also need to do this for the frequency control. I started by doing a little experiment with calls to servo.write(). I tried various angles until I found angles that looked and felt good, these angles were 26° and 154°, I found it is better to have a large range of movement to make it easier to play notes that are fairly close to one another. Then using those values and servo.write(), I used the serial monitor to see what input value is read by the Arduino. At 26° the input was 214, and at 154° it was 594. You have to remember that using a hacked servo as a potentiometer we don’t get the ideal range of 0 to 3.3 V that’s why we’re not seeing the whole range of 0 to 1024, but this is more than enough resolution for our purposes. You should test these values yourself, they may be different from mine. ( the drawing is not to scale, the range of movement is ~ 128°)

We need seven new variables to accomplish the calibration for the frequency servo

In the loop() section, we read the input and convert it to ulInput which is used to obtain the phase increment for a corresponding note (more on that in just a second). To ensure our value does not go below the smallest input we will accept, we call the max() function with analogRead(0) and fMinInput as arguments. To ensure that we don’t exceed the largest value we want to accept, we call the min() function with the call to max() and fMaxInput as arguments. At this point we have a valid input between fMinInput and fMaxInput. Next we need to rescale our input to correspond to range of minNote to maxNote. Think of it like this, we have 128 degrees of movement that change the frequency, if you only want to play 10 notes, then you’ll have to move the servo 12.8 deg to get a new note, if you want to play 50 notes, then its 2.56 deg per note, we are scaling our input to range of notes that we chosen to play as indicated by fMinNote and fMaxNote. You can change these settings so that it is comfortable for whatever you want to play, here is a video playing 50 notes over 128 degrees.

The last thing we need to do is add code that will play one of our songs as a demonstration.First we need to discuss how a song is represented, I wanted to make this as simple as possible and since a theremin has two inputs volume and pitch, we will need both of those parameters, the only other thing that will need is how long a note is played. So the data format we need can be a structure with three members, an integer representing the note to be played, and integer representing how long that note should be played, and an integer representing the volume that that note should be played at. I called this structure ‘songData’

struct songData {
int note;
int duration;
int volume;
};

When we actually transcribe a song every note needs to be a multiple of 20 ms, we can initialize a songData array like this

This program is getting kinda long, especially now that we are going to want to add songs. It is time to add a header file, where we can store some of our data types and song data. To add a header file you need to make your own library, I covered this in the Tone Generator tutorial. The Arduino site has a tutorial as well. So far whenever we have talked about notes, we were in effect talking about integers. Now that we want to specify a song we should make some #define statements so that we can write notes in a more human readable form, the header file is the place to do this e.g.

#define NOTE_DS4 63
#define NOTE_E4 64
....

The header file also contains the definition of our first song, “Somewhere over the Rainbow”, and our Song and SongData type definitions.

How do we actually play a song? First we need to take a detailed look at how the servo position gets converted into a musical note. A song is played is in ‘reverse’ order, by reverse order I mean we are going to specify a sequence of notes to be played with corresponding duration and volume. That sequence gets converted into an integer that represents an angle which is then passed to servo.write(), then the servo will move to that position and a note will be played, a way to think of this is that the main loop and interrupt routine are not aware that a song is being played, within the main loop() and Timer interrupt there is no way to tell if a person is moving the paddles. ( actually you could check a global variable, but I think its easier to understand how things work if you think about it this way). The limiting factor as to how fast we can play a song is with how fast the servo can respond, and as we discussed before we control a servo by writing a pulse every 20 ms. Therefore the shortest duration of note that we can play is 20 ms, this is fast enough. The way that we actually implement the song playing is to add another timer interrupt ( lets call it the song interrupt), this timer interrupt runs at a much lower rate than the sampling interrupt, it needs to occur once every 20 ms, to match to servo pulse train.

The way it works is that there is a global state variable named playingSong, every time the song interrupt occurs it checks if playingSong is true. If playingSong is true, the note and volume that needs to be played is obtained from the global Songs[] array, and the appropriate angle for the frequency and volume is written to the corresponding servo. The duration an individual note is played is controlled by counting the number of interrupts since the note last changed. We also need some code to check when a song is finished.

// this is the interrupt handler that is called every 20 mS (i.e. time between pulses sent to a servo), its job is to play a demo of a songvoid TC7_Handler()
{
// We need to get the status to clear it and allow the interrupt to fire again
TC_GetStatus(TC2, 1);
// handle servo output if we are in song playing modeif( playingSong )
{
fAngle = ((float)(Songs[iCurrSong].data[iNote].note - minNote) / (float)(maxNote - minNote)) * (fMaxAngle - fMinangle) + fMinAngle;
freqServo.write(fAngle);
// test if we need to increment to the next noteif( noteDurationCtr >= Songs[iCurrSong].data[iNote].duration )
{
noteDurationCtr = 0;
// increment note and test if we have finished the songif( ++iNote >= Songs[iCurrSong].SongLength )
{
// song is over
playingSong = false;
iNote = 0;
freqServo.detach();
volServo.detach();
displayLCD("Press sel 4Demo", Songs[iCurrSong].name );
}
else
{
// set volume for next note
volume = Songs[iCurrSong].data[iNote].volume;
vAngle = vMinAngle + ((float)(vMaxAngle - vMinAngle) / (float) MAX_VOL) * volume;
volServo.write(vAngle);
}
}
noteDurationCtr++;
}
}

we are converting the note we obtained from the Songs array into an angle to be passed to servo.write(). Lets decompose this line. What we are doing is rescaling from our range of notes to our range of angles. The first term is

(Note2Play – minNote) / (maxNote – minNote)

think of this as the fraction of the total note range that our current note represents. We want to multiply this by the range of angle that we are using.