BLE-MIDI to qwertyアダプタを作ってみた話

## はじめに

・やりたいこと
PS4にもMIDIキーボードを接続したい！でもMIDIを直接サポートしていないのでMIDI⇒qwertyに変換して普通のキーボードのようにふるまうアダプタを作ればいい！
という思惑から制作をスタートしました。
（何番煎じかわかりませんが，PS4でFF14を遊んでいるのでMIDIキーボードで演奏したいというやつですね）

文字入力用キーボードとMIDIキーボードと「キーボード」という語が2つ出てくるのでこの後は，文字入力用を「キーボード」，MIDIのほうを「鍵盤」として扱いますね。

初めはPS4←[USBキーボード]←[ESP32]←[USB MIDI鍵盤]という構成を考えていましたが，もうすでにほかの方が作成されていますしネイティブに2ポートのUSBコントローラを備えたチップはDIY界隈ではそんなにないようなので，思い切ってBLE-MIDI鍵盤を接続できないかと考えました。

https://jp.finalfantasyxiv.com/lodestone/character/17427786/blog/3940977/

https://github.com/Lybfip/Arduino-MIDI-to-FF14-adapter/blob/master/README.md

どうせなら，基板は裸のままじゃなくてケースに収めたいと思ってライブラリやチップを探しまして，次のものを利用すれば比較的簡単に行けましたので，その結果がこの記事というわけです。

https://github.com/max22-/ESP32-BLE-MIDI

※同じような内容のライブラリで「BLE-MIDI」というものもあるので注意してください。

https://www.espressif.com/en/news/ESP32-S3-BOX-3

## 事前準備
自分の開発環境と使用したライブラリのバージョン等は以下の通りです。
・Windows10 Home 64bit
・Arduino IDE v2.3.4
・ターゲットボード：ESP32-S3-BOX
・ライブラリ：ESP32-BLE-MIDI v0.3.2
・注意が必要な依存ライブラリ：NimBLE-Arduino v1.4.3
　※最新版は2.2.1ですがビルドエラーが出ます。以下の記事参考までに，同じ問題と思います。

 https://qiita.com/kenichih/items/003fc694aeeac4f15aed

使った鍵盤
CME XkeyAir 37

### チップの選択
今回のアダプタではUSBキーボードとして振舞えるチップを選択する必要があります。
今回はESP32-BLE-MIDIというライブラリを利用しますので，ESP32チップから選択します。
使用したESP32-S3-BOX-3はその名の通り，ESP32-S3チップが使用されています。

自分はM5シリーズが気に入っているので，その中から探しているうちに本家もM5のようなキットを売っているのを知り，買ってみました。
正直，サンプルコードとかあまり親切じゃないので創作意欲が．．．

## スケッチ

```c
#ifndef ARDUINO_USB_MODE
#error This ESP32 SoC has no Native USB interface
#elif ARDUINO_USB_MODE == 1
#warning This sketch should be used when USB is in OTG mode
void setup() {}
void loop() {}
#else

#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;

#include <Arduino.h>
#include <BLEMidi.h>

// keyboard data list. index: 0 to 36
char keys[] = {
  'a', '1',   // C,(48)
  'b', '2',   // D,
  'c',        // E,
  'd', '3',   // F,
  'e', '4',   // G,
  'f', '5',   // A,
  'g',        // B,
  'h', '6',   // C(60)
  'i', '7',   // D
  'j',        // E
  'k', '8',   // F
  'l', '9',   // G
  'm', '0',   // A
  'n',        // B
  'o', 'w',   // c(72)
  'p', 'x',   // d
  'q',        // e
  'r', 'y',   // f
  's', 'z',   // g
  't', '.',   // a
  'u',        // b
  'v'        // c(84)
};
bool onKeyflg[127];

#define BASE_NOTE 48  // 一番低い音のノート番号
const uint8_t keyLength = sizeof(keys); // キー配列の長さ
const uint8_t bias = keyLength - (BASE_NOTE % keyLength); // 一番低いノート番号を配列の0番目にするバイアス

/* MIDI IN MESSAGE REPORTING */
void onNoteOn(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp)
{
  if(velocity != 0){
    Keyboard.press(keys[(note + bias) % keyLength]);
  }else{
    Keyboard.release(keys[(note + bias) % keyLength]);
  }
}

void onNoteOff(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp)
{
  Keyboard.release(keys[(note + bias) % keyLength]);
}

void setup() {
  BLEMidiClient.begin("Midi client");
  BLEMidiClient.setNoteOnCallback(onNoteOn);
  BLEMidiClient.setNoteOffCallback(onNoteOff);

  Keyboard.begin();
  USB.begin();
}

void loop() {
    if(!BLEMidiClient.isConnected()) {
        // If we are not already connected, we try te connect to the first BLE Midi device we find
        int nDevices = BLEMidiClient.scan();
        if(nDevices > 0) {
            if(BLEMidiClient.connect(0))
                //Connection established
            else {
                //Connection failed
                delay(3000);    // We wait 3s before attempting a new connection
            }
        }
    }
    else {
        //running...
        delay(5000);
    }
}
#endif /* ARDUINO_USB_MODE */
```
## 解説
解説というのもおこがましいほど自分では何も作っていませんが，コードの詳細について説明します。

