はじめに
本記事は、下記のハードウェアを用いて、BLE-MIDIに準拠したMIDIキーボードとBLE接続によりLEDを光らせるソフトウェアの解説です。ハードウェアや開発環境の基本設定方法等については、本記事では詳細には説明致しませんので、必ず下記の記事をお読みください。
なお、このハードウェアは、この記事の執筆者である上野和昭氏が代表を務める合同会社Next StepのECサイトから購入可能です。
なお、今回使用したBLE-MIDIキーボードは、KORG MicroKEY AIR-25となります。筆者は、これ以外のBLE-MIDIキーボードを所有しておりませんので、これ以外のBLE-MIDIキーボードで動作するかは確認しておりませんので、ご了承願います。
事前準備
- KORG MicroKEY AIRのファームウェアを最新版Ver1.05(2023.2.10)にアップデートする。
Ver1.04では動作しなかったので、必ずアップデートしてください。 - ArduinoのライブラリAdafruit NeoPixelをインストールしてください。(前記、上野氏の記事中にもインストールするように書かれていますので、そちらの記事を読んでインストール済の方は改めてインストールする必要はありません。上野氏の記事には画像付きで詳しくインストール方法が書かれていますので、ライブラリのインストール方法の詳細はそちらをご覧ください。)
- 同じく、ArduinoのライブラリBLE-MIDI by lathoubをインストールしてください。
スケッチ例
#include <Arduino.h>
#include <BLEMIDI_Transport.h>
#include <hardware/BLEMIDI_Client_ESP32.h>
#include <Adafruit_NeoPixel.h>
#define PIN 5 //GPIO5
#define SW 4 //GPIO4
#define LED_BUILTIN 3 //GPIO3
#define DIPSW_0 6 //GPIO6
#define DIPSW_1 7 //GPIO7
#define DIPSW_2 8 //GPIO8
#define DIPSW_3 10 //GPIO10
#define NUM_LED 60 //number of LEDs
#define KEY_OFFSET -12 //KEY OFFSET
#define WIPE 72 + KEY_OFFSET
#define TH_CHASE 74 + KEY_OFFSET
#define RAINBOW 76 + KEY_OFFSET
#define TH_CH_RAIN 77 + KEY_OFFSET
#define TH_CHASE1 71 + KEY_OFFSET
#define TH_C3 60 + KEY_OFFSET
#define TH_D3 62 + KEY_OFFSET
#define TH_E3 64 + KEY_OFFSET
#define TH_F3 65 + KEY_OFFSET
#define TH_G3 67 + KEY_OFFSET
#define TH_A3 69 + KEY_OFFSET
#define TH_A31 70 + KEY_OFFSET
#define TH_B3 71 + KEY_OFFSET
#define NOTE_OFF 0
#define NOTE_NULL 255
unsigned char Red[3] = {255, 0, 0};
unsigned char Orange[3] = {255, 127, 0};
unsigned char Kon[3] = {32, 64, 150};
unsigned char Blue[3] = {0, 0, 255};
unsigned char White[3] = {240, 240, 240};
unsigned char Black[3] = {0, 0, 0};
unsigned char Cyan[3] = {0, 255, 255};
unsigned char Green[3] = {0, 255, 0};
unsigned char Pink[3] = {250, 0, 250};
unsigned char *Col_ptr;
void ReadCB(void *parameter); //Continuos Read function (See FreeRTOS multitasks)
unsigned long t0 = millis();
bool isConnected = false;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LED, PIN, NEO_GRB + NEO_KHZ800);
unsigned char midi_event_flag = NOTE_OFF;
unsigned char pre_event = NOTE_OFF;
bool restart_pat = true;
byte g_velo;
BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI) //Connect to the first server found
//BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI) //Connect to a specific name server
// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void setup()
{
Serial.begin(115200);
Serial.setTxTimeoutMs(0);
pinMode(SW, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
BLEMIDI.setHandleConnected([]()
{
Serial.println("---------CONNECTED !---------");
isConnected = true;
digitalWrite(LED_BUILTIN, HIGH);
});
BLEMIDI.setHandleDisconnected([]()
{
Serial.println("---------NOT CONNECTED !---------");
isConnected = false;
digitalWrite(LED_BUILTIN, LOW);
});
MIDI.setHandleNoteOn(OnAppleMidiNoteOn);
MIDI.setHandleNoteOff(OnAppleMidiNoteOff);
xTaskCreatePinnedToCore(ReadCB, //See FreeRTOS for more multitask info
"MIDI-READ",
3000,
NULL,
1,
NULL,
0); //Core0 or Core1
// LED init
Col_ptr = Black;
strip.begin();
strip.show(); // Initialize all pixels to 'off'
MIDI.begin();
}
// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void loop()
{
unsigned long interval;
//MIDI.read(); // This function is called in the other task
//midi_event_flag = TH_CH_RAIN;//TEST
//Col_ptr = Cyan;
if (midi_event_flag == RAINBOW)
{
interval = 20;
}
else if ((midi_event_flag == TH_CHASE1) || (midi_event_flag == TH_CH_RAIN) || (midi_event_flag == WIPE))
{
if (g_velo < 100)
{
interval = 50;
}
else
{
interval = 30;
if (midi_event_flag == WIPE)
{
interval = 20;
}
}
}
else
{
interval = 50;
}
if ((millis() - t0) > interval) //50ms
{
t0 = millis();
//Serial.print(".");
if (pre_event != midi_event_flag) //バカ避け NOTE OFFが来なくて、別のパターンの場合
{
restart_pat = true;
}
switch (midi_event_flag)
{
case WIPE:
Wipe();
break;
case TH_CHASE:
TheaterChase();
break;
case TH_CHASE1:
TheaterChase();
break;
case RAINBOW:
Rainbow();
break;
case TH_CH_RAIN:
theaterChaseRainbow();
break;
case NOTE_OFF:
All_off();
break;
default:
break;
}
pre_event = midi_event_flag;
}
}
/**
* This function is called by xTaskCreatePinnedToCore() to perform a multitask execution.
* In this task, read() is called every millisecond (approx.).
* read() function performs connection, reconnection and scan-BLE functions.
* Call read() method repeatedly to perform a successfull connection with the server
* in case connection is lost.
*/
void ReadCB(void *parameter)
{
//Serial.print("READ Task is started on core: ");
//Serial.println(xPortGetCoreID());
for (;;)
{
MIDI.read();
vTaskDelay(1 / portTICK_PERIOD_MS); //Feed the watchdog of FreeRTOS.
//Serial.println(uxTaskGetStackHighWaterMark(NULL)); //Only for debug. You can see the watermark of the free resources assigned by the xTaskCreatePinnedToCore() function.
}
vTaskDelay(1);
}
// ====================================================================================
// Event handlers for incoming MIDI messages
// ====================================================================================
// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void OnAppleMidiNoteOn(byte channel, byte note, byte velocity) {
digitalWrite(LED_BUILTIN, LOW);
g_velo = velocity;
switch (note)
{
case TH_C3:
midi_event_flag = TH_CHASE1;
Col_ptr = Red;
break;
case TH_D3:
midi_event_flag = TH_CHASE1;
Col_ptr = Orange;
break;
case TH_E3:
midi_event_flag = TH_CHASE1;
Col_ptr = Kon;
break;
case TH_F3:
midi_event_flag = TH_CHASE1;
Col_ptr = Blue;
break;
case TH_G3:
midi_event_flag = TH_CHASE1;
Col_ptr = White;
break;
case TH_A3:
midi_event_flag = TH_CHASE1;
Col_ptr = Cyan;
break;
case TH_A31:
midi_event_flag = TH_CHASE1;
Col_ptr = Pink;
break;
case TH_B3:
midi_event_flag = TH_CHASE1;
Col_ptr = Green;
break;
case WIPE: //C4
midi_event_flag = WIPE;
break;
case TH_CHASE: //D4
midi_event_flag = TH_CHASE;
if (velocity < 13) //10%未満
{
Col_ptr = Black;
}
else if (velocity < 26)
{
Col_ptr = Red;
}
else if (velocity < 39)
{
Col_ptr = Orange;
}
else if (velocity < 51)
{
Col_ptr = Kon;
}
else if (velocity < 64)
{
Col_ptr = Blue;
}
else if (velocity < 77) //60%未満
{
Col_ptr = White;
}
else //if (velocity <89)
{
Col_ptr = Cyan;
}
break;
case RAINBOW: //E4
midi_event_flag = RAINBOW;
break;
case TH_CH_RAIN: //F4
midi_event_flag = TH_CH_RAIN;
break;
default:
break;
}
}
// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void OnAppleMidiNoteOff(byte channel, byte note, byte velocity) {
digitalWrite(LED_BUILTIN, HIGH);
midi_event_flag = NOTE_OFF;
restart_pat = true;
}
// ====================================================================================
// Subroutine for LED
// Fill the dots one after the other with a color
// ====================================================================================
void All_off()
{
int i;
uint32_t c;
c = strip.Color(0, 0, 0);
for (i = 0; i < strip.numPixels(); i++)
{
strip.setPixelColor(i, c);
}
strip.show();
midi_event_flag = NOTE_NULL;
}
void Wipe(void)
{
static int i = 0;
static int j = 0;
static int loop_end;
static uint32_t c;
if (restart_pat == true)
{
i = 0;
loop_end = strip.numPixels();
switch (j)
{
case 0:
c = strip.Color(255, 0, 0);
break;
case 1:
c = strip.Color( 0, 255, 0);
break;
case 2:
c = strip.Color( 0, 0, 255);
break;
default:
break;
}
j++;
if (j >= 3) j = 0;
restart_pat = false;
}
strip.setPixelColor(i, c);
strip.show();
i++;
if (i >= loop_end)
{
restart_pat = true;
}
}
void Rainbow(void)
{
static int j = 0;
if (restart_pat == true)
{
j = 0;
restart_pat = false;
}
for (int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel((i + j) & 255));
}
strip.show();
j++;
if (j >= 256) j = 0;
}
void TheaterChase(void)
{
static int j = 0;
static int k = 0;
static int q = 0;
static bool even = true;
static uint32_t c;
if (restart_pat == true)
{
j = 0;
q = 0;
even = true;
switch (k)
{
case 0:
c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
break;
case 1:
c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
//c = strip.Color(Col_ptr[0], 0, 0);
break;
case 2:
c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
//c = strip.Color(0, 0, Col_ptr[2]);
break;
default:
break;
}
k++;
if (k >= 3) k = 0;
restart_pat = false;
}
if (even == true)
{
for (int i = 0; i < strip.numPixels(); i = i + 3) {
strip.setPixelColor(i + q, c); //turn every third pixel on
}
strip.show();
even = false;
}
else
{
for (int i = 0; i < strip.numPixels(); i = i + 3) {
strip.setPixelColor(i + q, 0); //turn every third pixel off
}
even = true;
q++;
if (q >= 3)
{
q = 0;
j++;
if (j >= 10)
{
j = 0;
}
}
}
}
void theaterChaseRainbow(void)
{
static int j = 0;
static int q = 0;
static bool even = true;
if (restart_pat == true)
{
j = 0;
q = 0;
even = true;
restart_pat = false;
}
if (even == true)
{
for (int i = 0; i < strip.numPixels(); i = i + 3) {
strip.setPixelColor(i + q, Wheel( (i + j) % 255)); //turn every third pixel on
}
strip.show();
even = false;
}
else
{
for (int i = 0; i < strip.numPixels(); i = i + 3) {
strip.setPixelColor(i + q, 0); //turn every third pixel off
}
even = true;
q++;
if (q >= 3)
{
q = 0;
j++;
if (j >= 256)
{
j = 0;
}
}
}
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if (WheelPos < 85) {
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if (WheelPos < 170) {
WheelPos -= 85;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
uint8_t device_id(void)
{
//INPUT_PULLUP Mode except for GPIO16
pinMode(DIPSW_0, INPUT);
pinMode(DIPSW_1, INPUT);
pinMode(DIPSW_2, INPUT);
pinMode(DIPSW_3, INPUT);
delay(10);
return ((digitalRead(DIPSW_3) << 3) | (digitalRead(DIPSW_2) << 2) | (digitalRead(DIPSW_1) << 1) | digitalRead(DIPSW_0)+1);
}
スケッチ例の説明
1.オクターブ調整
スケッチ例では、MIDIノートNo.60(C3)~No.77(F4)をONすることにより、LEDがいろいろなパターンで発光します。
MIDIキーボードの鍵盤数やメーカーによって左端の鍵盤がCxに配置されているかはまちまちですので、配置を調整したい場合は、
#define KEY_OFFSET -12 //KEY OFFSET
をオクターブ単位(12の倍数) で+-方向に変更してください。
デフォルトの-12はMicroKEY Air 25 に合うように調整しています。
2.接続するMIDIデバイスの指定
BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI) //Connect to the first server found
//BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI) //Connect to a specific name server
スケッチ例では、スキャンして最初に見つかったBLE-MIDI機器に接続します。大概これで問題ないですが、多数のMIDI機器が混在する環境で明示した機器に接続したい場合は、
//BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI) //Connect to the first server found
BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI) //Connect to a specific name server
に変更します。これにより、指定したBLE addressの機器に接続します。BLE機器はのアドレスは、デフォルトのソースのままスケッチを実行して、機器との接続完了時にシリアルモニタに表示されますので、それを上記ソースの
"d0:17:69:bc:bf:dd"
部分を表示されたアドレスに置き換えてください。(シリアルモニタのBLE address表示例は、下記のUSB CDC設定説明のキャプチャー画像の中の一番下の行です。)
注意点
この基板は、ESP32C3の内蔵USBをシリアルモニタ用のUARTとして使いますので、必ずUSB CDC On BootをEnabledにして下さい。
終わりに
Arduinoのライブラリが非常に充実しており、極めて少ないコードでESP32とBLE-MIDIキーボードを接続することができました。このようなライブラリを提供してくださったlathoub氏に感謝申し上げます。
MIDIライブラリーの使い方は、lathoub氏のGithub等をお読みください。
コールバック関数によるデータ受信処理への分岐やFreeRTOSを使いマルチスレッドによってMIDI受信データをポーリングして受信する等このライブラリを使うにはリアルタイム処理系によく使われる手法に対しての慣れが必要かと思います。
機会がありましたら、そのあたりも解説できればよいかと思います。