This guide expects basic familiarity with the MIDI protocol and the different types of MIDI messages. You can find more information about MIDI on https://www.midi.org/, the summary of MIDI messages can be especially handy.
Control Surface supports sending and receiving MIDI messages in many different ways: over a standard 5-pin DIN MIDI cable, over USB, over Bluetooth, over WiFi ...
Each of those methods of transport is available as a “MIDI Interface” class in the code, you can find a full overview in the MIDI Interfaces module.
The interfaces you're most likely to use are:
Other available interfaces are HairlessMIDI_Interface, SoftwareSerialMIDI_Interface, USBHostMIDI_Interface ...
Not all MIDI interfaces are supported on all Arduino boards. For example, not all Arduino boards support MIDI over USB natively. You can find an overview of boards that do support it on the MIDI over USB page. USB Host MIDI is currently only supported on the Teensy 3.6 and 4.1 boards.
MIDI over BLE is supported on the ESP32, the Raspberry Pi Pico W, and on boards supported by the ArduinoBLE library. More information can be found on the MIDI over BLE page.
The MIDI interfaces provide the following functionality:
In the remainder of this tutorial, one section will be devoted to each of these functions.
The following code snippet demonstrates how to instantiate, initialize, and update a MIDI over USB interface. By “instantiate”, we mean that a variable is created that is an instance of the USBMIDI_Interface
type.
Note how the words USBMIDI_Interface
, begin
and update
in the previous snippet are shown in different colors: these are links to the documentation for the relevant classes and functions. If you hover over them with your mouse pointer, you'll see a short description, and if you click on the link, it will take you to the detailed documentation.
The code above doesn't really do anything useful yet, it just sets up everything so we can start sending and receiving MIDI messages in the following sections.
You can of course replace USBMIDI_Interface
by a different type, such as USBDebugMIDI_Interface
if that's the MIDI interface you need for your application.
Some classes allow you to specify extra parameters: for example, USBDebugMIDI_Interface
accepts an optional argument that specifies the baud rate to use when communicating with the Serial Monitor. The possible parameters can be found in the documentation for the constructor of the class in question, for example, see USBDebugMIDI_Interface::USBDebugMIDI_Interface(), it lists baud
as an optional parameter. You can see that it's an optional parameter, because its name is followed by an equal sign (=
) and a default value in the function signature.
To specify the baud rate using this parameter, you can replace the declaration of the midi
variable in the previous snippet by the following:
Other interfaces might require multiple parameters. Consider HardwareSerialMIDI_Interface
as an example. Have a look at the documentation of its constructor HardwareSerialMIDI_Interface::HardwareSerialMIDI_Interface(), you'll see that the first required parameter is the serial port to use, and the second (optional) parameter is the baud rate.
For example:
The functions for sending MIDI messages are all listed in the MIDI_Sender class. These functions are available for all MIDI interfaces.
First, we'll give a simple example that just triggers a MIDI note every second:
First of all, compare this example to the code under Instantiating, initializing and updating a MIDI interface : you'll see that the instantiation, initialization and updating of the MIDI interface are exactly the same.
The second thing you'll notice are the two constants, note
and velocity
: these define the MIDI note number and the velocity (how hard the key is struck) to be used when sending the MIDI messages later.
The expression MIDI_Notes::C[4]
gives you the note C in the fourth octave, or middle C. The velocity is a value between 1 and 127, with higher values being louder and harder.
The MIDIAddress
data type is one of the most important types in the library, it specifies the destination of each MIDI message: in this case, it specifies the note we want to trigger. In a following section, we'll look into MIDIAddress
in a bit more detail.
In the loop function, the note
and velocity
constants are then used to actually send the MIDI messages. This is done by calling the sendNoteOn()
and sendNoteOff()
member functions of the MIDI interface. You can find the documentation in the MIDI_Sender class mentioned earlier, or you can click the brown links in the code snippet.
To send a MIDI note message, you need to know the MIDI note number of the note you want to trigger, the MIDI channel to send it to, and the virtual cable to send it over. These three pieces of information are stored by the MIDIAddress
type.
For MIDI messages other than Note On/Off, the note number might be replaced by a different type of address, such as the controller number for Control Change messages, or the patch/program number for Program Change messages, but the format remains the same:
The MIDI USB Cable Number is only used on boards that have support for multiple virtual MIDI cables over a single USB connection. As far as I know, only Teensies support this. USB-capable boards that use the MIDIUSB library do not support multiple MIDI cables.
Serial (5-pin DIN) MIDI and MIDI over Bluetooth LE have no notion of cables.
Have a look at the documentation of MIDIAddress. (You can click the link in the previous sentence). If you scroll down to the Constructors section, you'll see that there are different ways to create a MIDIAddress.
This is the most important one: MIDIAddress::MIDIAddress(int, Channel, Cable)
It allows you to specify all three of the fields explained above. The channel and the cable number have default values, so these parameters are optional. If you don't specify the channel and the cable number, it will use the first channel on the first cable.
For example:
If you specify just a single argument, as in the first example, you don't have to use curly braces ({}
). If you specify more than one argument, you need curly braces.
For entering MIDI note numbers, you can use the note names in the MIDI_Notes namespace:
If you look at the full list of note names in the MIDI_Notes namespace, you'll see that the sharps are missing: you can simply use the flats instead (e.g. instead of using G♯, you can use A♭).
Instead of specifying the controller number (address) as a number, you can also use the predefined constants in the MIDI_CC namespace:
Similar to the constants for controller numbers, you can find the constants for all General MIDI program numbers in the MIDI_PC namespace:
MIDI input is usually handled using callback functions. Whenever the MIDI interface receives a MIDI message, it calls the respective callback function that was specified by the user.
You can either use a callback for each specific message type (Note On, Note Off, Control Change ...), or a callback for each category of messages (Channel messages, System Exclusive message, System Common messages, Real-Time messages).
A third way is to poll the MIDI input manually, but this is not covered in this guide.
The FineGrainedMIDI_Callbacks class allows you to associate a callback function with each specific MIDI message type. The contents of the MIDI message (such as the channel, note number, velocity, etc.) are passed to the callback as arguments. For example:
The instantiation, initialization, and updating of the MIDI interface is the same as before. The first new component is the MyMIDI_Callbacks
class: this class inherits from the FineGrainedMIDI_Callbacks class that is provided by the library, and implements two of the callback functions, FineGrainedMIDI_Callbacks::onNoteOff() and FineGrainedMIDI_Callbacks::onNoteOn().
An instance of the MyMIDI_Callbacks
class is created, and then this callback is attached to the MIDI interface in the setup
function using MIDI_Interface::setCallbacks().
After the callback has been attached, every time you call midi.update()
in the loop
, the MIDI interface will check if any new messages have arrived, and if that's the case, it will call the corresponding callback function. For example, whenever a MIDI Note On message arrives, the onNoteOn()
function of the callback object is called.
The example above is similar to the MIDI-Input-Fine-Grained.ino example. You can find the full list of available callbacks and their arguments in the documentation for FineGrainedMIDI_Callbacks, under MIDI Callback Functions, and in the MIDI-Input-Fine-Grained-All-Callbacks.ino example.
While the fine-grained callbacks can be useful to easily handle most incoming MIDI messages, sometimes you need more flexibility. It's not always desirable to have an individual callback for each message type. The MIDI_Callbacks class allows you to specify a callback for each category of MIDI messages: Channel messages, System Exclusive messages, System Common messages and Real-Time messages. For example:
The first argument of the callback function is a reference to the MIDI interface that received the message (this is not used in this simple example), and the second argument is the message itself.
The previous example simply printed “Channel message received” whenever it received a channel message. This is not really that useful, so in this section we'll improve upon it by inspecting the contents of the messages and extracting the different components such as the channel, note number, velocity, and so on.
Have a look at the documentation for ChannelMessage. You'll notice that the message contains four fields: header
, data1
, data2
and cable
. These correspond to the contents of channel messages as specified by the MIDI standard. Apart from these fields, there are also many utility functions to access different parts of the message, such as getMessageType()
which will tell you what type of message it is (e.g. Note On, Note Off, Control Change), getChannel()
which extracts the MIDI channel number from the header, hasTwoDataBytes()
which tells you whether the message contains two data bytes (such as Note and Control Change messages) or just a single data byte (such as Program Change).
The following callback example simply checks how many bytes the message has, and then prints the contents to the serial port in hexadecimal.
If you send a Note On message on MIDI Channel 4 for note number 60 = 0x3C (middle C) with a velocity of 127 = 0x7F to the Arduino, you should see the following in the Serial monitor:
In the following example, we'll create a callback that listens for MIDI Note On and Note Off messages specifically.
If you send MIDI note messages to the Arduino, you should see output like this in the Serial monitor:
You can go even further, for example, matching only a specific note on a specific MIDI channel:
The snippet above only prints output if it receives a Note On/Off event for middle C on MIDI channel 1. All other incoming MIDI messages are ignored. Possible output in the Serial monitor could be:
The previous example can be simplified by using the MIDIAddress
type like in the previous section, and extracting the address from the incoming note message using the ChannelMessage::getAddress() function:
Let's start with a very simple example:
When you call Control_Surface.begin()
, it will automatically create a default route for you, which is shown in the following figure.
Two pipes are created, one for MIDI output (pipe_tx
in the figure), and one for MIDI input (pipe_rx
). These pipes are used to connect the Control_Surface
part of the code to the MIDI interface defined on line 4 (midi_usb
).
In the main loop of the program, Control_Surface
checks if the button was pressed, and if that's the case, a MIDI message is sent by Control_Surface
: The message travels over the MIDI output pipe (pipe_tx
) to the MIDI interface (midi_usb
), which will then send it to the computer over the USB connection.
Similarly, when a MIDI message from the computer arrives on the USB connection, the MIDI interface (midi_usb
) will send it over the MIDI input pipe (pipe_rx
) to the Control_Surface
code, which will then check whether it's a MIDI Note On/Off message that matches the address of the LED, and turn on/off the LED accordingly.
If you have multiple MIDI interfaces, you should explicitly set the default interface in your setup
function before calling Control_Surface.begin()
, using the MIDI_Interface::setAsDefault() function:
As a first exercise, we'll define the same pipes and routing ourselves, without relying on Control_Surface.begin()
to do it automagically.
As you can see, the shift operators >>
and <<
are used to define the routes between Control Surface and the MIDI interface. Note how they correspond to the arrows in the figure above.
You can specify the two endpoints in any order you like (left-to-right or right-to-left). For example, the following two lines of code are equivalent and define the same connection:
It is important to specify the routing before you call Control_Surface.begin()
. In that case, Control Surface detects that it is already connected to a MIDI interface, so it won't change anything to the manual routing we specified earlier.
If you call Control_Surface.begin()
first, it will create its default routing as discussed earlier, and if you then later add your manual connections on top of that, your code might not work as expected.
There are many situations where you want to connect both the MIDI input and output of two endpoints together. It's a bit cumbersome to have to define a separate connection for each of the two directions. Luckily, the library includes bidirectional MIDI pipes, which makes this process a bit more terse.
The following sketch does exactly the same as the previous two, just using a bidirectional pipe as a shorthand.
Connections to bidirectional pipes are made using the “vertical pipe” operator (|
).
If you only have a single MIDI interface, you don't really need to worry about pipes, as you saw in the Default routing section, you can just call Control_Surface.begin()
and it will automatically set everything up for you.
MIDI pipes really shine in scenarios where you have multiple MIDI interfaces in a single program, they allow you to fully customize the routes between them. In the following example, we'll add an extra serial MIDI interface, and route it as follows:
If you want to keep adding new connections, you need a new pipe for each one. For large numbers of pipes this is cumbersome, so the library includes a pipe factory that gives you a new pipe every time you use it.
Using the factory, the previous example can be simplified as follows:
The number of pipes that the factory can produce is specified between angle brackets (<>
). If you try to use more pipes than specified, you will get an error at run time, which is indicated by a blinking on-board LED.
Instead of specifying a specific pipe when doing the routing in the setup
function, you can now just use the pipes
factory, it will give you a new pipe each time you use it.
If you need bidirectional pipes, you can use the BidirectionalMIDI_PipeFactory class instead.
Up to now, we've only created connections between Control_Surface
itself and a MIDI interface, but you can also create routes between MIDI interfaces. This is demonstrated in the following example, where we connect a debug MIDI interface to a MIDI over USB interface.
In this case, all messages that arrive on the midi_dbg
interface are sent to the midi_usb
interface, and all messages that arrive on midi_usb
are sent to midi_dbg
.
Now that we're no longer using Control_Surface.begin()
and Control_Surface.loop()
, we have to take care of initializing and updating the MIDI interfaces ourselves.
If you have an Arduino with native MIDI over USB support, you can try out this example yourself:
Open your favorite MIDI monitor and select the MIDI over USB port of the Arduino. Then open the Serial Monitor, set the line ending to “Newline”, and then enter a MIDI message, e.g. 90 3C 7F
. You should see the message arrive in the MIDI monitor as a Note On message for note C4 on channel 1 with a velocity of 127. Similarly, if you send a message to the Arduino using the MIDI monitor, it will be printed in the Serial Monitor, because we used a bidirectional pipe.
If you only want to print MIDI messages, you can use the USBDebugMIDI_Output class instead of USBDebugMIDI_Interface. USBDebugMIDI_Output uses less memory, and one of the issues of having multiple USBDebugMIDI_Interfaces is that input cannot work (some of the characters you type in are sent to one of the interfaces, some to the others), USBDebugMIDI_Output doesn't have this issue.
In the following example, Control Surface works normally, using the USB MIDI interface: you can try sending note on/off messages for note C4 and it'll turn on/off the on-board LED, and if you press/release the button on pin 2 you'll receive note on/off messages.
Additionally, all MIDI input that arrives on the MIDI over USB interface is routed to a secondary MIDI debug output, allowing you to use the Serial Monitor to inspect the messages being sent to the Arduino.
It is possible to create custom MIDI pipes that can filter out certain MIDI messages, or even modify the MIDI messages that pass through them (for instance, to change the channel of certain messages). This is done by inheriting from the MIDI_Pipe class and overriding the MIDI_Pipe::mapForwardMIDI functions. See MIDI_Pipes-Filter.ino for more details.