
最近友人たちから「こんなことって実現できる?」と
ハードウェア絡みの質問を受け取ることが多い。
時代はハードウェア活況だなぁと噛み締めると同時に、
きっちり『エンジニアとしてできることの幅』を広げておかなければなるまい、
と気を引き締めるのでありまス。
ということでとある友人の要望から派生して
Arduino-MIDIコントローラというものにトライしてみた。
構成は単純で
スイッチ → Arduino → Mac上のAbleton Live
という接続で曲をコントロールしてみたのでそのメモを残す。
ちなみにMac前提で話を進めてしまうのでそこはご了承を。
ちょっと説明不足で不親切な部分もあるが、まぁそれは気が向いたときに追記することとする。
完成品のデモビデオ(笑)はこちら
とりあえず基板を作る。半田ごて握るべし
さてスイッチはなんでも良いし、当然動画にあったようなスライダーなんかも可能だ。
MouserやDigikeyなどで購入するといいだろう。
http://mouser.com
http://digikey.com
スイッチは全て
・片方のピンをGND
・もう片方のピンをArduinoのGPIOポート
というように接続する
ピンに直接接続しても構わないのはArduino上で『内部プルアップ』するからだ。
コードを参照して欲しい。
Arduinoのコーディングは一瞬
以下のコードはスイッチひとつだけのケース。(話をシンプルにしておく)
————————————————————-
switch_state = 0;
uint8_t temp = 0;
#define PIN_SW0 7 // ピン番号はお好みで
#define MIDI_0_0 0xb0
#define MIDI_0_1 0x01 // スイッチやノブを足す時はこのアサインを変える
#define MIDI_0_2 0x7f
void setup()
{
Serial.begin(38400);
delay(1000); // お好みで
pinMode(PIN_SW0, INPUT);
digitalWrite(PIN_SW0, HIGH); // 内部プルアップ
switch_state = digitalRead(PIN_SW0);
}
void loop()
{
uint8_t data[3] = {MIDI_0_0, 0, 0};
temp = digitalRead(PIN_SW0);
if(temp != switch_state && temp == 0){
data[1] = MIDI_0_1; // 複数スイッチがあるときはここを変える
data[2] = MIDI_0_2; // 複数スイッチがあるときはここを変える
for(int i = 0; i < 3; i++){
Serial.print(data[i]);
delay(1); // なぜこれを入れるかは後述
}
}
delay(50); // リンギングを嫌って50msのディレイを入れた
}
————————————————————-
とりあえず発行するMIDI信号は「0xB0 0x** 0x**」にしてある。
汎用的なシグナルチェンジのアサインだが
もしピッチベンドをやりたかったら「0xE0 0x** 0x**」など自由に書き換え可能だ。
ちなみに今回ArduinoはLeonardoを使った。
信号をどうスイッチからLiveまで送るかについてはちょっと頭をひねった
さてMac側では
『ArduinoのUSB Serialの信号をMIDIとして受け取る仕組み』
が必要だ。
MIDI出力を作ってそれをUSB-MIDIアダプタに入れて、、、とやっても良いがやぼったい。
またArduino自体をUSB MIDIデバイスとしてドライバを書き換える方法も存在するが、
それをやってしまうとISP(特別なプログラマ)を使ってプログラムを書き込まなきゃ
いけなくなるのでここではパス。
せっかくなのでPythonスクリプトを構築することにした。
Pythonをインストールしていない人はググってくださいな。ここでは省略。
さてPythonのコードはちょっとデバッグしながら書いた
simplecoremidiというライブラリを使うと簡単にできるのでそれを入れる。
ターミナルから以下を叩く
sudo pip install simplecoremidi
あとSerial通信に必要なのはpyserialだ。
こちらもインストールしよう。
sudo pip install pyserial
で、Pythonのコードはこんなカンジ。
from simplecoremidi import send_midi
import serial
usbport = ‘/dev/tty.usbmodem14541’ // セッティングに応じて変更必要
ser = serial.Serial(usbport, 38400, timeout=1)
data = [”,”,”]
data_count = 0
while (1):
result=ser.read(ser.inWaiting())
if result != “”:
data[data_count] = result
if data_count == 0:
while int(data[0]) < 0xb0:
// MIDI信号の最初のビットは必ず0xB0とか0xE0のはず、という想定で
result=ser.read(ser.inWaiting())
if result != “”:
data[0] = result
data_count = 1
else:
data_count = data_count + 1
if data_count > 2:
data_count = 0
send_midi((int(data[0]), int(data[1]), int(data[2])))
ser.close()
やっていることは3回USBシリアルポートから信号を取り込んで、
受け取った3bytesのメッセージをCoreMIDIに流し込んでいるだけ。
ところがここで問題が
ふとした拍子に受信バイトが分割されるなどでデータがおかしくなることがある。
そういう場合のために対処コードを多少入れた。
(合成や復元など複雑なことはせず、とりあえず破棄するようにしている)
ここで先ほどのdelay(1)の種明かしだが、本来はシリアルデータはだーっと送って
それを受け取った側で分割したり配列に入れ直したりするのだが、ここでは手抜きをした。
このコードでは1byteずつしか送られないため『単純に3回受け取れば良い』という寸法だ。
その分余計なディレイは当然入るが、用途から考えて問題ないと考える。
あと3byteオンリーでSysExメッセージには対応していないけど、
それもとりあえずヨシとする。
デバイスの検出は、、、まぁいいや
シリアルデバイス”/dev/tty.usbxxxxxxx”の部分はセッティングや使うArduino、
そして接続方法によっても変わるので確認して書き換えて欲しい。
確認方法はArduinoのIDEで “Tools” -> “Serial Port” というのを確認するか
ls /dev/ | grep tty | grep usb
とかやれば見つかるはずだ。
# きっと自動検出するコードが書けるのだろうけど別に売るものでもないので一旦コレで良しとする
ArduinoをMacに接続してさきほどのpythonスクリプトを起動しよう。
Ableton Live側の設定
さて、ここまでできたらお次はAbleton Liveでの設定について
これは単に自分が持っているソフトがLiveなのでそれでやっているだけなので
他のCoreMIDI対応ソフトがなんでも使える。
Liveのメニューの設定からMIDIのタブを選ぶと
“simplecoremidi”というのが見つかるはず。
そこで”Remote”の設定をONにしておけばこれでもう完了だ。
Live画面右上のMIDIを押すとMIDIアサインの設定状態になる。
アサインしたいところをクリックして次に対応させたい物理スイッチを押せばアサインが確定する。
ということでこれで演奏可能。自作Arduino-MIDIコントローラの完成だ。
(追記)
/dev/usb.ttyxxxxxx
を自動検出するコードは以下の形
import os
class Command(object):
def __init__(self, command):
self.command = command
def run(self, shell=True):
import subprocess as sp
process = sp.Popen(self.command, shell = shell, stdout = sp.PIPE, stderr = sp.PIPE)
self.pid = process.pid
self.output, self.error = process.communicate()
self.failed = process.returncode
return self
@property
def returncode(self):
return self.failed
com = Command("ls /dev |grep tty |grep usb").run()
print com.output
usbport = '/dev/' + com.output.rstrip('\n')
ser = serial.Serial(usbport, 9600, timeout=1)
大型追記w
変更点
・スライダーを追加
・ピンアサインは別ファイルに記載してマッピングを読み込む
・3byte以上のMIDIコマンドにも対応
・PythonのコードでのMIDIコマンド検出部分を整理(安定)
Arduinoのコード
#include "assign_map.h"
#include <MIDI.h>
uint8_t switch_state[6] = {0, 0, 0, 0, 0, 0};
uint8_t temp[6] = {0, 0, 0, 0, 0, 0};
uint16_t ad_value = 0;
#define DEBUG 0
#define ANALOG_SLIDER_ENABLE 1
void setup()
{
Serial.begin(38400);
delay(5000);
for(int i = 0; i < 6; i++){
pinMode(pins[i], INPUT);
digitalWrite(pins[i], HIGH);
switch_state[i] = digitalRead(pins[i]);
}
#if ANALOG_SLIDER_ENABLE
ad_value = analogRead(PIN_AD0);
#endif
#if DEBUG
Serial.print("AD_MAX = ");
Serial.println(AD_MAX);
Serial.print("AD_DIV = ");
Serial.println(AD_DIV);
#endif
}
void loop()
{
uint8_t data[3];
uint16_t ad_temp = analogRead(PIN_AD0);
#if DEBUG
Serial.print("AD = ");
Serial.println(ad_temp);
#endif
#if ANALOG_SLIDER_ENABLE
if(abs(ad_temp - ad_value) > 2* AD_DIV){
Serial.print('$');
Serial.print(MIDI_AD0_0);
Serial.print(',');
Serial.print(MIDI_AD0_1);
Serial.print(',');
ad_value = ad_temp;
ad_temp = (uint8_t)((double)ad_temp/AD_DIV);
if(ad_temp > 0x7F){ad_temp = 0x7F;}
Serial.print(ad_temp);
Serial.println('$');
}
#endif
for(int i = 0; i < 6; i++){
temp[i] = digitalRead(pins[i]);
if(temp[i] != switch_state[i] && temp[i] == 0){
Serial.print("$");
Serial.print(mapping[3*i]);
Serial.print(",");
Serial.print(mapping[3*i + 1]);
Serial.print(",");
Serial.print(mapping[3*i + 2]);
Serial.println('$');
delay(1);
}
switch_state[i] = temp[i];
}
delay(50);
}
ちみみに設定ファイルassign_map.hはこんな感じ。
#ifndef _ASSIGN_MAP_H_
#define _ASSIGN_MAP_H_
#define PIN_SW0 2
#define PIN_SW1 1
#define PIN_SW2 3
#define PIN_SW3 4
#define PIN_SW4 6
#define PIN_SW5 5
#define PIN_AD0 A0
#define MAX_RES 50
#define PULLUP_RES 10
#define AD_MAX 1024
double AD_DIV = (double)AD_MAX/128;
const char pins[6] = {PIN_SW0, PIN_SW1, PIN_SW2, PIN_SW3, PIN_SW4, PIN_SW5};
#define MIDI_0_0 0xb0
#define MIDI_0_1 0x01
#define MIDI_0_2 0x7f
#define MIDI_1_0 0xb0
#define MIDI_1_1 0x02
#define MIDI_1_2 0x7f
#define MIDI_2_0 0xb0
#define MIDI_2_1 0x03
#define MIDI_2_2 0x7f
#define MIDI_3_0 0xb0
#define MIDI_3_1 0x04
#define MIDI_3_2 0x7f
#define MIDI_4_0 0xb0
#define MIDI_4_1 0x05
#define MIDI_4_2 0x7f
#define MIDI_5_0 0xb0
#define MIDI_5_1 0x06
#define MIDI_5_2 0x7f
#define MIDI_AD0_0 0xb0
#define MIDI_AD0_1 0x07
const unsigned int mapping[3*6] =
{
MIDI_0_0, MIDI_0_1, MIDI_0_2,
MIDI_1_0, MIDI_1_1, MIDI_1_2,
MIDI_2_0, MIDI_2_1, MIDI_2_2,
MIDI_3_0, MIDI_3_1, MIDI_3_2,
MIDI_4_0, MIDI_4_1, MIDI_4_2,
MIDI_5_0, MIDI_5_1, MIDI_5_2,
};
#endif
Python側はこんな感じ。
from simplecoremidi import send_midi
import time
import serial
import os
class Command(object):
"""Run a command and capture it's output string, error string and exit status"""
def __init__(self, command):
self.command = command
def run(self, shell=True):
import subprocess as sp
process = sp.Popen(self.command, shell = shell, stdout = sp.PIPE, stderr = sp.PIPE)
self.pid = process.pid
self.output, self.error = process.communicate()
self.failed = process.returncode
return self
@property
def returncode(self):
return self.failed
com = Command("ls /dev |grep tty |grep usb").run()
print com.output
usbport = '/dev/' + com.output.rstrip('\n')
ser = serial.Serial(usbport, 38400, timeout=1)
while (1):
result=ser.read(ser.inWaiting())
while(result.find("$") < 0):
result=ser.read(ser.inWaiting())
temp = result.find("$")
while(result.rfind('$') == temp):
result=ser.read(ser.inWaiting())
data = result.split("$")[1].split(",")
if len(data) >= 3:
for i in range(len(data)):
data[i] = int(data[i])
print data
send_midi((data))
ser.close()
Yosemiteで動作確認済み。

