In our last article, we learned about the OSC communication basics with Arduino. Since OSC (Open Sound Control) was developed as a protocol for musical instruments, today we will try to create an instrument device.
While I was kind of lost about what kind of device to create, I thought that it would be a good idea to create an OSC keyboard for Arduino. After doing some research, I found about the sound programming app Sonic Pi, which was featured in “Audio programming with Sonic Pi!.” It runs on Raspberry Pi ,and it’s compatible with OSC. So, today let’s create a rhythm machine that works with Sonic Pi.
Expected time to complete: 150 minutes
Parts needed:
We designed the basic flow as follow. The data will be sent from the Arduino controller circuit to the Processing server using OSC. Based on the controller input value, it will be converted into sound programs for Sonic Pi. Then, they will be sent to Sonic Pi, which will create and play sounds. It’s fine if you don’t understand all the details. Sonic Pi is written with a programming language called Ruby, and today, we will use Ruby for the OSC signals.
Figure 1 System flow
Figure 2 Sonic Pi, the sound programming environment
Check this article for more information about Sonic Pi. Sonic Pi not only runs on Raspberry Pi, it also runs on Windows and Mac OS X. We already have the Processing environment set up from the part 1, so let’s download the Mac OS X version. Once we figure out the overall system structure, we will start building the actual device.
First of all, we need to build a controller. Let’s make a cool one! The cases I’m usually using are all flat and big, and I just couldn’t find a good one. So, I kept looking.
Picture 1 Chopstick box
I found the perfect size box, which actually is a chopstick box. The depth seems good too.
Picture 2 Inside the box
There’s something about making a rhythm machine out of a box that has Kanji (Chinese characters) on it. Well, now that we found a good case, let’s start creating the circuit.
Before we build the circuit, let’s align the parts and get an image of the interface we want. When we build the circuit there will be many limitations, so it may not always go as planned. It’s still better to decide on the specs beforehand like part arrangement or where adaptors/LAN cables will come out from (if they come out from numerous directions, it doesn’t fit well). Thinking about all kinds of ideas is the fun part.
Picture 3 Mounting the parts and thinking about the interface
Since we’re building a rhythm machine, we arranged the parts so that the sounds can be toggled on and off using a toggle switch and the 8-beat rhythm could be controlled. We are using Arduino Ethernet (Arduino and Ethernet shield combined) due to the depth of the case. We want to add the 7 segment LED as shown in Photo 3, but we don’t have enough Arduino pins, so let’s think about that later. Also, we used a transparent acrylic board for the lid. We will drill holes in the board for each part.
Once we decided on the arrangement, we will process the case and build the circuit.
Picture 4 Making the case based on the parts
Picture 5 Drilling holes in the acrylic board for the each part
Photo 6 Arranging the parts
Once we have most of the parts arranged in the case, let’s start building the circuit. There are many parts for this one, so we need to save space on the breadboard and save pins on Arduino. If we simply use 8 pins from Arduino for the 8 toggle switches, we won’t be able to add other parts in the future if we we want to make improvements, so let’s try to get creative with the circuits.
Figure 3 Rhythm machine interface circuit
The circuit we are building today has 8 toggle switches spread out in groups of 4s to determine the power input status. When we created the speaker, we arranged switches with different resistor values to remember which switch was pushed. We will use the same method here. (The actual device has LEDs too, but for the sake of the circuits explanation, we will skip that part)
Previously, we didn’t evaluate when multiple switches were being pushed, but we need to for this one. So, let’s go a step further. We will prepare 1KΩ, 2KΩ, 4KΩ, 8KΩ for the resistor values, so that even when multiple buttons get pushed, Arduino can determine each one by the input value. In simpler terms, if the resistor value is 1KΩ, 2KΩ, 4KΩ, 8KΩ from the top, the total will be 1 when #1 is being pushed, or 3 when #1 and #2 are being pushed, or 5 when #1 and #3 are being pushed, and so on. This way, if the total value is different for all patterns, Arduino can determine which buttons are being pushed. As long as it follows this rule, you can use other resistors as well.
With this method, the amount of pins used by Arduino will decrease significantly. But it still takes up a lot of space on the breadboard. So if you want more, try using an IC called shift register, which limits input and output.
Picture 7 Mounting the circuit once you have arranged the parts
Picture 8 The interface is complete!
Once the interface is done, let’s move on to the program.
We will be using the same program as before to connect Arduino to processing using OSC. In this case, the controller status is being monitored by analog A0~2, so we will send that value to processing.
OSC program from Arduino to Processing server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#include #include #include //for OSC byte myMac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; byte myIp[] = { 192, 168, 11, 177 }; int destPort = 12000; byte destIp[] = { 192, 168, 11, 2}; OSCClient client; OSCMessage global_mes; //end for OSC const int buttonPin0 = A0; const int buttonPin1 = A1; const int buttonPin2 = A2; const int ledPin = 7; int buttonState0 = 0; int buttonState1 = 0; int buttonState2 = 0; void setup() { Serial.begin(9600); Ethernet.begin(myMac ,myIp); pinMode(ledPin, OUTPUT); pinMode(buttonPin0, INPUT_PULLUP); //Enable pull up resistor in input mode pinMode(buttonPin1, INPUT_PULLUP); //Enable pull up resistor in input mode pinMode(buttonPin2, INPUT_PULLUP); //Enable pull up resistor in input mode } void loop(){ // buttonState = digitalRead(buttonPin); buttonState0 = analogRead(buttonPin0); buttonState1 = analogRead(buttonPin1); buttonState2 = analogRead(buttonPin2); Serial.print(buttonState0); Serial.print(":"); Serial.print(buttonState1); Serial.print(":"); Serial.println(buttonState2); sendOSC(0,buttonState0); sendOSC(1,buttonState1); sendOSC(2,buttonState2); //Limit LED by value if (buttonState0 == LOW) { // pin value is low if the button has been pushed digitalWrite(ledPin, HIGH); } else { digitalWrite(ledPin, LOW); } } //Send OSC void sendOSC(int pin,int val){ global_mes.setAddress(destIp,destPort); if(pin == 0){ global_mes.beginMessage("/pin01"); } else if(pin == 1){ global_mes.beginMessage("/pin02"); } else if(pin == 2){ global_mes.beginMessage("/pin03"); } //global_mes.addArgString(1.0); //global_mes.addArgFloat(1.0); global_mes.addArgFloat(val); client.send(global_mes); global_mes.flush(); delay(100); } |
Next, let’s look at the program on the Processing side. The flow on the Processing side will look like this.
With this flow, it is possible to communicate between Arduino – Processing – Sonic Pi. Some may ask, “Can’t I just connect Arduino and Sonic Pi directly?” Actually, we tried that first, but it seems that Sonic Pi can only communicate with OSC in a local host (network within the PC), so we had to throw in the Processing in between.
Program for the Processing server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
import oscP5.*; import netP5.*; OscP5 oscP5; NetAddress myRemoteLocation; void setup() { size(400,400); frameRate(25); /* start oscP5, listening for incoming messages at port 12000 */ oscP5 = new OscP5(this,12000); myRemoteLocation = new NetAddress("127.0.0.1",4557); } String code; float speed = 1.0; int[] ch1 = {0,0,0,0}; int[] ch2 = {0,0,0,0}; void changeCode(){ code = "speed="+str(speed)+";live_loop :foo do;"; code +="sample :bd_haus,rate:1,amp:"+str(ch1[0]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch1[1]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch1[2]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch1[3]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch2[0]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch2[1]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch2[2]); code += ";sleep speed;sample :bd_haus,rate:1,amp:"+str(ch2[3]); code += ";sleep speed;end;"; println(code); } void draw() { background(0); } void mousePressed() { /* in the following different ways of creating osc messages are shown by example */ OscMessage myMessage = new OscMessage("/run-code"); changeCode(); myMessage.add(code); /* send the message */ oscP5.send(myMessage, myRemoteLocation); } void keyPressed() { OscMessage myMessage = new OscMessage("/run-code"); if(key == CODED) { if(keyCode == UP) { myMessage.add("use_synth :piano"); /* add an int to the osc message */ }else if (keyCode == DOWN) { stopSonic Pi(); }else if (keyCode == LEFT) { myMessage.add("use_synth :dull_bell"); /* add an int to the osc message */ }else if (keyCode == RIGHT) { myMessage.add("use_synth :beep"); /* add an int to the osc message */ } } /* send the message */ oscP5.send(myMessage, myRemoteLocation); } void stopSonic Pi() { /* in the following different ways of creating osc messages are shown by example */ OscMessage myMessage = new OscMessage("/stop-all-jobs"); myMessage.add(""); oscP5.send(myMessage, myRemoteLocation); } /* incoming osc message are forwarded to the oscEvent method. */ void oscEvent(OscMessage theOscMessage) { /* print the address pattern and the typetag of the received OscMessage */ /* print("### received an osc message."); print(" addrpattern: "+theOscMessage.addrPattern()); println(" typetag: "+theOscMessage.typetag()); theOscMessage.print(); */ String ptn = theOscMessage.addrPattern(); int val; // println("PATTERN:"+ptn); if(ptn.equals("/pin01")){ val = int(theOscMessage.get(0).floatValue()); print("pin01-"); println(val); if(val == 231){ int[] ch = {0,0,0,0};ch1 = ch;} else if(val == 242){ int[] ch = {1,0,0,0};ch1 = ch;} else if(val == 281){ int[] ch = {0,1,0,0};ch1 = ch;} else if(val == 295){ int[] ch = {0,0,1,0};ch1 = ch;} else if(val == 334){ int[] ch = {0,0,0,1};ch1 = ch;} else if(val == 447){ int[] ch = {1,1,0,0};ch1 = ch;} else if(val == 523){ int[] ch = {1,0,1,0};ch1 = ch;} else if(val == 284){ int[] ch = {1,0,0,1};ch1 = ch;} else if(val == 647){ int[] ch = {0,1,1,0};ch1 = ch;} else if(val == 703){ int[] ch = {0,1,0,1};ch1 = ch;} else if(val == 825){ int[] ch = {0,0,1,1};ch1 = ch;} else if(val == 844){ int[] ch = {1,1,1,0};ch1 = ch;} else if(val == 682){ int[] ch = {0,1,1,1};ch1 = ch;} else if(val == 922){ int[] ch = {1,1,1,1};ch1 = ch;} } else if(ptn.equals("/pin02")){ val = int(theOscMessage.get(0).floatValue()); print("pin02-"); println(val); if(val == 231){ int[] ch = {0,0,0,0};ch2 = ch;} else if(val == 242){ int[] ch = {1,0,0,0};ch2 = ch;} else if(val == 281){ int[] ch = {0,1,0,0};ch2 = ch;} else if(val == 295){ int[] ch = {0,0,1,0};ch2 = ch;} else if(val == 334){ int[] ch = {0,0,0,1};ch2 = ch;} else if(val == 447){ int[] ch = {1,1,0,0};ch2 = ch;} else if(val == 523){ int[] ch = {1,0,1,0};ch2 = ch;} else if(val == 284){ int[] ch = {1,0,0,1};ch2 = ch;} else if(val == 647){ int[] ch = {0,1,1,0};ch2 = ch;} else if(val == 703){ int[] ch = {0,1,0,1};ch2 = ch;} else if(val == 825){ int[] ch = {0,0,1,1};ch2 = ch;} else if(val == 844){ int[] ch = {1,1,1,0};ch2 = ch;} else if(val == 682){ int[] ch = {0,1,1,1};ch2 = ch;} else if(val == 922){ int[] ch = {1,1,1,1};ch2 = ch;} } else if(ptn.equals("/pin03")){ float val2 = int(theOscMessage.get(0).floatValue()); print("pin03-"); print(val2); print(":"); speed =(val2-340)/408; //0~100% println(speed); } mousePressed(); } |
Once we’ve written the programs, run them, and we’re done! In this program, each toggle switch can turn on/off bass drum, and we assigned the rhythm speed for the volume resistor. By changing this program, you can use it as another instrument aside from a rhythm machine.
※ For more information about Sonic Pi programs or how to use them, check out Audio programming with Sonic Pi!
Try it out, it should work like this.
In the second part, we applied our knowledge about the OSC and created a musical instrument device. When creating your own device, you can choose where to place the knobs and buttons, and what each of them transmits. You can create controllers that control more than just sounds. You can program it to control videos simultaneously, or use ultrasonic wave sensors or a photo reflector to create a contactless Theremin-like controller. If you’re a musician, it may be interesting to create your own instrument and perform on stage.