When new technologies are introduced, old technology is often warped into compliance of the new standards. MIDI is no exception and is surprisingly easy to work with over a Bluetooth Low Energy link.
This tutorial uses an nRF52832 Breakout board as the BLE radio and the MIDI parsing processor. For the sake of learning, the breakout is connected to a standard MIDI shield. Serial MIDI to BLE is already available as a consumer product, but it's a good platform to talk about the various pieces of the problem.
This covers:
Parts or all of the code presented here can be used as a starting place to build MIDI BLE devices.
The following is required to progress through this tutorial.
An understanding of the original MIDI specification. It is available in full at www.midi.org (Use midi a lot? Join and become a member for free!). The SparkFun MIDI Tutorial breaks down the specification in depth. If only a brief refresher is needed, midi.org's Summary of MIDI Messages is the specific page used to implement the MIDI protocol.
A computer or mobile device that can interface with MIDI BLE devices.
Code examples in this tutorial are maintained in the GitHub repo MIDI-BLE-Adapter. Clone it.
An installation of Arduino and board package for the nRF52832. Follow the nRF52832 Breakout Board Hookup Guide to install the board files, and BLEPeriphrial library.
MIDI BLE has only been around for a couple years, so support is spotty. Take some time to Google your device plus "BLE MIDI" and see what comes up. Here's some general information. Have success? Please post device and OS info in the comments.
Go into your "System Report" through Utilities->System Information. MIDI BLE is available with "LMP Version" 0x06 or higher. More information available on checking your Mac in this imore.com article.
Generally, iOS 8 should have support for BLE MIDI but this is not tested.
Windows 10 machines that are fully updated should have MIDI BLE support.
The program MIDIBerry is pretty basic but can be used to connect BLE devices to other Windows programs.
Android 6.0 marshmallow or later should have software support. Try the app MIDI BLE Connect and see if you can scan for devices.
While just the nRF52832 Breakout and an FTDI are required to run the code, it's recommended to attach a MIDI shield. The next section details how to hook the two together.
Have a pile of midi devices available. When working with so many unknowns, it's nice to have something that is familiar and can be relied on to help narrow down where a problem is occurring.
Here are some options:
Prepare the nRF52832 Breakout board by adding a header to the FTDI serial pins. Prepare the MIDI shield by soldering in the two MIDI jacks.
Make the following connections with loose wires. The RX and TX through holes are occupied by the FTDI pin header, tack them on to the backside of the pins.
nRF52832 Breakout Board Pin |
MIDI Shield Pin |
---|---|
GND | GND |
3.3V |
5V |
26(RX) |
RX |
27(TX) |
TX |
11 | 7 (Red LED) |
12 |
6 (Green LED) |
Take a moment to make sure the development chain is all up and working.
Make sure that a basic blink sketch works. Try out driving the 3 status LEDs, active low. Test that serial messages are getting to your computer. After programming, the nRF82832 likes to freeze up its serial bus and requires a manual reset by the button. Familiarize yourself with getting the board into bootloader mode.
Note: The examples assumes you are using the latest version of the Arduino IDE on your desktop. If this is your first time using Arduino, please review our tutorial on installing the Arduino IDE. If you have not previously installed an Arduino library, please check out our installation guide.
Make sure that the following includes can be added without breaking the build process.
language:c
#include <MIDI.h>
#include <BLEPeripheral.h>
Also, make sure that the MIDI library in use not baked into Arduino or from a dubious source. It should be the latest and greatest from FortySevenEffects' GitHub, and should be manually installed:
With verbose debugging on, the build log reveals that MIDI.cpp is being sourced from the local sketchbook library folder.
"C:\ArduinoConfigurations\arduino-1.8.2_nRF52\localSB\libraries\arduino_midi_library\src\MIDI.cpp" -o
The switch on the midi shield directs the flow of data to the processor. When switched to "RUN" both the MIDI IN port and the FTDI's TX pin are connected to the nRF52832. Due to the way it's wired, the MIDI IN will overpower the FTDI and programs won't load. Alternately, in the "PROG" position, the MIDI IN port is disconnected and programming can occur.
Working with protocols can require a bit of patience. Hang in there!
The goal here is to make a program that meets the specification of MIDI BLE and appears when scanning for Bluetooth devices. The BLEPeripheral library does a good job of abstracting away the specifics of the communication but a bit of knowledge about how BLE works is useful to give meaning to the words used when setting up a BLE peripheral.
BLE is designed to allow a variety of devices, and therefore needs an expandable architecture. The term GATT is used, which stands for Generic Attribute Profile.
The profile describes the collection of objects which are required for a particular BLE system.
A profile can have multiple services, each which is a collection of one or more characteristic.
The characteristic contains the data that will be acted on. Each can only have a single value and a single property. For the nRF52832, that value can be up to 20 bytes.
Each service and each characteristic has a unique name which tells connected systems what kind of device it's dealing with. It's called a UUID (universally unique identifier), and can be 16 bits or 128 bits.
For more information, see bluetooth.com's documentation.
MIDI BLE is a very simple realization of the topology. There is one service, and one characteristic that are required.
As part of midi.org's published standard, a device shall have:
Next, the BLEPeripheral library is used to describe the topology -- its API is documented on GitHub. By using the API to tell the library how to form itself, the nRF52832 will become a MIDI BLE device, and data will be accessible through the characteristic's value.
To use the BLEPeripheral library, declare memory spaces for the various layers as persistent objects, then add them to the BLE Peripheral object. Then, the Peripheral object can be started and used.
The main object is declared. This is what will hold the other objects and operate the radio.
language:c
BLEPeripheral blePeripheral;
Individual layer objects (attributes) are declared for the service and characteristic. The characteristic is also constructed with a max value size of 20, which is the largest size that nRF52832 will allow.
language:c
BLEService service("03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
BLECharacteristic characteristic("7772E5DB-3868-4112-A1A9-F2669D106BF3", BLERead | BLEWriteWithoutResponse | BLENotify, 20 );
Also, a descriptor is created. It is optional and tells the central to disable notification, meaning the BLE peripheral can cast data at the central without acknowledgement. Depending on how the central is programmed, this may or may not have an effect.
language:c
BLEDescriptor descriptor = BLEDescriptor("2902", 0);
Creating the objects alone will not have any effect. There's a few more things to do which are placed in a routine called setupBLE()
for convenience. It is called once during setup()
. The peripheral is named, told to advertise the UUID of the service, and given an initial value. Then, the peripheral is started with .begin()
which connects it to the system. At this point, the BLE device should be discoverable.
language:c
void setupBLE()
{
blePeripheral.setLocalName("BLE MIDI Starter"); //local name sometimes used by central
blePeripheral.setDeviceName("BLE MIDI Starter"); //device name sometimes used by central
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();
}
In the main program loop, a object is created locally and used to determine if the peripheral is connected to a central. The structure of this code is taken from the BLEPeripheral examples.
language:c
void loop()
{
BLECentral central = blePeripheral.central();
if (central) {
while (central.connected()) {
if (characteristic.written()) {
}
}
}
}
Rolling it all together, a simple program can be written that establishes a connection to a BLE central. It turns on the green LED if the device is connected. If data is written to the device, it blinks the red LED and prints th value written to the serial terminal as hex.
language:c
#include <BLEPeripheral.h>
#define BLUE_STAT_PIN 7 // LED on pin 7
#define RED_STAT_PIN 11 // LED on pin 11
#define GREEN_STAT_PIN 12 // LED on pin 12
#define BTN_PIN 6 // User button
// 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);
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println("Program Started");
//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();
}
void loop()
{
BLECentral central = blePeripheral.central();
if (central) {
while (central.connected()) {
digitalWrite(GREEN_STAT_PIN, 0);
//Check if data exists coming in from BLE
if (characteristic.written()) {
digitalWrite(RED_STAT_PIN, 0);
//Receive the written packet and parse it out here.
Serial.print("Rx size: ");
Serial.println(characteristic.valueLength());
uint8_t * buffer = (uint8_t*)characteristic.value();
Serial.print("0x");
for( int i = 0; i < characteristic.valueLength(); i++ ){
if( buffer[i] < 0x10 ) Serial.print("0");
Serial.print( buffer[i], HEX );
}
Serial.println();
digitalWrite(RED_STAT_PIN, 1);
}
}
}
digitalWrite(BLUE_STAT_PIN, 1);
digitalWrite(GREEN_STAT_PIN, 1);
delay(500);
}
void setupBLE()
{
blePeripheral.setLocalName("BLE MIDI Starter"); //local name sometimes used by central
blePeripheral.setDeviceName("BLE MIDI Starter"); //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);
// set event handlers - Alternate ways of checking for BLE activity
//characteristic.setEventHandler(BLEWritten, BLEWrittenCallback);
//characteristic.setEventHandler(BLESubscribed, BLESubscribedCallback);
//characteristic.setEventHandler(BLEUnsubscribed, BLEUnsubscribedCallback);
blePeripheral.begin();
}
To test the program, it is loaded to the nRF52832, which is discovered by a computer that can send MIDI BLE data. The computer sends MIDI data for setting an instrument's configuration, and the following appears in the serial console. The data will be analyzed in the section "Encapsulating MIDI DATA".
Output:
Program Started
Rx size: 13
0xB9FDB06248FDB00600FDB0260A
Paraphrased from the MIDI Tutorial,
Bytes of MIDI messages are divided into 2 major categories, based on the setting of the most significant bit. If a byte's MSB is a 0, it is a data byte with 7 usable bits of data. If the MSB is a 1, it's a status byte (or could be a special case if SysEx messages are used, but they are ignored here).
A serial MIDI packet is started with a status byte, then contains a number of data bytes depending on what type of packet is indicated by the status byte (see Summary of MIDI Messages for more information).
In the world of BLE, data is thought of in terms of characteristics, which is just a size of data that can be written to, and is available on both ends of the connection by the unseen negotiations of the link.
The Bluetooth LE MIDI Specification serves as the source material for the next section. Go register with midi.org to download it for free, and it helps them see who's using the information. They're a great organization and are allowing direct reprinting of their copyrighted material for this tutorial.
The specification allows a few types of MIDI packets to exist within a characteristic.
The most basic type of MIDI BLE packet, or characteristic value, is one containing a single MIDI message.
The first and second bytes are overhead to the actual midi payload.
Shown here, a MIDI message of 3 bytes is appended to a timestamp to create a BLE packet. If the midi message was only 2 bytes long, the BLE packet size would be 4 bytes, and so on.
Also notice that the MSB is set for any byte that isn't data. This will be useful when parsing out MIDI messages of other forms.
A BLE characteristic can be written to a variable size, and can contain more than 1 MIDI message. When two or more MIDI messages are concatenated, the upper 6 bits of timestamp can be omitted because it can't rollover twice per BLE packet. All contained messages share the same upper timestamp bits and have their own lower bits.
Here two MIDI messages are contained in a single characteristic. The second message has the same header byte and so it's omitted to reduce overhead.
Look at the output from the basic BLE Peripheral example.
0xB9FDB06248FDB00600FDB0260A
This has been hand decoded in the following table.
Offset | Hex |
Binary | BLE Name |
MIDI Decode |
---|---|---|---|---|
0x00 |
0xB9 | 10111001b | Header | |
0x01 | 0xFD | 11111101b | Timestamp | |
0x02 | 0xB0 | 10110000b | Status | Control Change Channel 0 |
0x03 | 0x62 | 01100010b | Data | Controller 98 |
0x04 | 0x48 | 01001000b | Data | 74 |
0x05 | 0xFD | 11111101b | Timestamp | |
0x06 | 0xB0 | 10110000b | Status | Control Change Channel 0 |
0x07 | 0x06 | 00000110b | Data | Controller 6 |
0x08 | 0x00 | 00000000b | Data | 0 |
0x09 | 0xFD | 11111101b | Timestamp | |
0x0A | 0xB0 | 10110000b | Status | Control Change Channel 0 |
0x0B | 0x26 | 00100110b | Data | Controller 38 |
0x0C | 0x0A | 00001010b | Data | 10 |
This is an NRPN message. It's three controller change commands sent sequentially. Just as the MIDI specification indicates, the packet is formed with single Header and unique timestamps for each message (although they all seemed to originate simultaneously).
The last type of BLE packet that can exist is a running status message. In a message like this many MIDI messages all have the same timestamp and MIDI status, so the timestamp and status are only sent once followed by a block of data. The MIDI status indicates what size the data will be (by message type) and the data can be parsed accordingly.
From the previous example, the MacBook Air sent 3 messages that did have the same timestamp and status. It's unusual that it was instructed to send these as full messages rather than running status messages.
The gold standard for Arduino MIDI Libraries is the Arduino MIDI library written by GitHub user FortySevenEffects.
From the MIDI Tutorial,
- It can use hardware or software serial ports (leaving the hardware serial port for printing debug messages!).
- Incoming messages can be received by polling, or callbacks for specific messages can be installed.
- The library can filter for messages on a specific midi channel, or receive on all channels.
- It implements an optional "soft thru" port that can be configured to echo the input back to the output port.
It also has detailed documentation in doxygen format.
For some practical examples that demonstrate this library, take a look at the hookup guide for the SparkFun MIDI Shield.
The library will be configured in this way for the rest of this tutorial:
Adapting the library to operate on the nRF52832 is fairly straightforward. The HardwareSerial
object can be passed to the MIDI_CREATE_INSTANCE
function as normal and the library operates over the serial port. However, the baud rate is not correctly set. The nRF52832 board package only allows the standard discrete baud rates, so the port must be finagled after calling MIDI.begin()
Use the following code snippet to bend the port into 31250 baud.
language:c
#include "nrf52.h"
...
MIDI.begin(MIDI_CHANNEL_OMNI);
...
// 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;
The sketch "midi-lib-starter.ino" is an expanded version of the previous example, "ble-starter.ino", with the MIDI library included and configured. In the example, a note-on note-off pair of messages is sent out the serial port when the program starts in order to prove the system is working. During runtime, the nRF52832's user button can be pressed to send data out the port.
A function called parseMIDIonDIN()
is called periodically when the BLE connection is valid. Inside, MIDI.read()
is checked to see if new data is available. If so, the red LED is flashed but nothing else occurs. This is where MIDI data will be decoded and dealt with.
language:c
#include <MIDI.h>
#include "nrf52.h"
#include <BLEPeripheral.h>
#define LED_PIN 7 // LED on pin 7
#define RED_STAT_PIN 11 // LED on pin 7
#define GREEN_STAT_PIN 12 // LED on pin 7
#define BTN_PIN 6
uint8_t msgBuf[5];
unsigned long msOffset = 0;
#define MAX_MS 0x01FFF //13 bits, 8192 dec
// 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(LED_PIN, OUTPUT);
pinMode(RED_STAT_PIN, OUTPUT);
pinMode(GREEN_STAT_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
digitalWrite(LED_PIN, 1);
digitalWrite(RED_STAT_PIN, 1);
digitalWrite(GREEN_STAT_PIN, 1);
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();
//Send midi data by the press of the button to test while running.
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);
}
if (central) {
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);
processPacket();
digitalWrite(RED_STAT_PIN, 1);
}
//Check if data exists coming in from the serial port
parseMIDIonDIN();
}
}
digitalWrite(LED_PIN, 1);
digitalWrite(GREEN_STAT_PIN, 1);
delay(500);
}
void processPacket()
{
//Receive the written packet and parse it out here.
uint8_t * buffer = (uint8_t*)characteristic.value();
uint8_t bufferSize = characteristic.valueLength();
//hang to give the LED time to show (not necessary if routines are here)
delay(10);
}
void parseMIDIonDIN()
{
if ( MIDI.read())
{
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);
}
}
void setupBLE()
{
blePeripheral.setLocalName("MIDI BLE Starter"); //local name sometimes used by central
blePeripheral.setDeviceName("MIDI BLE Starter"); //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);
// set event handlers - Alternate ways of checking for BLE activity
//characteristic.setEventHandler(BLEWritten, BLEWrittenCallback);
//characteristic.setEventHandler(BLESubscribed, BLESubscribedCallback);
//characteristic.setEventHandler(BLEUnsubscribed, BLEUnsubscribedCallback);
blePeripheral.begin();
}
This code is useful if as a framework for using both the BLE MIDI configuration as well as the FortySevenEffects MIDI configuration. The next section will fill parseMIDIonDIN()
with code which translates incoming MIDI messages into BLE characteristic messages.
The strategy for converting serial MIDI packets into BLE Characteristic packets is to
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.
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.
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()
language:c
#include <MIDI.h>
#include "nrf52.h"
#include <BLEPeripheral.h>
#define BLUE_STAT_PIN 7 // LED on pin 7
#define RED_STAT_PIN 11 // LED on pin 11
#define GREEN_STAT_PIN 12 // LED on pin 12
#define BTN_PIN 6 // User button
// 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();
}
In the previous example of converting serial messages to BLE messages, the shortcut of "only send single, full MIDI message packets" was used. Going the other way, it's more difficult because the central can't be configured to only send one type of packet, they will be packetized as defined by the MIDI BLE standard. The program must separate the data which comes in many forms.
To do so, a routine is created (called processPacket
) that has the following logical flow.
To implement, the program uses array indeces, left and right. The left one starts at array position 2 because it is always a status byte. Next, the right is moved to seek out the next status byte by looking at the MSB positions. Afterwards, the size of the data following the left status byte is known, and can be dealt with. The left index is then moved to the next status byte and the process is repeated until the indeces exceed the bounds.
Inside, it calls another routine (transmitMIDIonDIN
) that interfaces the FortySevenEffects library. This is to isolate the functions from each other and provide a method of testing out the routines without having a full system running.
This code is the MIDI BLE Starter program with expanded processPacket function that translates incoming BLE packets into serial MIDI.
language:c
#include <MIDI.h>
#include "nrf52.h"
#include <BLEPeripheral.h>
#define BLUE_STAT_PIN 7 // LED on pin 7
#define RED_STAT_PIN 11 // LED on pin 11
#define GREEN_STAT_PIN 12 // LED on pin 12
#define BTN_PIN 6 // User button
unsigned long msOffset = 0;
#define MAX_MS 0x01FFF //13 bits, 8192 dec
// 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);
processPacket();
digitalWrite(RED_STAT_PIN, 1);
}
}
}
//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 decodes the BLE characteristics and calls transmitMIDIonDIN
//if the packet contains sendable MIDI data.
void processPacket()
{
//Receive the written packet and parse it out here.
uint8_t * buffer = (uint8_t*)characteristic.value();
uint8_t bufferSize = characteristic.valueLength();
//Pointers used to search through payload.
uint8_t lPtr = 0;
uint8_t rPtr = 0;
//lastStatus used to capture runningStatus
uint8_t lastStatus;
//Decode first packet -- SHALL be "Full MIDI message"
lPtr = 2; //Start at first MIDI status -- SHALL be "MIDI status"
//While statement contains incrementing pointers and breaks when buffer size exceeded.
while(1){
lastStatus = buffer[lPtr];
if( (buffer[lPtr] < 0x80) ){
//Status message not present, bail
return;
}
//Point to next non-data byte
rPtr = lPtr;
while( (buffer[rPtr + 1] < 0x80)&&(rPtr < (bufferSize - 1)) ){
rPtr++;
}
//look at l and r pointers and decode by size.
if( rPtr - lPtr < 1 ){
//Time code or system
transmitMIDIonDIN( lastStatus, 0, 0 );
} else if( rPtr - lPtr < 2 ) {
transmitMIDIonDIN( lastStatus, buffer[lPtr + 1], 0 );
} else if( rPtr - lPtr < 3 ) {
transmitMIDIonDIN( lastStatus, buffer[lPtr + 1], buffer[lPtr + 2] );
} else {
//Too much data
//If not System Common or System Real-Time, send it as running status
switch( buffer[lPtr] & 0xF0 )
{
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xE0:
for(int i = lPtr; i < rPtr; i = i + 2){
transmitMIDIonDIN( lastStatus, buffer[i + 1], buffer[i + 2] );
}
break;
case 0xC0:
case 0xD0:
for(int i = lPtr; i < rPtr; i = i + 1){
transmitMIDIonDIN( lastStatus, buffer[i + 1], 0 );
}
break;
default:
break;
}
}
//Point to next status
lPtr = rPtr + 2;
if(lPtr >= bufferSize){
//end of packet
return;
}
}
}
//This function takes a midi packet as input and calls the appropriate library
//function to transmit the data. It's a little redundant because the library
//reforms midi data from the calls and sends it out the serial port.
//
//Ideally, the MIDI BLE object would feed a MIDI library object as a serial
//object removing all of this code.
//
//A benefit of this redundant code is that it's easy to filter messages, and
//exposes how the library works.
void transmitMIDIonDIN( uint8_t status, uint8_t data1, uint8_t data2 )
{
uint8_t channel = status & 0x0F;
channel++;
uint8_t command = (status & 0xF0) >> 4;
switch(command)
{
case 0x08: //Note off
MIDI.sendNoteOff(data1, data2, channel);
break;
case 0x09: //Note on
MIDI.sendNoteOn(data1, data2, channel);
break;
case 0x0A: //Polyphonic Pressure
MIDI.sendAfterTouch(data1, data2, channel);
break;
case 0x0B: //Control Change
MIDI.sendControlChange(data1, data2, channel);
break;
case 0x0C: //Program Change
MIDI.sendProgramChange(data1, channel);
break;
case 0x0D: //Channel Pressure
MIDI.sendAfterTouch(data2, channel);
break;
case 0x0E: //Pitch Bend
MIDI.send(midi::PitchBend, data1, data2, channel);
break;
case 0x0F: //System
switch(status)
{
case 0xF1: //MTC Q frame
MIDI.sendTimeCodeQuarterFrame( data1 );
break;
case 0xF2: //Song position
MIDI.sendSongPosition(( (uint16_t)(data1 & 0x7F) << 7) | (data2 & 0x7F));
break;
case 0xF3: //Song select
MIDI.sendSongSelect( data1 );
break;
case 0xF6: //Tune request
MIDI.sendTuneRequest();
break;
case 0xF8: //Timing Clock
case 0xFA: //Start
case 0xFB: //Continue
case 0xFC: //Stop
case 0xFE: //Active Sensing
case 0xFF: //Reset
MIDI.sendRealTime( (midi::MidiType)status );
break;
default:
break;
}
break;
default:
break;
}
}
void setupBLE()
{
blePeripheral.setLocalName("BLE to DIN"); //local name sometimes used by central
blePeripheral.setDeviceName("BLE to DIN"); //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();
}
Combining the MIDI BLE Starter program, DIN to BLE program, and BLE to DIN program, a full program is written that converts MIDI messages both ways. Without the shield, this can be put on an nRF52832 that is connected to another processor's UART to add MIDI BLE to an existing MIDI device, such as the Tsunami.
language:c
#include <MIDI.h>
#include "nrf52.h"
#include <BLEPeripheral.h>
#define BLUE_STAT_PIN 7 // LED on pin 7
#define RED_STAT_PIN 11 // LED on pin 11
#define GREEN_STAT_PIN 12 // LED on pin 12
#define BTN_PIN 6 // User button
unsigned long msOffset = 0;
#define MAX_MS 0x01FFF //13 bits, 8192 dec
// 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);
processPacket();
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 decodes the BLE characteristics and calls transmitMIDIonDIN
//if the packet contains sendable MIDI data.
void processPacket()
{
//Receive the written packet and parse it out here.
uint8_t * buffer = (uint8_t*)characteristic.value();
uint8_t bufferSize = characteristic.valueLength();
//Pointers used to search through payload.
uint8_t lPtr = 0;
uint8_t rPtr = 0;
//lastStatus used to capture runningStatus
uint8_t lastStatus;
//Decode first packet -- SHALL be "Full MIDI message"
lPtr = 2; //Start at first MIDI status -- SHALL be "MIDI status"
//While statement contains incrementing pointers and breaks when buffer size exceeded.
while(1){
lastStatus = buffer[lPtr];
if( (buffer[lPtr] < 0x80) ){
//Status message not present, bail
return;
}
//Point to next non-data byte
rPtr = lPtr;
while( (buffer[rPtr + 1] < 0x80)&&(rPtr < (bufferSize - 1)) ){
rPtr++;
}
//look at l and r pointers and decode by size.
if( rPtr - lPtr < 1 ){
//Time code or system
transmitMIDIonDIN( lastStatus, 0, 0 );
} else if( rPtr - lPtr < 2 ) {
transmitMIDIonDIN( lastStatus, buffer[lPtr + 1], 0 );
} else if( rPtr - lPtr < 3 ) {
transmitMIDIonDIN( lastStatus, buffer[lPtr + 1], buffer[lPtr + 2] );
} else {
//Too much data
//If not System Common or System Real-Time, send it as running status
switch( buffer[lPtr] & 0xF0 )
{
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xE0:
for(int i = lPtr; i < rPtr; i = i + 2){
transmitMIDIonDIN( lastStatus, buffer[i + 1], buffer[i + 2] );
}
break;
case 0xC0:
case 0xD0:
for(int i = lPtr; i < rPtr; i = i + 1){
transmitMIDIonDIN( lastStatus, buffer[i + 1], 0 );
}
break;
default:
break;
}
}
//Point to next status
lPtr = rPtr + 2;
if(lPtr >= bufferSize){
//end of packet
return;
}
}
}
//This function takes a midi packet as input and calls the appropriate library
//function to transmit the data. It's a little redundant because the library
//reforms midi data from the calls and sends it out the serial port.
//
//Ideally, the MIDI BLE object would feed a MIDI library object as a serial
//object removing all of this code.
//
//A benefit of this redundant code is that it's easy to filter messages, and
//exposes how the library works.
void transmitMIDIonDIN( uint8_t status, uint8_t data1, uint8_t data2 )
{
uint8_t channel = status & 0x0F;
channel++;
uint8_t command = (status & 0xF0) >> 4;
switch(command)
{
case 0x08: //Note off
MIDI.sendNoteOff(data1, data2, channel);
break;
case 0x09: //Note on
MIDI.sendNoteOn(data1, data2, channel);
break;
case 0x0A: //Polyphonic Pressure
MIDI.sendAfterTouch(data1, data2, channel);
break;
case 0x0B: //Control Change
MIDI.sendControlChange(data1, data2, channel);
break;
case 0x0C: //Program Change
MIDI.sendProgramChange(data1, channel);
break;
case 0x0D: //Channel Pressure
MIDI.sendAfterTouch(data2, channel);
break;
case 0x0E: //Pitch Bend
MIDI.send(midi::PitchBend, data1, data2, channel);
break;
case 0x0F: //System
switch(status)
{
case 0xF1: //MTC Q frame
MIDI.sendTimeCodeQuarterFrame( data1 );
break;
case 0xF2: //Song position
MIDI.sendSongPosition(( (uint16_t)(data1 & 0x7F) << 7) | (data2 & 0x7F));
break;
case 0xF3: //Song select
MIDI.sendSongSelect( data1 );
break;
case 0xF6: //Tune request
MIDI.sendTuneRequest();
break;
case 0xF8: //Timing Clock
case 0xFA: //Start
case 0xFB: //Continue
case 0xFC: //Stop
case 0xFE: //Active Sensing
case 0xFF: //Reset
MIDI.sendRealTime( (midi::MidiType)status );
break;
default:
break;
}
break;
default:
break;
}
}
//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.
unsigned long currentMillis = millis();
if(currentMillis < 5000){
if(msOffset > 5000){
//it's been 49 days! millis rolled.
while(msOffset > 5000){
//roll msOffset - this should preserve current ~8 second count.
msOffset += MAX_MS;
}
}
}
//if the offset is more than 2^13 ms away, move it up in 2^13 ms intervals
while(currentMillis >= (unsigned long)(msOffset + MAX_MS)){
msOffset += MAX_MS;
}
unsigned long currentTimeStamp = currentMillis - msOffset;
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("nRF52832 MIDI"); //local name sometimes used by central
blePeripheral.setDeviceName("nRF52832 MIDI"); //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();
}
Once you've figured out the basics, the nRF52832 can be deployed directly to a device to build a MIDI instrument or controller.
Checkout some of the details of the circuit described in the post:
If you're interested in building your own MIDI system, we have some products to help you get started.
For more information about MIDI and BLE protocols, check out the information listed below.
learn.sparkfun.com | CC BY-SA 3.0 | SparkFun Electronics | Niwot, Colorado