mixture-art@Q
技術+アイディア+世俗+なんとなく思ったこと、すべての融合がmixture-art
DIY Arduino-MIDIコントローラーの作り方を説明する

最近友人たちから「こんなことって実現できる?」と
ハードウェア絡みの質問を受け取ることが多い。
時代はハードウェア活況だなぁと噛み締めると同時に、
きっちり『エンジニアとしてできることの幅』を広げておかなければなるまい、
と気を引き締めるのでありまス。

ということでとある友人の要望から派生して
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で動作確認済み。



Comments are closed.