Open Source Desktop Synthesizer Benjamin Miller, Mouser Electronics
Licensed under CC BY-SA 4.0
Download Full Project
BOM
Software
You can download the full sketch for this project here on GitHub . You can use the free Arduino IDE to program your board. Copy and paste this code into the IDE and upload it to your Arduino. If you have never used the Arduino 101, or any Arduino board, please refer to Arduino’s Getting Started page for setting up your IDE. If you are interested in how the program works, you can continue reading below about each section.
The MPR121 Adafruit Library
The capacitive touch sensors run on a library written by Adafruit . Instructions on how to download and install the library can be found on Adafruit’s website. You must install the library for the touch-capacitive keyboard to work properly.
This library sets a baseline capacitance and senses when one of the pins on the MPR121 breakout board is touched. Any program using the boards must include the library header and Wire.h. The example program that comes with the library details how to use the library. The part of the synth code that deals with this library is heavily based on Adafruit’s example program, yet it’s also doubled to account for the use of two breakout boards. The use of the ‘current touched’ and ‘last touched’ variables, one for each of the breakout boards, will be explained in the next section.
Adafruit_MPR121 cap = Adafruit_MPR121();
Adafruit_MPR121 cap2 = Adafruit_MPR121();
uint16_t currtouched = 0; //current keys being pressed
uint16_t currtouched2 = 0;
uint16_t lasttouched = 0; //keys that were previously pressed
uint16_t lasttouched2 = 0;
The boards use I2 C to communicate with the Arduino 101. One of the boards has the address 0x5A while the other has 0x5C. We gave the second board that address by connecting a wire between the ADDR pin and the SDA pin.
if (!cap.begin(0x5A)) {
while (1);
}
if (!cap2.begin(0x5C)) {
while (1);
}
These if statements trap the program in a while loop until the breakout boards have initialized.
Keys and Pins Explanation
There are two major steps in the program’s handling of the key inputs: the reading of the keys and the assignment of the pins. The program reads the keys and decides which notes need to be played. Then the program checks all of the digital output pins (pins 7-13), picks the next available pin, and assigns the new note to a pin. The pin plays this note and gets added to all of the other notes by the summing circuit on our protoboard. Now, in more detail:
The program first reads the capacitive touch sensors and puts them the variables “currtouched” and “currtouched2”.
/**********READ KEYS************************/
//read currently touched pins.
currtouched = cap.touched();
currtouched2 = cap2.touched();
Think of these variables as a 12 bit number, where each bit represents a key that is being pressed. Now that we have the keys that are currently being pressed, we need to figure out which ones were pressed but have since been released.
/*********NOTE RELEASE*********************/
//empty pins that no longer should have notes assigned to them.
for (uint8_t j=0; j< j++){
//if it was touched and now is not...
if (!(currtouched & _BV(j)) && (lasttouched & _BV(j)) ){
//...release the pin it was assigned to.
if (pin7.keyAssigned == j){
//find which pin was assigned to the released key.
pin7.used = 0;
pin7.keyAssigned = -1;
}
else if (pin8.keyAssigned == j){
pin8.used = 0;
pin8.keyAssigned = -1;
} …
I used a series of nested if-then statements inside of a for-loop to figure out which notes that used to be pressed are now not being pressed. This is done by comparing a particular bit in the “currtouched” variable with the same bit of a variable called “lasttouched.” I used “& _BV(j)” to mask the variables to compare one bit at a time, where j is the incremented variable in the for-loop. Basically, we check one bit at a time.
In the case that a key has been released, we then have to find out which pin that key was assigned to. The keyAssigned variable in each pin struct will tell you which key activated that pin. Releasing a note (stopping that note from playing) is as easy as setting the used variable to 0 and assigning that pin a key of -1.
Regarding the “pin struct”, I will now outline each of the variables and what they do. If you are not familiar with the concept of a “struct” in C/C++, check out this tutorial on data structures . I made the pin_t datatype to hold all of the important information about the pins in a way that I could refer to them in the program wherever I wanted to.
struct pin {
uint8_t used; //is the pin used? 1 for used, 0 for unused.
int8_t keyAssigned; //which key has been assigned to this pin? value from 0-23. -1 means no key was assigned to it.
uint8_t state; //HIGH or LOW
uint16_t index; //how many interrupts until the next toggle occurs? Determines pitch.
uint16_t origIndex; //remembers what the original note is supposed to be.
uint16_t resetIndex; //used to refresh index after each toggle.
};
typedef struct pin pin_t;
// create pin struct variables, which will be assigned to certain notes as they are played.
pin_t pin7;
pin_t pin8;
pin_t pin9;
pin_t pin10;
pin_t pin11;
pin_t pin12;
pin_t pin13;
The “used” variable can be checked whenever the program needs to know if that pin is in use (is playing a note).
“keyAssigned” tells the program which key activated the pin, which we just saw when releasing a note.
The “state” variable tells the program whether the pin is currently outputting a HIGH or a LOW. Our keyboard outputs square waves by quickly toggling the output pins, as you will read about in the next section.
The three index variables (“index,” “origIndex,” and “resetIndex”) store the pitch of the note that the pin is playing.
“index” is what the interrupt uses to time the toggles
“resetIndex” is used to refresh the first index variable every toggle
“origIndex” is there to hold the original reset index in case that gets changed (like it does when using the vibrato settings).
This will make more sense later, as I go into more detail about why we have different index variables in the later sections.
Once the old notes are released, we have to take a look at keys that may have been pressed since the last cycle.
/************NOTE PRESSED******************/
//take input from sensors and determine new keys being played.
//each key is assigned to a certain note index/pitch, but this changes depending on the mode.
uint16_t currentPitch = 0;
int16_t key = -1;
for (uint8_t j=0; j< j++){
if ((currtouched & _BV(j)) && !(lasttouched & _BV(j))){ //if a key was recently touched
if (j == 0){
if (mode == 1)currentPitch = C3index;
if (mode == 2)currentPitch = C4index;
if (mode == 3)currentPitch = C5index;
}
if (j == 1){
if (mode == 1)currentPitch = Cs3index;
if (mode == 2)currentPitch = Cs4index;
if (mode == 3)currentPitch = Cs5index;
}…
The structure of the index assignment is very similar to the note release code except instead of changing the “pin struct” variables, we are changing a variable called “currentPitch” depending on the current key pressed and the mode the system is in.
At the top of the sketch there is a large lookup table of index values which I chose based on a formula, with a little tweaking to account for tuning. Each note from C3 to B5 has an index that tells the program how many interrupts to wait before the program should toggle the pin. This section checks which key has been pressed and loads up the currentPitch variable with the appropriate index.
lasttouched = currtouched; // "save" the keys being pressed for the benefit of the new loop.
lasttouched2 = currtouched2;
The notes currently being touched are then saved in the “lasttouched” and “lasttouched2” variables. We do this so on the next cycle the program can comparing the pressed keys with what was previously pressed and change the pins appropriately. The key that has been assigned is also remembered by writing this into the variable “key”.
The final step of note processing is pin setting.
/***********SET PIN***********************/
//parse available pins by checking pin.used. Assign the note to a pin with currentPitch and key.
if (key >= 0){
if (pin7.used == 0) {
pin7.index = currentPitch;
pin7.origIndex = currentPitch;
pin7.resetIndex = pin7.origIndex;
pin7.keyAssigned = key;
pin7.used = 1;
}…
This step first checks that some change has occurred (by checking that “key” is not negative), then it checks which keys are not being used. As soon as it finds an unused key, it assigns the three index variables the “currentPitch,” tells the pin which key activated it (via the “keyAssigned” variable) and marks the pin as “used.”
The Interrupt and Pin Toggling
Now that each pin has an index assigned to it, we can talk about the interrupt service routine.
The actual sound is created by rapidly toggling the output pins on and off, creating a square wave. Each pin can only support one note at a time, which is why I assigned each note to a different pin. If there were seven different timers on the Curie module, you could simply set the pins to toggle at different speeds by assigning them each a timer, but there is one timer available for use: CurieTimerOne .
So we have to set CurieTimerOne to an extra fast interrupt and toggle the pins at various times on that interrupt. This is where the indexes become useful. Each note has its own index, or amount of interrupts that happen between toggles, for the note created to be at that exact pitch. So we set the interrupt to occur every 4 microseconds:
CurieTimerOne.start(4, interruptHandler);
From here on out, the interrupt will be constantly occurring, calling the function I have created called “interruptHandler.” The first thing interruptHandler does is check if a pin is currently being used. It does this for every pin.
if (pin7.used == 1) {
if (pin7.index == 0) {
pin7.state ^= 0x01;
pin7.index = pin7.resetIndex;
}
else pin7.index--;
}
If the pin is being used, it then checks the pin’s index. If the index has hit 0, it means it is time to toggle the pin (i.e., half a period has lapsed since the last toggle). The toggle uses an exclusive-or (XOR) operator to toggle the pin’s state, then resets the index to prepare it for the next interrupt. If the pin’s index has not hit zero (which is true most of the time) the index will decrement.
After the index has been assessed, the Interrupt Handler program must actually do the work of toggling the pin. Thus far, we have changed the pin’s state variable, but we have not actually affected the pin. So, at the end of every interrupt service routine, we use the digitalWrite function on each pin:
digitalWrite (7, pin7.state);
Interrupt service routines have to happen quickly or they will interfere with the flow of the program. So we let the main loop handle the major administrative duties like assigning pins and recognizing key presses, and let the interrupt do the dirty work of changing the pins.
The Vibrato System
The vibrato system alters a pin’s reset index to temporarily change the pitch of a note. How much that pitch is altered is determined by the distance the IR sensor reads. To do this, we will utilize two static variables, “BasePos” and “VibratoVar.”
The CheckVibrato function first makes sure that there is a hand in front of the IR sensor, reading analog pin A2 and checks if it has a value greater than 110 (the closer an object is to the sensor, the higher the number returned). If a hand is in front of the sensor, the vibrato is considered “engaged.”
Once the sensor is engaged, the program figures out if it was previously engaged. If not, a new “base position” has to be recorded. The base position is the first position that the user’s hand was in when it entered the sights of the IR sensor; holding your hand here keeps the pitch at its original frequency.
if (analogRead(A2) > 50){ //if vibrato is engaged, check if it was previously engaged
if (BasePos != 0){ //if it was previously engaged, and is currently still engaged
//calculate VibratoVar and add it to all current pin indexes then return
VibratoVar = analogRead(A2) - BasePos;
if (pin7.used == 1){
if((pin7.origIndex + VibratoVar) > 10){ //don't want to give pin negative indexes, in the case of a high pitch and a high pitch bend.
pin7.resetIndex = pin7.origIndex + VibratoVar;
}
else{
pin7.resetIndex = 0;
}
}
…
else { //if it was not previously engaged, set the BasePos then return
BasePos = analogRead(A2);
}
Once the base position has been recorded, the program waits until the current cycle comes back again to change the pitch. On this second pass, the program recognizes that the vibrato was previously engaged (it sees that BasePos is not equal to 0) and sets a new benchmark: “vibrato variable.” This VibratoVar is determined by subtracting the current IR reading (A2) from the base position: the distance between where the hand was and where it currently is. This number is then added to the reset index, changing the pitch of the note.
If the vibrato variable is negative, the pitch increases, and vice versa. When the hand is pulled away from the IR sensor, the program sets the vibrato variable and base positions back to zero, as well as sets the reset index to the original index value. Resetting the index back to the original note is the reason why we needed three different index variables in pin struct.
else { //if it is not engaged, check if it was previously engaged
if (BasePos != 0){ //if it was previously engaged but is now not engages, reset BasePos and VibratoVar to 0, reset notes to their original pitches, then return
BasePos = 0;
VibratoVar = 0;
pin7.resetIndex = pin7.origIndex;
pin8.resetIndex = pin8.origIndex;
pin9.resetIndex = pin9.origIndex;
pin10.resetIndex = pin10.origIndex;
pin11.resetIndex = pin11.origIndex;
pin12.resetIndex = pin12.origIndex;
pin13.resetIndex = pin13.origIndex;
}
//if it was not previously engaged and is still not, return immediately.
}
Mode Switching
Up to 4 capacitive touch sensor boards can be added to this system, but in the interest of space I elected to only use 2 boards. Each board allows an octave of notes, but since I wanted this synthesizer to be like an actual instrument, I chose to include a mode switch. The mode switch changes the octaves that the keyboard spans.
The 3-way on-on-on switch from C&K that I chose is double-pole, double-throw. Since it is double-pole, I made a little circuit around it that sends two signals to the Arduino 101 (specifically to the analog pins A0 and A1). The switch redirects either 5 volts or 0 volts to the analog pins. If I were wiring the switch to the digital pins, the program would read 00, 01, and 11 depending on the position of the switch, but since we are wiring them to the analog inputs, the program reads either 0 or 1023. I use <= 50 on analog pin A0 because I found that it occasionally will read a 1 or a 2 when it is supposed to be set to 0.
/************SET MODE*********************/
//set play mode based on selector switch position. Mode 1 is Octaves 2-3, Mode 2 is Octaves 3-4, Mode 3 is Octaves 4-5.
uint8_t mode;
if ((analogRead(A0) <= 50)&&(analogRead(A1) == 0)){
mode = 1;
}
if ((analogRead(A0) <= 50)&&(analogRead(A1) == 1023)){
mode = 2;
}
if ((analogRead(A0) == 1023)&&(analogRead(A1) == 1023)){
mode = 3;
}
We would love to hear what you think about this project; please tell us in the comments section below.