はじめての電子工作超入門

第31回 OSC通信でArduinoと他のアプリを連携させてみる〜Sonic Piとの連携でリズムマシン(後半)

DSC_0175

前回、ArduinoでOSC通信の基礎を学びました。今回は、OSC(正式名称Open Sound Control)が本来楽器用の通信プロトコルとして生まれていますので、その名にふさわしく楽器デバイスを作ってみます。

どんなデバイスを作ろうか悩んでいたところ、ArduinoからOSC通信対応可能なキーボードなどの楽器を操作するのでも良かったのですが、いろいろ調べたところ「IT女子のラズベリーパイ入門奮闘記」で紹介されていたRaspberry Piで動くSonic PiというサウンドプログラミングアプリがOSC通信に対応しているとのことで、今回Sonic Piと連携させてリズムマシンを作成してみます。

今回の電子工作レシピ

時間目安:150分
必要なパーツ

システム全体の構成を考える

まずはじめに全体の構成を考えます。基本的な流れとしては、Arduinoで作ったコントローラー回路から、OSC通信でProcessingサーバーへデータを送信、Processingサーバーでコントローラーの入力値を条件に基づいてSonic Pi用のサウンドプログラムに変換して、Sonic Piに送信(Processing – Sonic PiもOSC通信)して音を鳴らす、という構成になります。詳しいことはわからなくても大丈夫ですが、Sonic Piのコア部分はrubyというプログラムで動作しており(インタフェース側はまた別のプログラム)、今回そのrubyに対してOSC通信を行う仕様になります。

図1 システムの流れ

図1 システムの流れ

 

図2 サウンドプログラミング環境のSonic Pi

図2 サウンドプログラミング環境のSonic Pi

Sonic Piに関しては、こちらの記事で詳しく紹介しています。
Sonic PiはRaspberryPi以外にもWindows / MacOSで動作するとのこと。前回利用したProcessing環境がすでにありますので、MacOS版をダウンロードして使ってみます。

システムの構成を把握した上で、実際にデバイスの作成を進めていきましょう。

 

リズムマシンコントローラー回路の作成

まずは、何と言っても操作するコントローラー部分を作っていきましょう。どうせならかっこいいデバイスに!と思ったのですが、普段使う電子工作のケースだと、平たく大きめでちょうど良さそうなケースがなかったため、いろいろケースに合いそうなものを探していると……

写真1 お箸の箱

写真1 お箸の箱

おお、なんとちょうどよいサイズのお箸の箱をみつけました。深さもちょうど良さそうです。

写真2 深さもちょうど良さそうです

写真2 箱の中

漢字が書かれた箱をリズムマシンにするのはなんとも言えない趣が立ち込めますね。兎にも角にも良いケースが見つかったので、回路を作成していきます。

 

コントローラーのインタフェースを考える

いきなり回路を作成する前に、実際にどのようなインタフェースにするか、パーツを並べてイメージをしてみます。実際に回路を組むときにさまざまな制約がでてくるので、イメージ通りにはいかないこともありますが、最初にどこの箇所にどのパーツを置いたら使いやすいか、アダプタやLANケーブルなどの線が出る場所が適切か (複数の側面だと線が綺麗にまとまらない)など、いろいろ悩みながら仕様を決めていきます。いつまででも悩んでいられる楽しい時間ですね。

写真3 載せる部品を考える

写真3 パーツを載せてインタフェースを考える

今回、リズムマシンということなので、8つのトグルスイッチでオンオフして、8ビートのリズムを操作できることを基本にパーツを配置しました。今回、Arduinoにはケースの深さの関係でArduino Ethernet(Arduinoとイーサネットシールドが一緒になったタイプ)を利用しています。写真3の上にある7セグLEDもつけたいところですがArduinoのピンの数が足りなくなりそうなので、ひとまず可能であれば、というくらいで考えておきます。

また、今回蓋には透明のアクリルを採用しました。アクリルにパーツ用にドリルなどで穴をあけて実装していきます。

 

いざ、回路作成

実際に配置を決めたら、ケースを加工して回路を作成していきます。

写真4 ケースをパーツに合わせて加工

写真4 ケースをパーツに合わせて加工

 

写真5 パーツ用にドリルでアクリルに穴をあける

写真5 パーツ用にドリルでアクリルに穴をあける

 

写真6 パーツを配置

写真6 パーツを配置

ある程度パーツをケースに配置したら、回路を作成していきます。今回パーツの点数が多いため、ブレッドボードのスペースやArduinoのピンを節約する必要が出てきました。単純に8つのトグルスイッチをArduino側で8つピンをそのまま利用してしまうと、改良したいときに他にパーツを増やせなくなってしまうなど可能性が狭まりますので、できる限り回路上で工夫をしてみます。

 

