ESP32 Bluetooth Midi (BleMidi)

ESP32 Bluetooth Midi (BleMidi)

前回、ESP32を使ったMIDIインターフェースを作りました。しかし起動時にBleが自動的につながらない・SysExが4000byteリミットなど少々問題がありましたのでその辺を改善しつつ、シンプルなBleMidiを実装しloopバックしたBleMidiの生のコードを見ていこうと思います。


テスト環境はESP32とKORG BLE-MIDI Driver とArduinoIDE1.8.5を使用しています。ベースにしたコードはMIDI over BLE chegewaraさんの2番目のコードを参考にしています。(前回はarduinoぽいので1番目のコードでした。)


またライブラリはESP32_BLE_Arduinoを使用していますがサイズがどんどん大きくなっていますのですこし古めのVersion0.4.9を使用しています。


BlueToothの受信したコード解析

MIDIオーバー・ブルートゥースLE(英語版)を参考にコードを見ていきます。

midicode

まずはノーマルなコードでヘッダー/タイムスタンプが入りmidiコードが入ります。

midicode midicode

自前のBasicWebMidiApiでコードを送ります。DF DEがヘッダーとタイムスタンプでB1 01 01が送ったコードです。仕様どおりESP32のシリアルモニターで確認することができました。


midicode midicode

このコードはMIDIステータス付きコードとして出力されてしまい確認できませんでした。リアルタイムコード以外の直前のMIDIステータスを記憶してパースするとき修正が必要になります。


ここまでをまとめるとMIDIステータスを取得、MIDIデータが127以下を確認、出力しパースするカウントを進める、タイムスタンプであれば次のMIDIステータス取得、MIDIステータスがないときは前回のMIDIステータスを復活させる。これらを1回の取得パケット分繰り返すことで何とかパースすることができそうです。



midicode

さて問題はリアルタイムコードF8などはどこにでも突っ込める仕様でして、これまではパースできそうですがこの仕様は一筋縄ではパースできそうにありません。前回のスケッチではレアケースなので手を付けませんでした。とりあえずコードを見てみましょう。


midicode

コントロールチェンジにB1 01 01 に F8 を突っ込んでみますとタイムスタンプとF8が付加されて出力されてきました。


midicode midicode midicode

2バイトのプログラムチェンジにF8をはさむと「?」プログラムチェンジのコードが欠損して出力されました。これはWebMidiAPIのバグ?でしょうか。ループバックしてもF8しか出力されませんし2バイトx2回にF8をはさむと正しく出力されているようです。

Bomeで出力してみましたところ正常に出力されました。やはりWebMidiApiが怪しいかと思われます。


この仕様があると今までのパースが崩れてきます。MIDIデータにタイムスタンプがあった時は配列を組みなおして再度パースする?。ちょっと手間ですね。



受信したシステムエクスクルーシブ/System Exclusiveを見てみる

midicode midicode

32610byteのSysExをBomeで出力しESP32に出力させてみました。カウントは正しく取得できていますのでパースできると思われます。


仕様は【ヘッダー/タイムスタンプ/F0/*********/タイムスタンプ/F7】です。

パケットを分けるときは【ヘッダー/*********】でつなぎ

最後は【ヘッダー/*********/タイムスタンプ/F7】終わります。


今回のBleMIDI確認で使ったESP32のスケッチ

起動時に自動的にAppleMidiLibraryとBLE-MIDIがつなげる事が出来ました。スケッチ上はAppleMidiLibraryはコメントアウトしております。


/*
 * BaseCode 
 * Create a new BLE server.  Spcial thanks chegewara
 * https://github.com/nkolban/esp32-snippets/issues/510
 */

/***********AppleMidiLib************
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiUdp.h>
#include "AppleMidi.h"
char ssid[] = "your SSID"; //  your network SSID (name)
char pass[] = "Your NetWorkPass";    // your network password (use for WPA, or use as key for WEP)
bool isConnected = false;
APPLEMIDI_CREATE_INSTANCE(WiFiUDP, AppleMIDI); // see definition in AppleMidi_Defs.h
/*************************************/
 
