概要
M5StickCで作ったBLE MIDIコントローラーのMicro:bit版の記事です。
*このソースは、Micro:bit v1.3でしか動かず、現行品のv1.5では動きません。v1.3 -> v1.5で加速度センサが変わったにも関わらず、MBedのライブラリが更新されていないためです。(当然ですが、JavaScriptの方では動きました。)公式のMBedのページでもこの話題が議論されていますが、放置状態です。
時間見つけてv1.5対応版にも取り組んでみたいと思います。
'20/6/13追記
MBedのLSM303AGRのライブラリ使って動かそうとしてみたんですがダメでした。加速度センサのクラスのインスタンスが何しても出来ないという有様...
データシートの仕様読み込んで作ればいいんでしょうが、お手軽にプログラムできるのがMBedのいいところなので、そこまでやりたくないって言うのがホンネ...
M5Stack, M5stickCの限界
M5Stack, M5StickCで作ったBLE MIDIコントローラーは、おもちゃとしては良いのですが、「楽器」と呼ぶには少し厳しい、と感じていました。
M5StackもM5StickCも、
- MIDI信号送信間隔が、10ms程度までしか詰められないのでは、人間の耳には音の変化が不自然に聞こえる。
- IMUもADCも精度が怪しい。
ためです。
1は、ランニングステータス等の工夫で何とかなるかなあ、とも思ったんですが、
- 元々、ESP32のBluetooth周りの出来は良くない、という話がある。
- BLEライブラリは第三者の作った非公式のライブラリで、動けばOKくらいの出来の可能性がある。
と、頑張っても無駄な気がし、2の問題も解決できそうにないので、取り組むのを諦めました。
なぜMicro:bit?
上述の問題の解決策としたのが、Nordic社のBLEが搭載されているボードを使う、でした。Nordic社は、BLE業界でNo1のシェアを誇る半導体メーカーで、ここのBLE使って作ってダメならもう何使ってもダメだろうと考えたためです。
Nordic社のICを使った諸々のボードがある中で、Micro:bitを選んだのは、入手性と価格です。本当は、Arduino Nano33 BLE SenseがSwitch Scienceさんから発売されたら、これで挑戦することを考えていたのですが、いつまで経っても発売される気配がないんですよね...。輸入で購入して遊ぶことも考えたのですが、多分、電波法違反なので、おおっぴらに使うことはできませんし。
開発環境
公式のMakeCodeでもMicroPythonでもなく、MBedの環境で作成します。MakeCodeでBLE MIDIコントローラ作れるような雰囲気はあるんですが作れませんでした。MicroPythonは、BLEが扱えません。
Micro:bitは、主にSTEM用に作られているので、おおっぴらにはされていないのですが、MBedでも5x5のLEDなどが簡単に扱えるようなライブラリが公開されており(というか、MakeCodeは、これのラッパー)、大部分のプログラムは、これを利用します。
ただ、肝心の独自BLE ProfileであるBLE MIDI部分は、自分でいろいろ見ながら作れ、となっており、ここをどうしたのか?、が今回の記事のソースのポイントになります。
開発環境構築
kosakalab様のmicro:bit + mbed または micro:bit + Arduino IDE でプログラム開発(備忘) を参考にしてみてください。
構成
- Micro:bit
- KORG Gadget2 for iPad(iPhoneでも可)
ソース
#include "MicroBit.h"
const static char DEVICE_NAME[] = "MY_MICROBIT_BLE_MIDI";
const uint8_t MIDI_SERVICE[16] = {
0x03, 0xb8, 0x0e, 0x5a, 0xed, 0xe8, 0x4b, 0x33, 0xa7, 0x51, 0x6c, 0xe3, 0x4e, 0xc4, 0xc7, 0x00
};
const uint8_t MIDI_CHAR[16] = {
0x77, 0x72, 0xe5, 0xdb, 0x38, 0x68, 0x41, 0x12, 0xa1, 0xa9, 0xf2, 0x66, 0x9d, 0x10, 0x6b, 0xf3
};
MicroBit uBit;
Serial pc(USBTX, USBRX);
bool isConnect = false;
//CC Buff
uint8_t cc[128];
//Pitch Buff
uint16_t pitch;
//imu buff
int x, y;
//BtnBuff
bool btnBuff[2];
//MIDI Charactarastics/Service
uint8_t midiPayload[5] = { 0x80, 0x80, 0x00, 0x00, 0x00 };
GattCharacteristic midiChar (MIDI_CHAR,
midiPayload, 5, 5,
GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
GattCharacteristic *midiChars[] = {&midiChar, };
GattService midiService(MIDI_SERVICE, midiChars,
sizeof(midiChars) / sizeof(GattCharacteristic *));
//Map and Limit
int mapAndLimit(int value, int fromLow, int fromHigh, int toLow, int toHigh);
//Midi Notify
void notifyNote(uint8_t note, uint8_t velocity);
void notifyCC(uint8_t ccNum, uint8_t value, uint8_t sensitivity);
void notifyPitch(uint16_t value, uint8_t sensitivity);
//BLE Connection Callback
void onConnectionCallback(const Gap::ConnectionCallbackParams_t *params)
{
isConnect = true;
pc.printf("connected. Got handle %u\r\n", params->handle);
uBit.display.printChar('C');
}
void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
isConnect = false;
pc.printf("Disconnected handle %u, reason %u\r\n", params->handle, params->reason);
pc.printf("Restarting the advertising process\r\n");
uBit.display.printChar('A');
pc.printf("Start Advertising\r\n");
uBit.ble->gap().startAdvertising();
}
int main()
{
//InitializeBtnBuff
for(int i=0; i < 2; i++)
{
btnBuff[i] = false;
}
//InitializeCC
for(int i = 0; i < 128; i++)
{
cc[i] = 64;
}
//InitPitch
pitch = 8192;
// Initialise the micro:bit runtime.
pc.printf("Initialising MicroBit\r\n");
uBit.ble = new BLEDevice();
uBit.init();
uBit.ble->init();
pc.printf("Init done\r\n");
uBit.accelerometer.setPeriod(5);
pc.printf("IMU Period: %d, Range: %d\n\r", uBit.accelerometer.getPeriod(), uBit.accelerometer.getRange());
//Add conection and disconnection callback
uBit.ble->gap().onConnection(onConnectionCallback);
uBit.ble->gap().onDisconnection(disconnectionCallback);
/* setup advertising */
uBit.ble->gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
//Reverse UUID for Adv
uint8_t TMP_ADV_SERVICE[16];
for(int i = 0; i < 16; i++)
{
TMP_ADV_SERVICE[i] = MIDI_SERVICE[15-i];
}
uBit.ble->gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS, (uint8_t*)TMP_ADV_SERVICE, sizeof(TMP_ADV_SERVICE));
uBit.ble->gap().accumulateAdvertisingPayload(GapAdvertisingData::GENERIC_COMPUTER);
uBit.ble->accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
uBit.ble->gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
uBit.ble->setAdvertisingInterval(160); /* 100ms; in multiples of 0.625ms. */
uBit.ble->startAdvertising();
pc.printf("Start Advertising\r\n");
uBit.display.printChar('A');
uBit.ble->gattServer().addService(midiService);
// Insert your code here!
while(1) {
if(isConnect)
{
//BtnA
if(uBit.buttonA.isPressed() == 1 && !btnBuff[0])
{
btnBuff[0] = true;
notifyNote(60, 100);
}
else if(uBit.buttonA.isPressed() == 0 && btnBuff[0])
{
btnBuff[0] = false;
notifyNote(60, 0);
}
x = mapAndLimit(uBit.accelerometer.getX(), -500, 500, 0, 127);
y = mapAndLimit(uBit.accelerometer.getY(), 500, -500, 0, 16383);
notifyCC(7, (uint8_t)x, 1);
notifyPitch((uint16_t)y, 1);
uBit.sleep(5);
}
}
// If main exits, there may still be other fibers running or registered event handlers etc.
// Simply release this fiber, which will mean we enter the scheduler. Worse case, we then
// sit in the idle task forever, in a power efficient sleep.
//release_fiber();
}
//Map and Limit
int mapAndLimit(int value, int fromLow, int fromHigh, int toLow, int toHigh)
{
int tmp, minValue, maxValue;
if(toLow < toHigh)
{
minValue = toLow;
maxValue = toHigh;
}
else
{
minValue = toHigh;
maxValue = toLow;
}
tmp = value - fromLow;
tmp = (int)((double)tmp * (double)(toHigh - toLow) / (double)(fromHigh - fromLow));
tmp += toLow;
tmp = tmp < minValue ? minValue : tmp;
tmp = tmp > maxValue ? maxValue : tmp;
return tmp;
}
//NotifyNote
void notifyNote(uint8_t note, uint8_t velocity)
{
//Send Note
midiPayload[2] = 0x90;
midiPayload[3] = note;
midiPayload[4] = velocity;
//Notify MIDI Data
uBit.ble->gattServer().write(midiChar.getValueAttribute().getHandle(), midiPayload, sizeof(midiPayload));
}
//NotifyCC
void notifyCC(uint8_t ccNum, uint8_t value, uint8_t sensitivity)
{
if(abs(cc[ccNum] - value) > sensitivity)
{
cc[ccNum] = value;
//Send Note
midiPayload[2] = 0xb0;
midiPayload[3] = ccNum;
midiPayload[4] = value;
//Notify MIDI Data
uBit.ble->gattServer().write(midiChar.getValueAttribute().getHandle(), midiPayload, sizeof(midiPayload));
}
}
//NotifyPitch
void notifyPitch(uint16_t value, uint8_t sensitivity)
{
if(abs(pitch - value) > sensitivity)
{
pitch = value;
//Send Note
midiPayload[2] = 0xe0;
midiPayload[3] = (uint8_t)(value & 127);
midiPayload[4] = (uint8_t)(value >> 7);
//Notify MIDI Data
uBit.ble->gattServer().write(midiChar.getValueAttribute().getHandle(), midiPayload, sizeof(midiPayload));
}
}
解説
今回は、省略します。
M5StickCとの比較(あくまでBLE MIDIコントローラとして)
良い点
- 5msまでウェイトを攻めても安定動作。まだ攻めれそう。
- IMUの精度もMicrobitの方が良さそう。(このプログラム作る前にボリュームでCC送るプログラムを作ってみたのですが、ADCの精度は、間違いなくMicrobitの方が上でした。)
- メモリリソースも余裕がある。
今回のデモや別プログラムで遊んだ感じでは、Micro:bitのBLE MIDIであれば、「楽器」として使えるかなあ、という印象で、当初の目的は達成できました。
悪い点
- ソースが分かりにくい。
良いもの作ろうとしたら、プログラムのソースが、多少、難しくなる(低いレイヤのプログラムは、人間には辛いものです)のは、仕方ない部分ではあります。
最後に
話は少し脱線しますが、STEMの教材として、大体同じ価格であるMicorbitとM5StickCを比較すると、
- 開発環境はほぼ差異なし(Scratch系でも、MicroPythonでも開発可)
- M5StickCのWLAN、ディスプレイ、バッテリーの標準搭載は、Micro:bitに勝る魅力。
- BLE MIDIコントローラを両方で作った感覚としては、モノの完成度はMicro:bitの方が上。ただ、STEMで使う分には、M5StickCの完成度で必要十分とも思える。
と、M5StickCは、後発な分、上かなあ、と感じました。
まあ、大学でZ80を使った簡易コンピューターの作成 & 機械語プログラムを行ったことのある人間からすると、どちらもとんでもなく完成度が高く、安く、小さいもので、良い時代になったものです。