MIDI BLE Tutorial
DIN to BLE
The strategy for converting serial MIDI packets into BLE Characteristic packets is to
- Ignore All SysEx Messages
- Attach a Header to Each Incoming Message (and Timestamp)
- Write It to the Characteristic
Detect Incoming MIDI messages
The function parseMIDIonDIN()
previously just checked MIDI.read()
and blinked an LED if data was present. Now, that function is filled with a packet building and sending routine.
Calculating a Timestamp
To calculate the timestamp, the built-in millis()
is used. The BLE standard only specifies 13 bits worth of millisecond data though, so it's bitwise anded with 0x1FFF
for an ever repeating cycle of 13 bits.
This is done right after a MIDI message is detected. It's split into a 6 upper bits, 7 lower bits, and the MSB of both bytes are set to indicate that this is a header byte. Both bytes are placed into the first two position of an array in preparation for a MIDI message.
Interpreting MIDI Messages and Building Variable Length Characteristics
The MIDI Shield Hookup Guide contains a switch statement that's used to build a MIDI analyzer. This code is leveraged to build the interpreter. It calls MIDI.getType()
and compares for midi scoped types, such as midi::NoteOff
and midi::PitchBend
to determine what type (and more importantly, length) of packet to build.
The array that's being build then gets the statysByte programmed in, then data if there is any. Afterwards, characteristic.setValue(...)
is called. It's passed the array (a pointer) and a size, which is known by the results of MIDI.getType()
The Program Listing
// create peripheral instance, see pinouts above //const char * localName = "nRF52832 MIDI"; BLEPeripheral blePeripheral; BLEService service("03B80E5A-EDE8-4B33-A751-6CE34EC4C700"); BLECharacteristic characteristic("7772E5DB-3868-4112-A1A9-F2669D106BF3", BLERead | BLEWriteWithoutResponse | BLENotify, 20 ); BLEDescriptor descriptor = BLEDescriptor("2902", 0); MIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI); void setup() { delay(1000); //Setup diag leds pinMode(BLUE_STAT_PIN, OUTPUT); pinMode(RED_STAT_PIN, OUTPUT); pinMode(GREEN_STAT_PIN, OUTPUT); digitalWrite(BLUE_STAT_PIN, 1); digitalWrite(RED_STAT_PIN, 1); digitalWrite(GREEN_STAT_PIN, 1); //Setup nRF52832 user button pinMode(BTN_PIN, INPUT_PULLUP); setupBLE(); // Initiate MIDI communications, listen to all channels MIDI.begin(MIDI_CHANNEL_OMNI); MIDI.turnThruOff(); // The nRF52832 converts baud settings to the discrete standard rates. // Use the nrf52.h names to write a custom value, 0x7FFC80 after beginning midi NRF_UARTE_Type * myUart; myUart = (NRF_UARTE_Type *)NRF_UART0_BASE; myUart->BAUDRATE = 0x7FFC80; //Write data to the serial output pin to make sure the serial output is working. //Sometimes serial output only allows 1 byte out then hangs. Resetting the //nRF52832 resolves the issue digitalWrite(RED_STAT_PIN, 0); MIDI.sendNoteOn(42, 66, 1); delay(500); MIDI.sendNoteOff(42, 66, 1); digitalWrite(RED_STAT_PIN, 1); } void loop() { BLECentral central = blePeripheral.central(); if(digitalRead(BTN_PIN) == 0){ digitalWrite(GREEN_STAT_PIN, 0); MIDI.sendNoteOff(0x45, 80, 1); delay(100); digitalWrite(GREEN_STAT_PIN, 1); } if (central) { //Prep the timestamp msOffset = millis(); digitalWrite(BLUE_STAT_PIN, 0); // central connected to peripheral while (central.connected()) { digitalWrite(GREEN_STAT_PIN, 0); //If connected, send midi data by the button here if(digitalRead(BTN_PIN) == 0){ digitalWrite(GREEN_STAT_PIN, 0); MIDI.sendNoteOn(0x45, 80, 1); delay(100); MIDI.sendNoteOff(0x45, 80, 1); digitalWrite(GREEN_STAT_PIN, 1); } //Check if data exists coming in from BLE if (characteristic.written()) { digitalWrite(RED_STAT_PIN, 0); //hang to give the LED time to show (not necessary if routines are here) delay(10); digitalWrite(RED_STAT_PIN, 1); } //Check if data exists coming in from the serial port parseMIDIonDIN(); } } //No longer connected. Turn off the LEDs. digitalWrite(BLUE_STAT_PIN, 1); digitalWrite(GREEN_STAT_PIN, 1); //Delay to show off state for a bit delay(100); } //This function is called to check if MIDI data has come in through the serial port. If found, it builds a characteristic buffer and sends it over BLE. void parseMIDIonDIN() { uint8_t msgBuf[5]; //Outgoing buffer //Calculate timestamp. uint16_t currentTimeStamp = millis() & 0x01FFF; msgBuf[0] = ((currentTimeStamp >> 7) & 0x3F) | 0x80; //6 bits plus MSB msgBuf[1] = (currentTimeStamp & 0x7F) | 0x80; //7 bits plus MSB //Check MIDI object for new data. if ( MIDI.read()) { digitalWrite(RED_STAT_PIN, 0); uint8_t statusByte = ((uint8_t)MIDI.getType() | ((MIDI.getChannel() - 1) & 0x0f)); switch (MIDI.getType()) { //2 Byte Channel Messages case midi::NoteOff : case midi::NoteOn : case midi::AfterTouchPoly : case midi::ControlChange : case midi::PitchBend : msgBuf[2] = statusByte; msgBuf[3] = MIDI.getData1(); msgBuf[4] = MIDI.getData2(); characteristic.setValue(msgBuf, 5); break; //1 Byte Channel Messages case midi::ProgramChange : case midi::AfterTouchChannel : msgBuf[2] = statusByte; msgBuf[3] = MIDI.getData1(); characteristic.setValue(msgBuf, 4); break; //System Common Messages case midi::TimeCodeQuarterFrame : msgBuf[2] = 0xF1; msgBuf[3] = MIDI.getData1(); characteristic.setValue(msgBuf, 4); break; case midi::SongPosition : msgBuf[2] = 0xF2; msgBuf[3] = MIDI.getData1(); msgBuf[4] = MIDI.getData2(); characteristic.setValue(msgBuf, 5); break; case midi::SongSelect : msgBuf[2] = 0xF3; msgBuf[3] = MIDI.getData1(); characteristic.setValue(msgBuf, 4); break; case midi::TuneRequest : msgBuf[2] = 0xF6; characteristic.setValue(msgBuf, 3); break; //Real-time Messages case midi::Clock : msgBuf[2] = 0xF8; characteristic.setValue(msgBuf, 3); break; case midi::Start : msgBuf[2] = 0xFA; characteristic.setValue(msgBuf, 3); break; case midi::Continue : msgBuf[2] = 0xFB; characteristic.setValue(msgBuf, 3); break; case midi::Stop : msgBuf[2] = 0xFC; characteristic.setValue(msgBuf, 3); break; case midi::ActiveSensing : msgBuf[2] = 0xFE; characteristic.setValue(msgBuf, 3); break; case midi::SystemReset : msgBuf[2] = 0xFF; characteristic.setValue(msgBuf, 3); break; //SysEx case midi::SystemExclusive : // { // // Sysex is special. // // could contain very long data... // // the data bytes form the length of the message, // // with data contained in array member // uint16_t length; // const uint8_t * data_p; // // Serial.print("SysEx, chan: "); // Serial.print(MIDI.getChannel()); // length = MIDI.getSysExArrayLength(); // // Serial.print(" Data: 0x"); // data_p = MIDI.getSysExArray(); // for (uint16_t idx = 0; idx < length; idx++) // { // Serial.print(data_p[idx], HEX); // Serial.print(" 0x"); // } // Serial.println(); // } break; case midi::InvalidType : default: break; } digitalWrite(RED_STAT_PIN, 1); } } void setupBLE() { blePeripheral.setLocalName("DIN to BLE"); //local name sometimes used by central blePeripheral.setDeviceName("DIN to BLE"); //device name sometimes used by central //blePeripheral.setApperance(0x0000); //default is 0x0000, what should this be? blePeripheral.setAdvertisedServiceUuid(service.uuid()); //Advertise MIDI UUID // add attributes (services, characteristics, descriptors) to peripheral blePeripheral.addAttribute(service); blePeripheral.addAttribute(characteristic); blePeripheral.addAttribute(descriptor); // set initial value characteristic.setValue(0); blePeripheral.begin(); }