モバイルアプリ(iOS/Android)とBluetooth経由でMIDIメッセージをやり取りするデバイスを製作したいと思い、良いモジュールがないか探してみたところ、以下の基板が見つかりました。
Bluetooth MIDI 基板(QUICCO SOUND 社)
この基板だけでは動作できないので、Arduinoと組み合わせて1つのデバイスとします。
ハンダ付け
まず、Bluetooth MIDI 基板にある4箇所のピンにArduinoと組み合わせるためのワイヤ線をハンダ付けします。(これがなかなか大変でした)
[左上] Tx(MIDI入力) [右上] Vcc(電源)
[左下] Rx(MIDI出力) [右下] GND(グランド)
配線図
ArduinoとBluetooth MIDI 基板を組み合わせたデバイス(以降「デバイス」)の配線は下図のようになります。
【重要】
下の配線図は、筆者が試したものであって、メーカーが了承や推奨をしているものではありません。実際に試される際は、基板を購入時に同封の「取扱説明書」をよく確認の上、配線を行ってください。
この配線の状態でArduinoに電源を供給して、Bluetooth MIDI 基板にあるLEDが(約1秒ごとに)明るく点滅すれば正常な状態です。もしそうでない動作をした場合、ハンダ付けの状態を確認するなどしてみてください。
右側のブレッドボードにあるスイッチは、Arduino側からMIDIメッセージを送信するテストとして使用します。(デバイスを受信のみとして使用する場合は、スイッチを含む右側のブレッドボードは不要です。)
配線と接続に関する注意点です。
- Arduinoで5Vの電源に接続する場合、必ず抵抗(220Ω)を入れてください。(故障の恐れがあるとのことです)
- Arduino側のRx(MIDI出力)のピン(上図では12ピン)は、内部プルアップを有効にしてください。
- 配線図にあるスイッチは、内部プルアップを有効にすることを想定しています。もしそうしない場合は、抵抗(10kΩ)を入れてください。
- Bluetooth MIDI 基板のMIDI信号ピンのArduinoへの配線は、今回は「ソフトウェアシリアル」を用いているため、独自に12ピン/13ピンを選択しています。(下記のArduinoのサンプルプログラムについてもこの想定です)
プログラム
デバイスの準備は整いましたので、モバイルアプリとデバイス側(Arduino)のプログラムを確認していきます。
それぞれMIDIに関する箇所を抜粋して説明します。(MIDIに関する基本的な内容については、他で調べていただけると助かります)
モバイルアプリについては、iOSについて説明します。(Androidは結構ボリュームがあったので、またの機会で。。)
詳細は、以下のGitHubのリンクに置いた、iOSのサンプルプロジェクトを参照してください。
Arduinoのサンプルプログラムは、本記事の最後に全てを載せました。
準備(iOS)
CoreAudioKitフレームワークを利用することで、Bluetooth経由のMIDI接続は、セントラル/ペリフェラルいずれからも簡単に行うことができます。今回はセントラルの方を使って、デバイスとの接続を行います。
Bluetooth MIDIデバイスの名称が「mi.1」のものが見つかれば、それがBluetooth MIDI基板になります。
#import <CoreAudioKit/CoreAudioKit.h>
// Central
CABTMIDICentralViewController *pCentralVC = [[CABTMIDICentralViewController alloc] init];
[self.navigationController pushViewController:pCentralVC animated:YES];
// Peripheral
CABTMIDILocalPeripheralViewController *pPeripheralVC = [[CABTMIDILocalPeripheralViewController alloc] init];
[self.navigationController pushViewController:pPeripheralVC animated:YES];
MIDIの送信/受信については、「PGMidi」のプログラムを利用させていただきました。
PGMidiの宣言、オブジェクトの定義を行います。
#import "PGMidi.h"
@interface ViewController () <PGMidiDelegate, PGMidiSourceDelegate>
{
PGMidi *_midi;
}
@end
PGMidiのインスタンスの生成、Delegateのセットなどを行います。
- (void)viewDidLoad
{
[super viewDidLoad];
_midi = [[PGMidi alloc] init];
_midi.networkEnabled = YES;
_midi.delegate = self;
for (PGMidiSource *source in _midi.sources) {
[source addDelegate:self];
}
_midi.virtualDestinationEnabled = YES;
_midi.virtualSourceEnabled = YES;
}
準備(Arduino)
ソフトウェアシリアルを宣言して、シリアル通信用として、12ピンをRx(入力)、13ピンをTx(出力)として定義します。
#include <SoftwareSerial.h>
int MY_BLE_Rx = 12;
int MY_BLE_Tx = 13;
SoftwareSerial mySerial(MY_BLE_Rx, MY_BLE_Tx);
Rxの内部プルアップを有効にします。(必須)
MIDIのシリアル通信の転送レートは「31250 bps」としてください。(異なる値にすると、正しく動作しません)
void setup() {
pinMode(MY_BLE_Rx, INPUT_PULLUP); // 内部プルアップ
pinMode(MY_BLE_Tx, OUTPUT);
// MIDI
mySerial.begin(31250);
}
モバイル → デバイス (iOS → Arduino)
モバイルアプリ(の送信ボタン)から送信されたMIDIメッセージを、デバイス側で受信する場合です。
モバイル(iOS)
MIDIメッセージを送信します。
PGMidiの sendBytes:size メソッドを使用します。これは、CoreMIDIフレームワークの MIDIPacketListAdd() 関数(なんとdeprecated。。)を呼び出しています。
以下はノートオンを送信する例です。([ノートオン + チャンネル], [キー], [音量])
const UInt8 noteOn[] = { 0x90 + iIndex, iNote, iVelocity };
[_midi sendBytes:noteOn size:sizeof(noteOn)];
デバイス(Arduino)
MIDIメッセージを受信します。
ソフトウェアシリアルの available() メソッドでバッファにあるデータのバイト数を確認して、read() メソッドでデータを読み込みます。
その後、ステータスバイト(status byte: 128 ~ 255)かデータバイト(data byte: 0 ~ 127)かの確認を行います。
サンプルでは、ノートオン/オフのみを想定していますので、3バイトを受け取ったら1つのMIDIメッセージとして判断しています。
void loop() {
byte data_size = mySerial.available();
if (data_size > 0) {
byte bReceiveData = mySerial.read();
int isStatus = bReceiveData & 0x80;
if (isStatus == 0x80) {
// status byte
...
} else {
// data byte
...
}
...
}
}
デバイス → モバイル (Arduino → iOS)
デバイス(のスイッチ)から送信されたMIDIメッセージを、モバイルアプリ側で受信する場合です。
デバイス(Arduino)
スイッチは9ピンとGNDを繋ぐので、変数SW_PINを定義しておきます。
内部プルアップを有効にすることで、スイッチと9ピンの間に抵抗(10kΩ)を入れる必要がなくなるので便利です。
int SW_PIN = 9;
void setup() {
pinMode(SW_PIN, INPUT_PULLUP); // 内部プルアップ
}
MIDIメッセージを送信します。
digitalRead() メソッドでスイッチの状態を確認します。サンプルでは、押したときと離したときのタイミングを取得するためにフラグの変数を用意しています。
void loop() {
if (digitalRead(SW_PIN) == HIGH) {
// スイッチは押されていない状態
...
} else {
// スイッチが押された状態
...
}
}
モバイル(iOS)
MIDIメッセージを受信します。
ノートオン/オフを受信すると、PGMidiのDelegateメソッド midiSource:midiReceived が呼び出されます。
これは、CoreMIDIフレームワークの MIDIInputPortCreate() 関数(これもdeprecated。。)の第3引数で定義しているコールバック関数 PGMIDIReadProc() が呼び出されることで、midiSource:midiReceived も呼び出されています。
- (void)midiSource:(PGMidiSource *)midi midiReceived:(const MIDIPacketList *)packetList
{
const MIDIPacket *packet = &packetList->packet[0];
for (int i = 0; i < packetList->numPackets; ++i) {
//
// packet->data のバイト列がMIDIメッセージ
//
packet = MIDIPacketNext(packet);
}
}
Arduino 全プログラム
スイッチによる送信テストは、「ド」の音のノートオン/オフを行います。
モバイルアプリから受信したMIDIメッセージは、ノートオン/オフのみを処理します。
送信/受信したバイト列は、Arduinoソフトウェアのシリアルモニタで確認することができます。
#include <SoftwareSerial.h>
// BLE MIDI
int MY_BLE_Rx = 12; // RX-I of bluetooth
int MY_BLE_Tx = 13; // TX-O of bluetooth
SoftwareSerial mySerial(MY_BLE_Rx, MY_BLE_Tx);
// 送信
int SW_PIN = 9;
int isChangeSwOn = 0;
// 受信
byte arNoteData[3] = {};
int iNoteCount = 0;
void setup() {
// BLE MIDI
pinMode(MY_BLE_Rx, INPUT_PULLUP); // 内部プルアップ
pinMode(MY_BLE_Tx, OUTPUT);
// 送信
pinMode(SW_PIN, INPUT_PULLUP); // 内部プルアップ
// シリアルモニタ用
Serial.begin(9600);
// MIDI
mySerial.begin(31250);
}
void loop() {
// 送信テスト
if (digitalRead(SW_PIN) == HIGH) {
// スイッチは押されていない状態
if (isChangeSwOn == 1) {
// NOTE OFF
sendMIDI(0x80, 60, 0);
isChangeSwOn = 0;
Serial.println("NOTE OFF");
}
} else {
// スイッチが押された状態
if (isChangeSwOn == 0) {
// NOTE ON
sendMIDI(0x90, 60, 127);
isChangeSwOn = 1;
Serial.println("NOTE ON");
}
}
// 受信テスト
byte data_size = mySerial.available();
if (data_size > 0) {
byte bReceiveData = mySerial.read();
int isStatus = bReceiveData & 0x80;
if (isStatus == 0x80) {
// status byte
// reset
for (int i = 0; i < 3; i++) {
arNoteData[i] = 0;
}
iNoteCount = 0;
int isNoteOn = bReceiveData & 0x90;
int isNoteOff = bReceiveData & 0x80;
if ((isNoteOn == 0x90) || (isNoteOff == 0x80)) {
arNoteData[iNoteCount] = bReceiveData;
iNoteCount ++;
}
} else {
// data byte
int isNoteOn = arNoteData[0] & 0x90;
int isNoteOff = arNoteData[0] & 0x80;
if ((isNoteOn == 0x90) || (isNoteOff == 0x80)) {
arNoteData[iNoteCount] = bReceiveData;
iNoteCount ++;
}
}
if (iNoteCount == 3) {
Serial.println("Note: " + String(arNoteData[0]) + " " + String(arNoteData[1]) + " " + String(arNoteData[2]));
// wait
delay(30);
}
}
}
// 送信
void sendMIDI(byte type, byte note, byte velocity) {
byte data[3] = {type, note, velocity};
mySerial.write(data, sizeof(data));
}
最後に
双方向からのノートオン/オフのやり取りをできることが確認できました。
今後の発展としては、以下のようなものが考えられると思います。
- デバイス側に複数のスイッチを追加して、モバイルアプリで受信したノートオンで音を鳴らすことで、自作のシンセサイザーを作成。
- デバイス側に複数のLEDを追加して、モバイルアプリの各音程の送信ボタンを押して、それぞれアサインしたLEDを光らせる。
Bluetooth MIDI 基板を試していただけたら、感想やアイデアなど共有できるとうれしいです。