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.


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;
}