Tutorial 24. MIDI Input
Controlling MEAP with a keyboard. |
MIDIis probably the single most important invention in the world of audio electronic in the past 50 years. It is a universal standard that lets instruments, sequencers, computers and more talk to each other and at its core it has not changed since its introduction in 1983, meaning that a modern macbook can talk to 80s synthesizers with no problem.
The important thing to remember about MIDI is that it does not carry audio signal. It is merely a set of messages telling audio equipment to do various things. For example, you may have a MIDI keyboard and a synthesizer. The MIDI keyboard could tell the synthesizer to play a note, which the synthesizer will do in whatever way it has been internally configured to play the note, but the MIDI keyboard will never generate sound on its own. MIDI messages can do much more complex things than triggering notes as well, from modulating timbre, to synchronizing sequencers to storing and retrieving patch data. In this tutorial, we will just focus on the basic case of triggering notes on MEAP with an external keyboard.
Some background on MIDI hardware.
There are some ways in which modern MIDI can differ from classic MIDI though.
- Hardware MIDI:Though MIDI messages themselves haven't really changed, the way they are transmitted has.
MIDI was developed primarily by Roland, and because of this they had the final say on what connectors MIDI is usually
send over. Because they were already using 5-pin DIN connectors for a sync protocol they were using before the creation of MIDI,
they decided that, for their convenience, they would make that the standard MIDI connector.
Just about every older MIDI-compatible device uses this connector and it has some benefits and drawbacks. This DIN connector is not really used for much else in the world of music technology, so you don't have to worry about anyone accidentally plugging a MIDI signal into an audio jack, but you also always need this specialized cable that you won't have any other uses for. A big drawback, especially if you are designing a piece of equipment however, is that 5-pin DIN connectors are unnecessarily bulky. You may notice that MIDI is a 3-pin protocol, meaning that the two extra pins on the 5-pin connector above are actually not used at all! We could theoretically send MIDI over any cable that has at least three connectors. Recently, more and more manufacturers have started using stereo 3.5mm cables as a way of sending MIDI because the connectors are smaller are the cables are more ubiquitous. A few manufacturers implemented this differently until in 2018, the MIDI Association officially released a standard.
MIDI equipment manufactured from here on out should hopefully follow that standard, ushering in a new era of standardized smaller footprint MIDI jacks. Because these jacks deal with the exact same data, baud rates and currents as 5-pin MIDI, they are totally compatible with devices that use DIN MIDI through the use of cables or adapters.
MEAP uses this Type A minijack standard to fit both a MIDI input and output jack (the two pink jacks) into its small enclosure.
- USB MIDI: Another tricky form of MIDI is USB MIDI, which is a bit further away from the initial MIDI
standard but which has become very common lately. Again, the fundamental messages of USB MIDI (note on, control change, etc) stay the same, but the
way they are transmitted over USB is totally incompatible with hardware MIDI. With the proliferation of computers in electronic music systems, it
became increasingly desirable to connect general purpose computers (which of course are not outfitted with 5-pin DIN jacks) directly
to MIDI equipment. Similar to the creation of the MIDI standard, in the 90s, different computer manufacturers came together to create a universal
standard for serial communication in computer environments, resulting in the creation of USB, which quickly was included in most
computers on the market. The MIDI association decided to standardize a way of transmitting MIDI over USB so that it could
be easily sent to any computer. Though USB connectors and protocols have changed drastically throughout the years, for the most part they are compatible, and
USB MIDI has stood the test of time. MIDI sent over USB uses USB's electrical specs rather than MIDI's so it has a different baud rate, voltages and more than
DIN MIDI so even a theoretical USB to DIN cable adapter would not be able to interface these two different kinds of signal.
USB MIDI is most useful in systems that involve a computer and is somewhat deficient in connecting standalone hardware to each other. If you connect several USB MIDI devices to a computer it can handle all of these MIDI streams and route them where they need to go. On the other hand some of this handling and routing can be tricky to do without the interface of a computer, so in addition to the fact that MIDI keyboards are often powered over the same USB jack that powers them, it can be difficult to connect USB MIDI hardware directly to other MIDI devices.
- Other MIDI transmission: similar standards have also been created to transmit MIDI over serial ports, Firewire, Ethernet, Bluetooth and more but that won't be discussed here!
All of that said, it is easy to connect a DIN MIDI keyboard to MEAP with one of the adapter cables linked above, but though it is likely possible with some modifications, a system has yet to be developed to connect USB MIDI to MEAP.
MEAP uses the Arduino MIDI libary which is documented here.
Up until now, you've probably used the MEAP_BASIC_TEMPLATE program as the basis of all of your sketches. If you want to use MIDI, I would recommend using MEAP_MIDI_TEMPLATE_WITH_CLOCK instead. It includes a few more code blocks that will be useful for dealing with MIDI input, output and synchronization.
The first thing you may notice is some code in the loop() function, which we don't normally have. This continuously listens for MIDI messages at the MIDI input jack and will call the midiEventHandler() function if it receives one. midiEventHandler() is where we parse the message and decide what to do with it.
If you scroll down to the bottom of the sketch, you will find midiEventHandler(), which runs once every time a new MIDI message is received. First, we are given the variables,
channel
, data1
, and data2
which give us information about the message. The channel tells us which MIDI channel (1-16) the
message was received on. data1 and data2 differ depending on the message type but for example, in a noteOn message, data1 tells us the
note number and data2 tells us the note velocity (aka amplitude).
Additionally, you will see a switch statement that checks what type of message we have received. This statement is not exhaustive, but covers most of the commone message types we'll be dealing with. If you want something to occur when a certain message type is received, you add your code to that message type's case statement.
A list of MIDI message types and what the two data bytes represent is here.
In our example, we only care about note on and note off messages, so we'll be coming back to those case statements in a bit.
First, let's create a basic synth voice that we can play with our keyboard: just a sine wave connected to an ADSR envelope. We're not going to worry about polyphony in this tutorial.
We set up that voice and connect it in updateAudio as normal, and then we just trigger the note to start when we receive a note on message and stop when we receive a note off message in midiEventHandler()
Connect a cable from the MIDI output jack of a MIDI keyboard to the MIDI input jack on MEAP (the top of the two pink jacks), play some notes and you should hear a sine wave on MEAP!
FULL CODE BELOW
/*
Extension of basic template to include a framework for handling midi messages.
Implements a basic 24 Pulse-Per-Quarter note clock (the MIDI standard that most
devices use for synchronization) which can be generated internally or received
from an external MIDI clock source.
*/
#define CONTROL_RATE 64 // Hz, powers of 2 are most reliable
#include <Meap.h> // MEAP library, includes all dependent libraries, including all Mozzi modules
Meap meap; // creates MEAP object to handle inputs and other MEAP library functions
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); // defines MIDI in/out ports
enum ClockModes {
kINTERNAL,
kEXTERNAL
} clock_mode;
// MIDI clock timer
uint32_t clock_timer = 0;
uint32_t clock_period_micros = 10000; // dummy value, gets overwritten in setup
int clock_pulse_num = 0;
float clock_bpm = 120; // BPM when in internal clock mode
// ---------- YOUR GLOBAL VARIABLES BELOW ----------
#include <tables/sin8192_int16.h>
mOscil<sin8192_int16_NUM_CELLS, AUDIO_RATE, int16_t> osc(sin8192_int16_DATA);
ADSR<AUDIO_RATE, AUDIO_RATE> env;
void setup() {
Serial.begin(115200); // begins Serial communication with computer
Serial1.begin(31250, SERIAL_8N1, 43, 44); // sets up MIDI: baud rate, serial mode, rx pin, tx pin
startMozzi(CONTROL_RATE); // starts Mozzi engine with control rate defined above
meap.begin(); // sets up MEAP object
clock_mode = kINTERNAL; // set the midi clock mode to internal, ignores incoming clock messages
clock_period_micros = meap.midiPulseMicros(clock_bpm); // converts BPM into number of microseconds per 24 PPQ MIDI clock pulse
// ---------- YOUR SETUP CODE BELOW ----------
env.setADLevels(255, 200);
env.setTimes(1, 250, 100000000, 500);
}
void loop() {
audioHook(); // handles Mozzi audio generation behind the scenes
if (MIDI.read()) // Is there a MIDI message incoming ?
{
midiEventHandler(); // function that parses midi messages, be careful about doing too much processing
// in here because it could disrupt audio generation
}
// handle generating midi clock if internal clock mode is selected
if (clock_mode == kINTERNAL) {
uint32_t t = micros();
if (t > clock_timer) {
clock_timer = t + clock_period_micros;
MIDI.sendRealTime(midi::Clock); // sends clock message to MIDI output port
clockStep();
}
}
}
/** Called automatically at rate specified by CONTROL_RATE macro, most of your mode should live in here
*/
void updateControl() {
meap.readInputs(); // reads DIP switches, potentiometers and touch inputs
// ---------- YOUR updateControl CODE BELOW ----------
}
/** Called automatically at rate specified by AUDIO_RATE macro, for calculating samples sent to DAC, too much code in here can disrupt your output
*/
AudioOutput_t updateAudio() {
int64_t out_sample = 0;
env.update();
out_sample = osc.next() * env.next();
return StereoOutput::fromNBit(24, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
}
/**
* Runs whenever a touch pad is pressed or released
*
* int number: the number (0-7) of the pad that was pressed
* bool pressed: true indicates pad was pressed, false indicates it was released
*/
void updateTouch(int number, bool pressed) {
if (pressed) { // Any pad pressed
} else { // Any pad released
}
switch (number) {
case 0:
if (pressed) { // Pad 0 pressed
Serial.println("t0 pressed ");
} else { // Pad 0 released
Serial.println("t0 released");
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
} else { // Pad 1 released
Serial.println("t1 released");
}
break;
case 2:
if (pressed) { // Pad 2 pressed
Serial.println("t2 pressed");
} else { // Pad 2 released
Serial.println("t2 released");
}
break;
case 3:
if (pressed) { // Pad 3 pressed
Serial.println("t3 pressed");
} else { // Pad 3 released
Serial.println("t3 released");
}
break;
case 4:
if (pressed) { // Pad 4 pressed
Serial.println("t4 pressed");
} else { // Pad 4 released
Serial.println("t4 released");
}
break;
case 5:
if (pressed) { // Pad 5 pressed
Serial.println("t5 pressed");
} else { // Pad 5 released
Serial.println("t5 released");
}
break;
case 6:
if (pressed) { // Pad 6 pressed
Serial.println("t6 pressed");
} else { // Pad 6 released
Serial.println("t6 released");
}
break;
case 7:
if (pressed) { // Pad 7 pressed
Serial.println("t7 pressed");
} else { // Pad 7 released
Serial.println("t7 released");
}
break;
}
}
/**
* Runs whenever a DIP switch is toggled
*
* int number: the number (0-7) of the switch that was toggled
* bool up: true indicated switch was toggled up, false indicates switch was toggled
*/
void updateDip(int number, bool up) {
if (up) { // Any DIP toggled up
} else { //Any DIP toggled down
}
switch (number) {
case 0:
if (up) { // DIP 0 up
Serial.println("d0 up");
} else { // DIP 0 down
Serial.println("d0 down");
}
break;
case 1:
if (up) { // DIP 1 up
Serial.println("d1 up");
} else { // DIP 1 down
Serial.println("d1 down");
}
break;
case 2:
if (up) { // DIP 2 up
Serial.println("d2 up");
} else { // DIP 2 down
Serial.println("d2 down");
}
break;
case 3:
if (up) { // DIP 3 up
Serial.println("d3 up");
} else { // DIP 3 down
Serial.println("d3 down");
}
break;
case 4:
if (up) { // DIP 4 up
Serial.println("d4 up");
} else { // DIP 4 down
Serial.println("d4 down");
}
break;
case 5:
if (up) { // DIP 5 up
Serial.println("d5 up");
} else { // DIP 5 down
Serial.println("d5 down");
}
break;
case 6:
if (up) { // DIP 6 up
Serial.println("d6 up");
} else { // DIP 6 down
Serial.println("d6 down");
}
break;
case 7:
if (up) { // DIP 7 up
Serial.println("d7 up");
} else { // DIP 7 down
Serial.println("d7 down");
}
break;
}
}
/**
* Called whenever a MIDI message is recieved.
*/
void midiEventHandler() {
int channel = MIDI.getChannel();
int data1 = MIDI.getData1();
int data2 = MIDI.getData2();
switch (MIDI.getType()) // Get the type of the message we received
{
case midi::NoteOn: // ---------- MIDI NOTE ON RECEIVED ----------
osc.setFreq(mtof(data1));
env.noteOn();
break;
case midi::NoteOff: // ---------- MIDI NOTE OFF RECEIVED ----------
env.noteOff();
break;
case midi::ProgramChange: // ---------- MIDI PROGRAM CHANGE RECEIVED ----------
break;
case midi::ControlChange: // ---------- MIDI CONTROL CHANGE RECEIVED ----------
break;
case midi::PitchBend: // ---------- MIDI PITCH BEND RECEIVED ----------
break;
case midi::Clock: // ---------- MIDI CLOCK PULSE RECEIVED ----------
if (clock_mode == kEXTERNAL) {
clockStep();
}
break;
case midi::Start: // ---------- MIDI START MESSAGE RECEIVED ----------
break;
case midi::Stop: // ---------- MIDI STOP MESSAGE RECEIVED ----------
break;
case midi::Continue: // ---------- MIDI CONTINUE MESSAGE RECEIVED ----------
break;
}
}
// Executes when a clock step is received. Each "if" statement represents a musical division of a quarter note.
// For example, if you want an event to occur every eigth note, place the code for this event within the
// second if statement. If you want events to happen at different subdivisions of a quarter note add more if
// statements checking the value of clock_pulse_num.
void clockStep() {
if (clock_pulse_num % 24 == 0) { // quarter note
}
if (clock_pulse_num % 12 == 0) { // eighth note
}
if (clock_pulse_num % 6 == 0) { // sixteenth note
}
if (clock_pulse_num % 3 == 0) { // thirtysecond notex
}
clock_pulse_num = (clock_pulse_num + 1) % 24;
}