図3 リズムマシンインタフェース回路

図3 リズムマシンインタフェース回路

今回の回路は、8つのトグルスイッチを4つずつにわけて、入力状態を判定できるようにしています。以前スピーカー編では抵抗値が違うものを並べて、どのスイッチが押されたかわかるようにしました。今回もその方法を利用します。(実際のデバイスにはLEDも付けていますが回路の説明のため、省略しています)

以前は複数押されている場合の判定をしていませんでしたが、今回はその必要があるため、もうちょっとだけ工夫します。抵抗値に1KΩ、2KΩ、4KΩ、8KΩ用意して、複数箇所押された場合でも、Arduino側の入力値で判別できるようにしました。簡単に説明すると、先頭から抵抗が1KΩ、2KΩ、4KΩ、8KΩだとすると、1番目を押しているときは1、1番目2番目を押しているときは合計3、1番目3番目を押しているときは合計5などというように全部押したパターンの合計値が違えば、Arduino側で状態を判定することができるということです。このルールに沿っていれば抵抗は上記のもの以外でも大丈夫です。

この方法だと、Arduinoが利用するピンはだいぶ減ります。とは言えまだまだブレッドボードのスペースをかなり占有してしまうため、さらに、という方は、シフトレジスターという入出力を制御してくれるICがありますので、これを調べてみて下さい。

 

写真7 パーツを配置したら回路を実装

写真7 パーツを配置したら回路を実装

 

写真8 インタフェース完成!

写真8 インタフェース完成!

インタフェースが完成したところで、プログラムに移りましょう。

 

OSCプログラムを実装

Arduino側からProcessingへOSC通信で送信するプログラムは、基本的に前回と同じ方法です。今回はアナログA0〜2番でコントローラーの状態を監視しているので、その値をProcessingに送信します。

 

ArduinoからProcessingサーバーへOSC送信プログラム

#include <SPI.h>
#include <Ethernet.h>
#include <ArdOSC.h>

//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); // Inputモードでプルアップ抵抗を有効に
 pinMode(buttonPin1, INPUT_PULLUP); // Inputモードでプルアップ抵抗を有効に
 pinMode(buttonPin2, INPUT_PULLUP); // Inputモードでプルアップ抵抗を有効に
}

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);
 
 //数値によってLEDを制御
 if (buttonState0 == LOW) { // ボタンが押されていたら、ピンの値はLOW
 digitalWrite(ledPin, HIGH); 
 } 
 else {
 digitalWrite(ledPin, LOW); 
 }
}

//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);
}

次に、Processing側のプログラムです。Processing側の処理では、下記の流れになります。

  1. [OSC受信]Arduino側から入力値を受け付ける
  2. 入力値に基づいてSonic Piのコードを作成
  3. [OSC送信]ProcessingからSonic Piへコード(/run-code)を送信
  4. Sonic Piで受信したコードを再生する

この流れで、Arduino – Processing – Sonic Piの通信が可能となります。ここで、「Arduino – Sonic Piが直接通信できないの?」と思われる方もいるかと思います。実は今回、初めにその方法を試したのですが、Sonic Piが基本的にはローカルホスト(自分自身のPC内のネットワーク)でしかOSC通信ができないような仕組みになっているため、Processingを挟んだ結果となっています。

Processingサーバープログラム

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();
}

 

リズムマシンの完成!

プログラムを書き込んだらそれぞれのプログラムを動作させて完成です!今回のプログラムでは、トグルスイッチ一個一個にバスドラムの音のオンオフ、ボリューム抵抗はリズムの速さを割り当てました。このプログラムを変更することで、リズムマシン以外の楽器としても利用することができます。

※Sonic Piのプログラム、使い方についてはIT女子のラズベリーパイ入門奮闘記第17回「Sonic Piで音プログラミング!」をごらんください。

  • スイッチをONにすればバスドラムの音が鳴る
  • 全部ONにすれば、切れ目無く鳴り続ける
  • ボリュームつまみでテンポが変わる

……ということがわかると思います。

まとめ

後半編はOSCの応用編として楽器デバイスを作成しました。自分でデバイスを作る場合、好きな場所につまみやボタンを配置できたり、送信する内容も自由に考えられます。今回のようにサウンドプログラミングのコントローラーだけではなく、同時に映像なども操作可能なコントローラーにしたり、超音波センサやフォトリフレクタなどを利用して、非接触なテルミンのような操作コントローラーを作成したりすることも可能です。楽器を演奏される方は、自作楽器でステージに立ってみるのも面白いかもしれないですね。

電子工作マニュアル Vol.3
赤川シホロ

電子工作や新しいデバイスをこよなく愛するエンジニア。日常生活のちょっとしたことを電子工作で作って試して、おもしろく過ごしたいと日々考えています。