#include "BLEDevice.h"
#include "BLEServer.h"
#include "BLEUtils.h"
#include "BLE2902.h"
//#include <esp_log.h>:
#include <string>
#include <Task.h>

#include "sdkconfig.h"

/********blecirclebuff************/
int cBindex[128][2];/*サークルバッファのインデックス*/
uint8_t cBuff1[512];
int cBCnt=0;
uint8_t cBindexCntStart=0;
/********blecirclebuff************/
boolean bleSysEx=false;
//static char LOG_TAG[] = "MIDIDemo";
BLECharacteristic* pCharacteristic;

uint8_t midiPacket[30];
uint8_t mpCnt=0;
unsigned long t ; // timer used for BLE-MIDI timestamps
boolean blefull = false;



class MyTask : public Task {
    void run(void*) {
       while(1){
           if (blefull){
            blefull = false;
            //midiPacket[0] = 0x80 | ((millis() & 0x00001fff) >> 7); // header & timestampHigh
            //midiPacket[1] =  0x80 | ((millis() & 0x00001fff) & 0x003f); // timestampLow
            pCharacteristic->setValue(midiPacket, mpCnt); // packet, length in bytes
            pCharacteristic->notify();
           }
          vTaskDelay(2/portTICK_PERIOD_MS);
        }
    }
};

MyTask *task;
class MyCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer){
        task->start();
  }
  void onDisconnect(BLEServer* pServer){
        task->stop();
  }
};

class MyCharacteristicCallback : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* pChar){
      std::string rxValue = pCharacteristic->getValue();
      if (rxValue.length() > 0 ) {

        cBindex[cBindexCntStart][1] = cBCnt+rxValue.length();
        cBindex[cBindexCntStart++][0]= cBCnt;
        if(cBindexCntStart>=128)cBindexCntStart=0;
        //cBindex[cBindexCntStart][0]= cBCnt;
        //cBindexCntStart=++cBindexCntStart%128;
        if(rxValue[2]==0xf0)bleSysEx=true;

        for (uint8_t i = 0; i < rxValue.length(); i++) {
          cBuff1[cBCnt++] = rxValue[i];
          if (512 <= cBCnt) cBCnt = cBCnt - 512;
          //cBuff1[cBCnt] = rxValue[i];
          //cBCnt= ++cBCnt%512;
        }
        if (bleSysEx)vTaskDelay(4/portTICK_PERIOD_MS);
      }
    }
};

class MainBLEServer: public Task {
  void run(void *data) {
    //ESP_LOGD(LOG_TAG, "Starting BLE work!");

    task = new MyTask();
    BLEDevice::init("BooleansBLE");
    BLEServer *pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyCallbacks());

        BLEService* pService = pServer->createService("03b80e5a-ede8-4b33-a751-6ce34ec4c700");
        pCharacteristic = pService->createCharacteristic("7772e5db-3868-4112-a1a9-f2669d106bf3", 
                    BLECharacteristic::PROPERTY_READ   |
                    BLECharacteristic::PROPERTY_NOTIFY |
                    BLECharacteristic::PROPERTY_WRITE_NR
        );

        pCharacteristic->setCallbacks(new MyCharacteristicCallback());

        pCharacteristic->addDescriptor(new BLE2902());
        pService->start();


    BLEAdvertising *pAdvertising = pServer->getAdvertising();
    pAdvertising->addServiceUUID(pService->getUUID());
    pAdvertising->start();


    BLESecurity *pSecurity = new BLESecurity();
    pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);

    ESP_LOGD(LOG_TAG, "Advertising started!");
    delay(portMAX_DELAY);
  }
};

#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif

void loopTask(void *pvParameters)
{
    setup();
    for(;;) {
        micros(); //update overflow
        loop();
    }
}

extern "C" void app_main(void)
{
  initArduino();
  xTaskCreatePinnedToCore(loopTask, "loopTask", 8192, NULL, 1, NULL, ARDUINO_RUNNING_CORE);

  //esp_log_level_set("*", ESP_LOG_DEBUG);
  MainBLEServer* pMainBleServer = new MainBLEServer();
  pMainBleServer->setStackSize(8192);
  pMainBleServer->start();
} // app_main