まず冒頭部分
```c
#ifndef ARDUINO_USB_MODE
#error This ESP32 SoC has no Native USB interface
#elif ARDUINO_USB_MODE == 1
#warning This sketch should be used when USB is in OTG mode
void setup() {}
void loop() {}
#else
```
この部分は，使用するチップにネイティブUSB機能があるか判定を行っています。
ArduinoIDEの設定で，使用するチップのUSB MODEをUSB-OTGに変更していない場合コンパイルエラーとなります。
設定方法は，メニューバーの「ツール」から「USB Mode」を選択して変更します。
そもそも，ネイティブUSBに対応していないチップを搭載しているボードを選択している場合には，この項目は表示されませんので，項目がないという場合は選択しているボードが間違っていないか確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253612/8afe70a9-56ed-49c1-becf-8a713e353913.png)

次にキー設定の部分
こちらは引用記事からそのまま引っ張ってきた部分です。
詳しくは先達にお任せするとして，簡単に触れると，USBキーボードから送出する文字の定義マップと鍵盤から送られてきたNote番号からマップのindexに変換するための定数の設定になります。

```c
// keyboard data list. index: 0 to 36
char keys[] = {
  'a', '1',   // C,(48)
  'b', '2',   // D,
  'c',        // E,
  'd', '3',   // F,
  'e', '4',   // G,
  'f', '5',   // A,
  'g',        // B,
  'h', '6',   // C(60)
  'i', '7',   // D
  'j',        // E
  'k', '8',   // F
  'l', '9',   // G
  'm', '0',   // A
  'n',        // B
  'o', 'w',   // c(72)
  'p', 'x',   // d
  'q',        // e
  'r', 'y',   // f
  's', 'z',   // g
  't', '.',   // a
  'u',        // b
  'v'        // c(84)
};
bool onKeyflg[127];

#define BASE_NOTE 48  // 一番低い音のノート番号
const uint8_t keyLength = sizeof(keys); // キー配列の長さ
const uint8_t bias = keyLength - (BASE_NOTE % keyLength); // 一番低いノート番号を配列の0番目にするバイアス
```

次がこのスケッチの核心部分で，鍵盤から入力を受け取った際にCallback関数が自動で呼び出されます。そのCallback関数の内容を次の関数で置き換えます。
やっていることは，NoteOn（鍵盤が押された）のとき，キーボードのキーを押す（押したまま離さない）。
NoteOFF(鍵盤から指が離れた）のとき，キーボードのキーを離す。という動作を行います。
`Keyboard.press()`がキーを押す,`Keyboard.release()`がキーを離すという動作に対応します。
ちなみに`Keyboard.write()`というものもあり，これはキーを押して離すという動作に相当します。今回は使いません。
```c
/* MIDI IN MESSAGE REPORTING */
void onNoteOn(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp)
{
  if(velocity != 0){
    Keyboard.press(keys[(note + bias) % keyLength]);
  }else{
    Keyboard.release(keys[(note + bias) % keyLength]);
  }
}

void onNoteOff(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp)
{
  Keyboard.release(keys[(note + bias) % keyLength]);
}
```

setup関数です。ここで，うえで定義した関数をCallback処理に割り当てます。
サンプルスケッチ「03 Receieving-Data」では，`BLEMidiServer`についてコールバックのコード例があったのですが，やりたいことはデバイスではなくてクライアント(鍵盤をさがして接続する側)なので`BLEMidiClient`を開始します。
```c
void setup() {
  BLEMidiClient.begin("Midi client");
  BLEMidiClient.setNoteOnCallback(onNoteOn);
  BLEMidiClient.setNoteOffCallback(onNoteOff);

  Keyboard.begin();
  USB.begin();
}
```

最後にループ処理の部分です。
```c
void loop() {
    if(!BLEMidiClient.isConnected()) {
        // If we are not already connected, we try te connect to the first BLE Midi device we find
        int nDevices = BLEMidiClient.scan();
        if(nDevices > 0) {
            if(BLEMidiClient.connect(0))
                //Connection established
            else {
                //Connection failed
                delay(3000);    // We wait 3s before attempting a new connection
            }
        }
    }
    else {
        //running...
        delay(5000);
    }
}
```
こちらはサンプルスケッチの「02 Basic-Midi-Client」をそのまま引用しています（シリアル通信は削除しました）。
ソース中のコメントでもありますが，接続する機器は選択できません。デバイスを検索して一番最初に見つかったデバイスと接続を行います。
今のところ，問題なく鍵盤とペアリングできて動作しています。

## さいごに

これで楽しいFF演奏生活が送れますね！
せっかくディスプレイがついている品種なので，デバイスの選択とかできるようにした...~~でも，サンプルコードが難解だから多分やりませんね♪~~