My application is a typical midi use-case. Using a D1 Wemos board with a self-designed midi interface, I use Pin 21 for midi-transmit and pin 16 for midi-receive. On top, I have another Midi connection using BLE to connect to mobile devices. All in all, my application has two midi connections, (1) physical midi connection and (2) BLE midi connection. For (1) I am using this midi library
https://github.com/FortySevenEffects/ar ... di_library
While using my larger application during rehearsals, I detected strange behavior every now and then. Thus, I reduced the application further and further to find the source of the issue. Here is my current sketch:
- #include <OpenBleMidi.h>
- #include <string>
- #include <MIDI.h>
- #define TX_MIDI 21
- #define RX_MIDI 16
- #define HX_STOMP_MIDI_CHANNEL 4
- #define PROGRAM_CHANGE_NOTE_C_m1 0
- #define ON 0x7f
- #define OFF 0x00
- MIDI_CREATE_INSTANCE(HardwareSerial, Serial2, hxMidiInterface);
- OpenBleMidi openBleMidi;
- // #define WITH_BLE
- #ifdef WITH_BLE
- class HelixMorph : public IOpenBleMidi {
- #else
- class HelixMorph {
- #endif
- private:
- uint8_t m_midiPacket[5] = {0x80, 0x80, 0xB0, 0x53, 0x01};
- public:
- // ------------------------------------------------------------
- // IOpenBleMidi
- // ------------------------------------------------------------
- virtual void onBleMidiControlChange(uint8_t cc, uint8_t val, uint8_t ch) {
- Serial.print("BLE Control Change: "); Serial.print(cc); Serial.print(", value: "); Serial.print(val); Serial.print(", channel: "); Serial.println(ch);
- }
- virtual void onBleMidiNoteOn(uint8_t cc, uint8_t val, uint8_t ch) {
- Serial.print("BLE Note On: "); Serial.print(cc); Serial.print(", channel: "); Serial.println(ch);
- }
- virtual void onBleMidiNoteOff(uint8_t cc, uint8_t val, uint8_t ch) {
- Serial.print("BLE Note Off: "); Serial.print(cc); Serial.print(", channel: "); Serial.println(ch);
- }
- virtual void onBleMidiProgramChange(uint8_t pc, uint8_t ch) {
- Serial.print("BLE Program Change: "); Serial.print(pc); Serial.print(", channel: "); Serial.println(ch);
- }
- virtual void onBleMidiSystemExlusive(uint16_t byteCnt, uint8_t *bytes) {
- Serial.print("System Exclusive, data: ");
- for(int i=0; i<byteCnt; i++) {
- Serial.print(bytes[i]);
- Serial.print(" ");
- }
- Serial.println();
- }
- virtual void onBleMidiConnect() {
- Serial.println("BLE Connected to client");
- }
- virtual void onBleMidiDisconnect() {
- Serial.println("BLE Disconneted from client");
- }
- // ------------------------------------------------------------
- // Midi
- // ------------------------------------------------------------
- static void onNoteOn(byte channel, byte note, byte velocity) {
- Serial.print("OpenHxStomp onNoteOn"); Serial.print(", note: "); Serial.print(note); Serial.print(", velocity: "); Serial.print(velocity); Serial.print(", ch: "); Serial.println(channel);
- if(channel != HX_STOMP_MIDI_CHANNEL) {
- Serial.println("****** WRONG MIDI CHANNEL ***************************************");
- return;
- }
- // for testing only
- if(velocity != 78 && velocity != 79) {
- Serial.println("*** WRONG VELOCITY **********************************************");
- }
- switch(note) {
- case(PROGRAM_CHANGE_NOTE_C_m1):
- break;
- default:
- Serial.println("*** DEFAULT IN CASE *******************************************");
- break;
- }
- }
- static void onNoteOff(byte channel, byte note, byte velocity) {
- Serial.print("OpenHxStomp onNoteOff"); Serial.print(", note: "); Serial.print(note); Serial.print(", velocity: "); Serial.print(velocity); Serial.print(", ch: "); Serial.println(channel);
- Serial.println("*** NOTE OFF **************************************************");
- }
- };
- HelixMorph helixMorph;
- void setup() {
- Serial.begin(115200);
- #ifdef WITH_BLE
- openBleMidi.setup("HelixMorph", &Serial);
- openBleMidi.registerIOpenBleMidiCb(&helixMorph);
- #endif
- hxMidiInterface.setHandleNoteOn(HelixMorph::onNoteOn);
- hxMidiInterface.setHandleNoteOff(HelixMorph::onNoteOff);
- Serial2.begin(31250, SERIAL_8N1, RX_MIDI, TX_MIDI);
- }
- void loop() {
- hxMidiInterface.read();
- }
Besides the code above I wrote a small python script that sends midi Note On messages to the physical connection in a loop:
- def test_02(self):
- # make sure we start at 27b
- self.set_preset(79)
- time.sleep(1)
- num_tests = 10000
- for i in range(0, num_tests):
- print('Running test ' + str(i) + ' of ' + str(num_tests))
- self.midi_if.send_midi_message(mido.Message('note_on', channel=3, note=0, velocity=78))
- time.sleep(0.1)
- self.midi_if.send_midi_message(mido.Message('note_on', channel=3, note=0, velocity=79))
- time.sleep(0.1)
If you take a look onto my ESP32 code you'll see the following line:
- #define WITH_BLE
I use this define as a switch - if the define is there, I compile my solution with midi BLE functionality. If I comment the line off, the solution will build without BLE.
If I run my application without BLE functionality everything works as expected. I believe I sent more than 100000 midi messages to my microcontroller without any issue - everything was received as I expected.
If I activate the BLE some messages get corrupted (rough guess 1 of 2000). As some of you might know, a Note On message consists of three bytes. The first byte also carries the midi channel information. Byte 2 is the note and byte 3 the velocity. I have different corruptions – sometimes the Note On message is detected as Note Off (so I guess the first byte gets corrupted), sometimes the note are velocity values are simply wrong.
I had a deeper look into the midi library I use and found this function which parses the incoming midi data:
- // Private method: MIDI parser
- template<class SerialPort, class Settings>
- bool MidiInterface<SerialPort, Settings>::parse()
- {
- if (mSerial.available() == 0)
- // No data available.
- return false;
- // Parsing algorithm:
- // Get a byte from the serial buffer.
- // If there is no pending message to be recomposed, start a new one.
- // - Find type and channel (if pertinent)
- // - Look for other bytes in buffer, call parser recursively,
- // until the message is assembled or the buffer is empty.
- // Else, add the extracted byte to the pending message, and check validity.
- // When the message is done, store it.
- const byte extracted = mSerial.read();
- Serial.print("MIDI::parse() after mSerial.read(): "); Serial.println(extracted);
- // Ignore Undefined
- if (extracted == 0xf9 || extracted == 0xfd)
- (...)
I added the Serial.print commands and figured out, that the data is already corrupted at this stage.
All in all, it look like the BLE functionality has influences on the serial data buffer used to receive the physical midi data. Is this possible? How could I further check this issue? Any idea what I could try?
For completeness, here is the code for midi BLE functionality I moved into a library called OpenBleMidi:
- #include "OpenBleMidi.h"
- #include <MIDI.h>
- uint8_t OpenBleMidi::deviceConnected = 0;
- class OpenBleMidiServerCallbacks: public BLEServerCallbacks {
- private:
- OpenBleMidi *m_pOpenBleMidi;
- public:
- OpenBleMidiServerCallbacks(OpenBleMidi *pOpenBleMidi) {
- m_pOpenBleMidi = pOpenBleMidi;
- }
- void onConnect(BLEServer* pServer) {
- m_pOpenBleMidi->onBleMidiConnect();
- OpenBleMidi::deviceConnected = 1;
- };
- void onDisconnect(BLEServer* pServer) {
- m_pOpenBleMidi->onBleMidiDisconnect();
- OpenBleMidi::deviceConnected = 0;
- }
- };
- class OpenBleMidiCharacteristicCallbacks: public BLECharacteristicCallbacks {
- private:
- OpenBleMidi *m_pOpenBleMidi;
- public:
- OpenBleMidiCharacteristicCallbacks(OpenBleMidi *pOpenBleMidi) {
- m_pOpenBleMidi = pOpenBleMidi;
- }
- void onWrite(BLECharacteristic *pCharacteristic) {
- std::string rxValue = pCharacteristic->getValue();
- uint8_t midiMessageLength = rxValue.length();
- uint8_t processedBytes = 0;
- uint8_t type, note, velocity, channel, d1, d2;
- // timestamp #1 and timestamp #2
- processedBytes += 2;
- while(processedBytes < midiMessageLength) {
- channel = rxValue[processedBytes] % 16;
- type = rxValue[processedBytes++] - channel;
- switch(type) {
- case midi::ControlChange:
- note = rxValue[processedBytes++];
- velocity = rxValue[processedBytes++];
- m_pOpenBleMidi->onBleMidiControlChange(note, velocity, channel+1);
- break;
- case midi::NoteOn:
- note = rxValue[processedBytes++];
- velocity = rxValue[processedBytes++];
- channel = rxValue[processedBytes++];
- m_pOpenBleMidi->onBleMidiNoteOn(note, velocity, channel+1);
- break;
- case midi::NoteOff:
- note = rxValue[processedBytes++];
- velocity = rxValue[processedBytes++];
- channel = rxValue[processedBytes++];
- m_pOpenBleMidi->onBleMidiNoteOff(note, velocity, channel+1);
- break;
- case midi::ProgramChange:
- note = rxValue[processedBytes++];
- m_pOpenBleMidi->onBleMidiProgramChange(note, channel+1);
- break;
- case midi::SystemExclusive: {
- uint8_t *sysexData = new uint8_t[midiMessageLength];
- uint8_t byteCnt = 0;
- for(int i=0 ; i<midiMessageLength ; i++) {
- if(0x80 == rxValue[i])
- continue;
- sysexData[byteCnt++] = rxValue[i];
- }
- m_pOpenBleMidi->onBleMidiSystemExlusive(byteCnt, sysexData);
- break;
- }
- default:
- break;
- }
- // skip one byte as we expect the next timestamp
- processedBytes++;
- }
- }
- };
- OpenBleMidi::OpenBleMidi() {
- }
- void OpenBleMidi::registerIOpenBleMidiCb(IOpenBleMidi *p_pCb) {
- m_pIOpenBleMidiCb = p_pCb;
- }
- void OpenBleMidi::onBleMidiControlChange(uint8_t cc, uint8_t val, uint8_t ch) {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiControlChange(cc, val, ch);
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiNoteOn(uint8_t note, uint8_t val, uint8_t ch) {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiNoteOn(note, val, ch);
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiNoteOff(uint8_t note, uint8_t val, uint8_t ch) {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiNoteOff(note, val, ch);
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiProgramChange(uint8_t pc, uint8_t ch) {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiProgramChange(pc, ch);
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiSystemExlusive(uint16_t byteCnt, uint8_t* bytes) {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiSystemExlusive(byteCnt, bytes);
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiConnect() {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiConnect();
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::onBleMidiDisconnect() {
- if(m_pIOpenBleMidiCb)
- m_pIOpenBleMidiCb->onBleMidiDisconnect();
- else if(m_pSerialMonitor)
- m_pSerialMonitor->println("No IOpenBleMidi callback registered so far");
- }
- void OpenBleMidi::setup(std::string sz_productName, HardwareSerial *pSerialMonitor) {
- m_szProductName = sz_productName;
- m_pSerialMonitor = pSerialMonitor;
- if(m_pSerialMonitor)
- m_pSerialMonitor->print("Initialising BLE device... ");
- BLEDevice::init(m_szProductName);
- // Create the BLE Server
- BLEServer *pServer = BLEDevice::createServer();
- pServer->setCallbacks(new OpenBleMidiServerCallbacks(this));
- BLEDevice::setEncryptionLevel((esp_ble_sec_act_t)ESP_LE_AUTH_REQ_SC_BOND);
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("Done!");
- if(m_pSerialMonitor)
- m_pSerialMonitor->print("Creating MIDI BLE Service... ");
- // Create the BLE Service
- BLEService *pService = pServer->createService(BLEUUID(MIDI_SERVICE_UUID));
- // Create a BLE Characteristic
- m_pCharacteristic = pService->createCharacteristic(
- BLEUUID(MIDI_CHARACTERISTIC_UUID),
- BLECharacteristic::PROPERTY_READ |
- BLECharacteristic::PROPERTY_NOTIFY |
- BLECharacteristic::PROPERTY_WRITE_NR
- );
- m_pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("Done!");
- // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
- // Create a BLE Descriptor
- m_pCharacteristic->addDescriptor(new BLE2902());
- OpenBleMidiCharacteristicCallbacks *pBleMidiCallbacks = new OpenBleMidiCharacteristicCallbacks(this);
- m_pCharacteristic->setCallbacks(pBleMidiCallbacks);
- if(m_pSerialMonitor)
- m_pSerialMonitor->print("Starting BLE service... ");
- pService->start();
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("Done!");
- if(m_pSerialMonitor)
- m_pSerialMonitor->print("Starting advertisement... ");
- BLESecurity *pSecurity = new BLESecurity();
- pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
- pSecurity->setCapability(ESP_IO_CAP_NONE);
- pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
- pServer->getAdvertising()->addServiceUUID(MIDI_SERVICE_UUID);
- pServer->getAdvertising()->start();
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("Done!");
- if(m_pSerialMonitor) {
- m_pSerialMonitor->print("BLE service is running - device discoverable as: ");
- m_pSerialMonitor->println(m_szProductName.c_str());
- }
- }
- void OpenBleMidi::sendControlChange(uint8_t control, uint8_t value, uint8_t channel) {
- if(0 == OpenBleMidi::deviceConnected) {
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("No connection to any BLE device - can't send CC message");
- return;
- }
- uint8_t midiPacket[] = {0x80, 0x80, 0xB0 | channel, control, value};
- m_pCharacteristic->setValue(midiPacket, 5); // packet, length in bytes
- m_pCharacteristic->notify();
- }
- void OpenBleMidi::sendProgramChange(uint8_t program, uint8_t channel) {
- if(0 == OpenBleMidi::deviceConnected) {
- if(m_pSerialMonitor)
- m_pSerialMonitor->println("No connection to any BLE device - can't send PC message");
- return;
- }
- uint8_t midiPacket[] = {0x80, 0x80, 0xC0 | channel, program};
- m_pCharacteristic->setValue(midiPacket, 4); // packet, length in bytes
- m_pCharacteristic->notify();
- }