Introduction: Circle of Fifths MIDI Device
This is my first project using MUSE, a circuit board that I designed for creating Music Using Simple Electronics. It's very similar to and largely inspired by the Makey Makey. I prepared this as a demo for kids in a class called Magical Musical Objects at the Children's Museum of Bozeman STEAMlab, where they wired up their own MUSE kits and programmed them to play notes and songs by touching potatoes. While developing the first prototype, my son enjoyed playing an apple!
The circle of fifths plays an important role in music composition. A chord progression based on the circle of fifths generally sounds pleasant as it guides the ear to a specific resolving chord called the tonic. When I was researching the circle of fifths for a potential project that used MUSE, I found this interactive circle of fifths widget. It seemed like a great way to learn about the circle of fifths, but it was missing any way to hear the chords that you select. I put some thought into how I could create a real life version of that widget which also allowed the chord progressions to be played. This is what I came up with.
If you enjoy this Instructable, please consider voting for it in the Arduino and Epilog contests in the top right (or on mobile at the very bottom).
Step 1: The Plan
MUSE has 16 programmable pins available. One is needed for each of the 12 notes, so that when touched the corresponding chord is played. I also needed a way to be able to select one of the 12 notes as the tonic chord. Luckily, the 4 remaining pins leaves enough room to be able to select 16 different states. I only needed 12, so 4 pins is plenty. I'll refer to these 4 pins as the selector pins.
The selector pins can each be on or off. If we treat the state of each pin as a bit, then with 4 pins we can represent the numbers 0-15 in binary. For reference, here are 0-15 represented in binary: 0 = 0000, 1 = 0001, 2 = 0010, 3 = 0011, 4 = 0100, 5 = 0101, 6 = 0110, 7 = 0111, 8 = 1000, 9 = 1001, 10 = 1010, 11 = 1011, 12 = 1100, 13 = 1101, 14 = 1110, 15 = 1111. The selectors pins are all pulled high, so by default their state would be 1111, or 15. By shorting specific selector pins to ground, we can represent any of the above states. I decided to start at the top of the circle of fifths and go clockwise assigning numbers from 0 through 11: C = 0, G = 1, D = 2, A = 3, E = 4, B = 5, E# = 6, Db = 7, Ab = 8, Eb = 9, Bb = 10, F = 11.
So how do we rig this scheme up as a circuit?
I imagined a wooden board with the circle of fifths engraved in it. On the back side of the board is all our circuitry and we can drill holes to bring certain signals through. Let's create 4 concentric metal rings, each connected to a different selector pin. The inner most ring will represent the least significant bit (the right most bit). In each section, let's drill holes through to each ring if it represents a 0 for the given bit. Since C = 0 or 0000, holes will be drilled for all 4 rings. G = 1 or 0001, so the inner most ring doesn't get a hole. D = 2 or 0010 so the second from the inner most ring doesn't get a hole. If you keep following that pattern you'll get the diagram above.
Now all we need to do is have some way to short all exposed bolts to ground for the currently selected tonic chord. I'll show you how I achieved that in the steps below.
Step 2: What You'll Need
Materials
- 12" x 12" x 1/2" plywood - I bought a 2' x 4' sheet from Home Depot, had them cut it in half lengthwise (so I had two 1' x 4' boards), then cut 8 of my own pieces that were just shy of 12" x 12".
- 3/4" dowel rod - We just need four 2.5" spacers, so this could be any number of things. Any dowel about 3/4"-1" in diameter would work or cut some blocks that about 1" wide x 2.5" long.
- 1-1/2" Screws - These are the screws I used, but you could use whatever you have lying around that will go through 1/2" plywood plus a little extra. You could also use glue instead. These are used to attach the spacers to the main board.
- MUSE kit - If there's enough interest, I can put together these kits. If enough people fill out the form on the product page in my shop, I'll put some orders in and make a $20 kit. For now, you'll have to source your own kit.
- MUSE PCB - You can order them in multiples of 3 from Osh Park. You can source them elsewhere with the Eagle files here.
- 22Mohm resistors - This is a pack of 50. You'll only need 16 of them.
- 220 ohm resistors - This is a pack of 50. You'll only need 2 of them.
- SDS-50J MIDI connector - This is the only one I could find on Amazon, and it shows the wrong picture, but seems to be the right part number. This is the link to the right part on DigiKey (if you go that route, you'll probably want to get the 22M and 220 resistors from DigiKey as well).
- M3 machine screws - You'll need 18 to put one in every hole in the PCB.
- M3 hex nuts - Again, you'll need 18.
- Female headers - If you want the Arduino-compatible board to be removable, you can solder on these headers. Otherwise, you can just solder the Pololu to the MUSE PCB (I prefer having the headers, so I can pop out the Arduino and use it for other projects when I'm not actively using this Circle of Fifths).
- Pololu A-Star 32U4 Mini - This is the Arduino-compatible board that will plug into the MUSE board.
- Hookup Wire - I used 20 gauge, but 22 gauge is more common (that's what I linked).
- Ring Terminal Connectors - These make it easy to connect wires to the MUSE board, but you could also just connect the raw wire directly.
- Nylon Spacers - I picked up a few from a local hardware store, but these should suffice. You'll need 4 spacers about 3/4" long. The 18 or 20mm ones in this kit would do.
- #4 x 1" wood screws - Again, I nabbed mine at the local hardware store, but these will do.
- Heat Shrink - Not absolutely necessary, but is nice to help ensure wires don't short.
- Banana Jacks - These will be used to trigger our chords.
- Solder - You'll need this to assemble your MUSE kit. I also used this to connect the wires to the banana plugs.
- Sheet metal - We'll use this to fashion our selector rings and other miscellaneous pieces.
- 1/4" x 1-1/4" hex bolt - I recommend sourcing this from a local hardware store to avoid having to buy 50 of them.
Tools
- Miter Saw - I used this to cut my 12" x 12" boards, along with my 2 1/2" dowel spacers. A hand saw would do.
- Impact Driver - I used this to attach my 4 spacers. A hand screw driver would do, in fact I used a hand screw driver to attach the PCB to the wood as I wanted to be a little more delicate.
- Drill - There are many holes to drill.
- Drill Guide - I used this and a drill instead of a drill press to make all the holes.
- Soldering Iron - This is the exact one I used in this project.
- Smoke Absorber - This is key to avoid breathing in soldering fumes.
- Hair dryer - If you use heat shrink, you'll need something to shrink it with.
- Wire strippers - There are a lot of wires to strip. This is also able to crimp the ring terminals.
- Snips - You'll need these to cut the sheet metal.
- Laser cutter - I used my friend's Epilog Mini laser cutter, but I have a Glowforge on order. If you purchase a Glowforge using this link you'll get $100 off.
Step 3: Laser Cutting
I made two designs for a laser cutter, one for the bottom that resembles the diagram above and the other for a wooden piece that selects the tonic chord and shows the chord progression for that selection. You can download the designs here:
- Circle of fifths - The diagram above with some additional marks for drilling holes. The text should be rasterized and other lines are thin enough to be traced as vector lines. The intent is to be engraved into 1/2" plywood, so I wasn't worried about cutting all the way through with the 40W laser I have access to.
- Selector overlay - The selector needed to be cut, so I used 1/8" plywood so the laser would have an easy time with it. If I did it over, I'd probably try to go with 1/4" instead. Blue vector lines should have a power/speed setting to cut, while black vector lines should be set to an engrave power/speed setting.
Step 4: Assembly
The first step is to drill all the holes that are marked with crosses on the laser cut parts. The holes for the selector pins are 1/8" in diameter. The center hole and the outer holes are 1/4" in diameter. I used a drill guide to keep them as straight as possible (my drill press didn't have a long enough neck to drill the center most holes). I used M3 machine screws and sheet metal to fashion spring-like connections on the front side of each of the selector pins. On the back, I again used sheet metal to cut 4 metal rings for each of the selector pin rings. I wired each selector pin on the MUSE board to each of those rings. Then, using another piece of sheet metal, glued to the back of the 1/8" plywood selector overlay (aligned on the back with the tonic chord, labeled I), all selector pins will be shorted to ground for the currently select tonic. Calibrating the makeshift sheet metal springs to touch just right can be a tricky process. I went through a lot of tweaking with the pliers and filing sharp edges. Even so, the board only reliably turns clockwise. When turning counter clockwise, the springs are more likely to catch and bend, throwing off the calibration. If I go through another design iteration, I'll revisit these springs to make them a more reliable connection.
I wired up the MUSE board to banana jacks that fit through the 1/4" holes we drilled. I soldered one end of each wire to the connectors that came with the banana jacks and crimped the other end to a ring terminal that easily attaches the MUSE board using an M3 machine screw and nut. It's not all that important which pins go where. I chose to wire them up based on how close each hole was to the board and this is what I programmed into the Arduino:
Note | Pin |
---|---|
C | 8 |
G | 7 |
D | 6 |
A | 5 |
E | 4 |
B | 3 |
F♯ | 2 |
D♭ | 20 |
A♭ | 19 |
E♭ | 18 |
B♭ | 10 |
F | 9 |
And this is how I wired the selector pins:
Selector Bit | Selector Pin |
---|---|
0 | 17 |
1 | 16 |
2 | 15 |
3 | 14 |
I then drilled 4 additional holes in the corners, put banana jacks in them and wired them up to ground. Whenever a ground wire is shorted to any of the other notes (either directly, or through a conductive material such as your body, a fruit, a metal chair), it will trigger that chord to be played.
Step 5: Coding
Now we need to program the Arduino to output the desired MIDI notes when pressed. I'll walk you through the code and then post the whole program at the bottom. You'll need to install the MUSE library, which I also developed. Download the MUSE-master.zip file from GitHub, and install it as explained here.
Alright, let's take a look at the code. First, are some compile-time configuration variables:
//#define DEBUG #define MUSE_USE_SERIAL1 #define SELECTOR_BIT0_PIN 17 #define SELECTOR_BIT1_PIN 16 #define SELECTOR_BIT2_PIN 15 #define SELECTOR_BIT3_PIN 14
The DEBUG line is commented out using // at the beginning of the line. You'll want to remove those slashes when you first start testing your circle of fifths to see if your selector pins are being correctly shorted, along with which note is being played. When uncommented, debugging information will be sent to the serial monitor so you can see what's happening. When it looks like everything is working correctly, comment out that line so you're only sending MIDI events. MUSE_USE_SERIAL1 tells the MUSE library to output MIDI events to Serial1 rather than Serial, which on the MUSE board means send the MIDI events through the provided MIDI port. If this line is commented out or removed, the MIDI events will go over the USB Serial interface and you'll need a Serial to MIDI software bridge of some kind on your computer. Finally, we define what pins we wired up our selector pins to.
Next, we initialize the MUSE library by including it and instantiating a MUSE object in the variable muse:
#include <MUSE.h> MUSE muse;
The MUSE library provides some basic MIDI functionality as well as simple event handling for detecting changes on the digital pins. You'll see the muse variable throughout the code.
After our MUSE object is created, we define a couple more variables:
int roots[] = { MUSE_C4, MUSE_G4, MUSE_D4, MUSE_A4, MUSE_E4, MUSE_B4, MUSE_FS4, MUSE_CS4, MUSE_GS4, MUSE_DS4, MUSE_AS4, MUSE_F4 }; int selector = 0;
The roots array lists the 12 root notes as you go clockwise around the circle of fifths. We'll use this array when deciding what chords to play. The selector variable represents the current state of the selector pins. It should always be between 0 and 11 when one of the tonic chords is selected. When no selector pins are shorted to ground, they should all be pulled high and the selector variable should be 15, but in the code I enforce the 0-11 range and set selector to 0 when its above 11 (that way C is used as the tonic when no selector pins are shorted or the selector pins are improperly configured). We'll talk more about the selector pins later on.
Now we've reached the setup and loop functions, where we initialize the MUSE object and register callback functions that will be called when changes on the digital pins are detected. If you've wired up the notes differently, change the pins numbers here.
void setup() { muse.setup(); // Callbacks for the 12 different notes around the circle of fifths muse.registerDigitalInputCallback(C_Callback, 8, false); muse.registerDigitalInputCallback(G_Callback, 7, false); muse.registerDigitalInputCallback(D_Callback, 6, false); muse.registerDigitalInputCallback(A_Callback, 5, false); muse.registerDigitalInputCallback(E_Callback, 4, false); muse.registerDigitalInputCallback(B_Callback, 3, false); muse.registerDigitalInputCallback(Fs_Callback, 2, false); muse.registerDigitalInputCallback(Db_Callback, 20, false); muse.registerDigitalInputCallback(Ab_Callback, 19, false); muse.registerDigitalInputCallback(Eb_Callback, 18, false); muse.registerDigitalInputCallback(Bb_Callback, 10, false); muse.registerDigitalInputCallback(F_Callback, 9, false); // Callbacks for changes on the 4 selector pins muse.registerDigitalInputCallback(bit0, SELECTOR_BIT0_PIN, false); muse.registerDigitalInputCallback(bit1, SELECTOR_BIT1_PIN, false); muse.registerDigitalInputCallback(bit2, SELECTOR_BIT2_PIN, false); muse.registerDigitalInputCallback(bit3, SELECTOR_BIT3_PIN, false); // initialize the selector pins initSelector(); } void loop() { muse.loop(); // check for and respond to events that we registered }
muse.setup() initializes Serial and MIDI communications. The registerDigitalInputCallback will tell the muse.loop() function to check for changes on the digital pin provided in the second argument and when a change is detected, the current state of the pin is passed to the callback function that is provided as the first argument. The third argument (in this case, always false) tells MUSE not to activate the internal pull up resistors. The MUSE board uses external 22Mohm pull up resistors so we don't need the internal ones. The initSelector function initializes the selector variable and is defined below.
void initSelector() { int bit0 = digitalRead(SELECTOR_BIT0_PIN); int bit1 = digitalRead(SELECTOR_BIT1_PIN); int bit2 = digitalRead(SELECTOR_BIT2_PIN); int bit3 = digitalRead(SELECTOR_BIT3_PIN); selector = bit0 | (bit1 << 1) | (bit2 << 2) | (bit3 << 3); if(selector > 11) { selector = 0; } #ifdef DEBUG debugSelector(); #endif }
In initSelector, we read the current state of the selector pins and store them in the bit0, bit1, bit2 and bit3 variables. Each one will be equal to 0 or 1. We combine them into a single variable using bit shifting operator (<<) and the logical OR operator ( | ). bit0 is the least significant bit (the right most bit) and since its already 0 or 1 is already in the least significant bit so we don't need to change anything. bit1 needs to be shifted 1 bit to the left so we use << to do so: (bit1 << 1). 0 or 1 changes to 00 or 10. bit2 needs to be shifted 2 bits and bit3 needs to be shifted 3 bits. Then we combine them all so selector will be equal to something between 0 and 15 (or in binary 0000-1111). Then we set selector to 0 if it's greater than 11 so it defaults to C as the tonic when its not configured correctly. Lastly, if DEBUG mode is on, print out the state of the selector pins by calling debugSelector which is defined below:
#ifdef DEBUG void debugSelector() { switch(selector) { case 0: Serial.println("Selected C Major"); break; case 1: Serial.println("Selected G Major"); break; case 2: Serial.println("Selected D Major"); break; case 3: Serial.println("Selected A Major"); break; case 4: Serial.println("Selected E Major"); break; case 5: Serial.println("Selected B Major"); break; case 6: Serial.println("Selected F# Major"); break; case 7: Serial.println("Selected Db Major"); break; case 8: Serial.println("Selected Ab Major"); break; case 9: Serial.println("Selected Eb Major"); break; case 10: Serial.println("Selected Bb Major"); break; case 11: Serial.println("Selected F Major"); break; } } #endif
Next, are the selector pin callbacks, which we registered in the setup function:
void bit0(bool state) { selector = (selector & 0b1110) | state; if(selector > 11) { selector = 0; } #ifdef DEBUG debugSelector(); #endif } void bit1(bool state) { selector = (selector & 0b1101) | (state << 1); if(selector > 11) { selector = 0; } #ifdef DEBUG debugSelector(); #endif } void bit2(bool state) { selector = (selector & 0b1011) | (state << 2); if(selector > 11) { selector = 0; } #ifdef DEBUG debugSelector(); #endif } void bit3(bool state) { selector = (selector & 0b0111) | (state << 3); if(selector > 11) { selector = 0; } #ifdef DEBUG debugSelector(); #endif }
Each selector pin callback is very similar. We start by blanking out the corresponding bit using the bitwise AND operator ( & ) in case the bit changed to 0. Then we add back in the current state of the corresponding bit using the bitwise OR operator ( | ) in case it changed to 1. Here's an example:
If selector = 6 and we detect that bit1 changed to 0:
6 in binary is 0110.
We want to blank out bit1 so we combine it with 1101 (any bit that is both 1 will stay 1 otherwise it goes to 0):
0110 &1101 ----- 0100 = 4
Bit 1 changed to 0 so when we combine them with | there is no change (either bit can be 1 for it to stay 1):
0100 |0000 ----- 0100 = 4
So the selector pin would now equal 4. Let's do one more example going the other way.
If selector = 4 and we detect that bit1 changed to 1:
4 in binary is 0100.
Using bitwise AND to blank out bit1, results in no change in this case:
0100 &1101 ----- 0100 = 4
But a bitwise OR with the changed bit1 changes selector back to 6:
0100 |0010 ----- 0110 = 6
At the end of each function, we check to make sure the selector variable isn't greater than 11 and then prints out the current state of the selector if we're in DEBUG mode.
Next comes the note callbacks. Each consist of printing the debug information when in DEBUG mode, then doNote is called with the current state of the note pin (pressed or not) along with the corresponding MUSE note which will be used to build a chord (see below). I've included just the C_Callback here, but they all do the same thing with a different note passed to doNote.
void C_Callback(bool state) { #ifdef DEBUG if(state) { Serial.print("Released "); } else { Serial.print("Pressed "); } Serial.print("C "); #endif doNote(state, MUSE_C4); }
The doNote function is below.
void doNote(bool state, int note) { for(int i = -1; i < 2; i++) { if(roots[(selector+i+12)%12] == note) { #ifdef DEBUG Serial.print("major"); #endif doMajorTriad(state,note); } } for(int i = 2; i < 5; i++) { if(roots[(selector+i)%12] == note) { #ifdef DEBUG Serial.print("minor"); #endif doMinorTriad(state,note); } } if(roots[(selector+5)%12] == note) { #ifdef DEBUG Serial.print("diminished"); #endif doDiminishedTriad(state,note); } #ifdef DEBUG Serial.println(); #endif }
The doNote function uses modular arithmetic to scan through the roots array to find what relation the current note has to the tonic note. If the current note is the note before the tonic, the tonic or the note after the tonic, then we need to form a major chord. To perform that check, we use the variable i to loop from -1 to 1 and the expression (selector+i+12)%12 to get an index into the roots array. How did we get that expression? Let's step through it.
selector is the current position of the tonic note in the roots array. i is our loop variable that starts at -1 and goes through 1. So, selector+i will be the previous, then current, then next tonic chord. But let's focus on when i is -1.
selector-1 should be the note before the tonic note. But, what if selector is 0? Then selector-1 would equal -1 and in C/C++ using a negative index would try to access memory before the start of the array (not what we want). When selector is 0, the previous note is actually the last note in the roots array or the note with an index of 11. So if we add 12 to our expression, giving (selector+i+12), we've accounted for when selector is 0 and i is -1. But now we've introduced another problem. What about when selector is anything other than 0? Let's say 6. Now our expression is (6-1+12) or 17. Our roots array is only 12 elements, so we'd be attempting to index into memory beyond our array, which is just as bad as trying to access memory before our array. What we're actually trying to get is the previous index, 5. We can use the mod operator (%) to make our index "wrap around" back to the beginning of the array. The mod operator returns the remainder when you divide the second number into the first. If we divide 17 by 12, the remainder is 5, exactly what we're looking for! Our final expression is (selector+i+12)%12. We use similar expressions when checking for the minor and diminished chords, but we don't need to worry about the index being negative. This kind of math is called modular arithmetic and is often used when cycling through values. For more information about modular arithmetic, check out Khan Academy's explanation.
The last pieces of the puzzle are the doMajorTriad, doMinorTriad and doDiminishedTriad functions:
void doMajorTriad(bool state, int note) { if(state) { muse.noteOff(note); muse.noteOff(note+4); muse.noteOff(note+7); } else { muse.noteOn(note); muse.noteOn(note+4); muse.noteOn(note+7); } } void doMinorTriad(bool state, int note) { if(state) { muse.noteOff(note); muse.noteOff(note+3); muse.noteOff(note+7); } else { muse.noteOn(note); muse.noteOn(note+3); muse.noteOn(note+7); } } void doDiminishedTriad(bool state, int note) { if(state) { muse.noteOff(note); muse.noteOff(note+3); muse.noteOff(note+6); } else { muse.noteOn(note); muse.noteOn(note+3); muse.noteOn(note+6); }
Given the root note of a major chord, a triad can be formed by adding the third and fifth, which can be found by adding 4 half steps and 7 half steps. Similarly, a minor chord adds a minor third and a fifth, which requires adding 3 half steps and 7 half steps. The diminished chord adds a minor third and minor fifth, which requires adding 3 half steps and 6 half steps. And that's it! You can get the whole program (along with the vector graphics and hardware schematics) off GitHub, here.
Step 6: All Done!
You're all done! Now you can wire up anything that conducts electricity to play circle of fifths chord progressions.
Thanks for following along. If you liked this Instructable, please consider voting for it in the Arduino and Epilog contests!