void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < 128; i++) {
    for (uint8_t j = 0; j < 2; j++) {
      cBindex[i][j]=600;
    }
  }

  /********AppleMidiLibrary**********
  delay(2000);//Make Ble Connect time
  Serial.print(F("Getting IP address..."));
  WiFi.begin(ssid, pass);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(F("."));
  }
  Serial.println(F(""));
  Serial.println(F("WiFi connected"));

  Serial.println();
  Serial.print(F("IP address is "));
  Serial.println(WiFi.localIP());

  Serial.println(F("OK, now make sure you an rtpMIDI session that is Enabled"));
  Serial.print(F("Add device named BooleansWIFI with Host/Port "));
  Serial.print(WiFi.localIP());
  Serial.println(F(":5004"));

  // Create a session and wait for a remote host to connect to us
  AppleMIDI.begin("BooleansWIFI");

  AppleMIDI.OnConnected(OnAppleMidiConnected);
  AppleMIDI.OnDisconnected(OnAppleMidiDisconnected);

  delay(1000); //safe
 /********AppleMidiLibrary***********/

  
}



int ExCnt;

void loop() {
  
  uint8_t SearchCnt ;
  
  int Hit = 0;
  int Hitend = 0;
  
    SearchCnt = cBindexCntStart+99;
    SearchCnt=++SearchCnt%128;
    //SearchCnt = cBindexCntStart;
    for (uint8_t i = 0; i < 128; i++) {
      SearchCnt = SearchCnt + i;
      if(SearchCnt>=128)SearchCnt=SearchCnt -128;
      if(cBindex[SearchCnt][0] != 600) {/*[0]!=[1]で取りこぼし発生*/
        Hit = cBindex[SearchCnt][0];
        Hitend = cBindex[SearchCnt][1]-1;
        Hitend = Hitend%512; 
        cBindex[SearchCnt][0]= 600;
        cBindex[SearchCnt][1]= 600;
        break;
      }
    }
  

  if(Hit != Hitend){
    mpCnt=0;
    for(;;){
      midiPacket[mpCnt] = cBuff1[Hit];
      Serial.print(midiPacket[mpCnt] ,HEX);
      mpCnt=++mpCnt%30;
      if(Hit == Hitend)break;
      Hit = ++Hit%512;
      Serial.print(":");
    }
    
    Serial.print("  length:");
    Serial.println(mpCnt);


    
   if(bleSysEx==true){
     if(midiPacket[2]==0xf0){
      ExCnt = mpCnt - 2;
     }else {
      ExCnt = ExCnt + mpCnt -1;
     }
   }

   if(bleSysEx==false){
    midiPacket[0] =  0x80 | ((millis() & 0x00001fff) >> 7); // header & timestampHigh
    midiPacket[1] =  0x80 | ((millis() & 0x00001fff) & 0x003f); // timestampLow
    pCharacteristic->setValue(midiPacket, mpCnt); // packet, length in bytes
    pCharacteristic->notify();
   }

   if(bleSysEx==true&&midiPacket[mpCnt-1]==0xf7){
    ExCnt = ExCnt -1 ;
    Serial.print("GotExCnt::"); 
    Serial.println(ExCnt);
    ExCnt=0;
    bleSysEx=false;
   }


  }
}

/* --------Apple Midi Lib----------------------
// rtpMIDI session. Device connected
// --------------------------------------------
void OnAppleMidiConnected(uint32_t ssrc, char* name) {
  isConnected  = true;
  Serial.print(F("Connected to session "));
  Serial.println(name);
}

// ---------------------------------------------
// rtpMIDI session. Device disconnected
// --------------------------------------------
void OnAppleMidiDisconnected(uint32_t ssrc) {
  isConnected  = false;
  Serial.println(F("Disconnected"));
}
/* --------Apple Midi Lib----------------------*/

終わりに朗報です。Arduino-AppleMIDI-Library作者のlathoubさんがArduino-BLE-MIDI を公開しています。


logo