McLaren MIDI Kit - Introduction

The McLaren MIDI Kit (prefix "MMK") is an Objective-C library implementing the RTP-MIDI protocol. The software components of the engine are similar to those distributed in the McLaren Labs rtpmidi product. The MMK Library encapsulates much of the logic of the RTP-MIDI protocol so that MIDI-over-IP networks can be easily implemented into your own software projects.

RTP-MIDI Background

RTP (Real-Time Transport Protocol) is a network protocol for delivering media packets over IP networks. The most typical packets sent over RTP are audio and video packets, and RTP is the foundation of most audio and video conferencing systems.

RTP typically runs over UDP (User Datagram Protocol) rather than TCP (Transmission Control Protocol) in order to obtain low latency and to avoid the overhead of TCP.

RTP-MIDI is a specialization of RTP for sending MIDI (Musical Instrument Digital Interface ) information over an IP network. RTP-MIDI is used over UDP. But because UDP does not guarantee message delivery and it does not include error-correction, the RTP-MIDI standard defines an error-correction protocol for detecting missing information and correcting it in a musically acceptible manner and with low latency. Both are necessary in live-music performance settings.

Note: The McLaren MIDI Kit implements the RTP-MIDI protocol including a forward error-correction algorithm compatible with the standard and popular implementations. (These are the sending and receiving recovery Journals.)

Because RTP-MIDI is implemented using standard UDP packets, RTP-MIDI collaborations can occur on local networks using Ethernet connections or WiFi, or even over the internet for remote real-time musical collaborations. The combination of low-latency and general applicability is what makes RTP-MIDI an interesting and useful tool to have available.

RTP-MIDI Basic Operation

In a simple RTP-MIDI set up, two hosts wish to connect their MIDI streams over an IP network. Each host operates a pair of UDP ports (5004 and 5005) that send and receive packets with the corresponding port on the remote host. An RTP-MIDI Engine manages the translation of MIDI events into UDP packets to be sent or received over the network.

RTP-MIDI Operation

An RTP-MIDI instance on a host uses two UDP ports, and they are always adjacent numbered N and N+1. It is typical to say that RTP-MIDI runs on "port 5004" with the understanding that both ports 5004 and 5005 will be used.

A host may choose to run an RTP-MIDI session on any pair of UDP ports, and the UDP ports of the hosts involved in the RTP-MIDI stream do not need to the same. A host running on ports 5004/5005 can communicate with ports 6010/6011 on a remote host.

RTP-MIDI Ports

Session Set-up and Operation

Before the RTP-MIDI stream is established between two hosts, a "set-up" phase occurs. In this phase, one host "Invites" the other to join its MIDI stream by sending an INVITATION message. The INVITATION message includes the "Session Name" of the inviting host. The receiving host reponds with an ACCEPT message that includes its "Session Name". It can also reject the invitation with a REJECT message. After the ACCEPT message, the two hosts coordinate their clocks by sending a series of SYNCHRONIZATION messages.

RTP-MIDI Set-Up and Operational Modes

The set-up phase establishes a "Session" between the two hosts: this is the normal operational mode in which MIDI can be sent. By definition, MIDI payload messages are sent over port N+1 and control messages that implement feedback about the error-correction of the MIDI stream are sent over port N.

Sessions and Participants

An RTP-MIDI Session is the "virtual path" from one pair of MIDI IN/OUT to another pair of MIDI IN/OUT. It includes the state on the first computer as well as the second computer that tracks the error correction state of the MIDI stream. Each end of this "virtual path" is called a Participant.

Elements of a Session

In practical terms, a Participant is identified by a specific IP Address and Port, and a Session is a running connection between two Participants.

Each side of a Session knows the name of the other Participant as the "Session Name" of the RTP-MIDI Engine running on the remote host, because the "Session Names" were exchanged in the INVITATION and ACCEPT messages.

Multiple Hosts

One RTP-MIDI host may participate in MIDI streams with multiple hosts. In the figure below one host is shown sending and receiving MIDI data to two remote hosts simultaneously.

RTP-MIDI Multiple Streams

In this configuration, the MIDI data received from Host 2 and Host 3 will be "merged" into one stream received by Host 1. Notice that the MIDI-Merge functionality in an RTP-MIDI system does not require special hardware. Similarly, the MIDI data from Host 1 will be sent to both Host 2 and Host 3.

Multiple Sessions

A single computer can run multiple RTP-MIDI streams at the same time over different ports, by running multiple instances of an RTP-MIDI processing Engine. Since each Session carries 16 MIDI channels, adding Sessions is a way to use more distinct MIDI channels. In the figure below, Host 1 is running two RTP-MIDI Engines with Session Names "host1A" and "host1B". The Engines on Host 2 have Session Names "host2A" and "host2B", and Host 3 has just one Engine with Session name "host3".

RTP-MIDI Multiple Sessions

In general most computers can run multiple RTP-MIDI Engines. For computers running multiple Engines, it is customary for each Session Name to be unique. It is this unique name that it advertised over Bonjour to nearby hosts along with the port-pair that the Engine is running on.

Terminology

The terminology used in this section is defined below for an easy reference.

Term Definition
Bonjour a protocol for announcing network services to nearby hosts
Endpoint the IP address and UDP Port of an RTP-MIDI Engine
Engine the software managing connecting one UDP port pair to one MIDI In/Out
Host a computer with an IP network address running one or more RTP-MIDI sessions
MIDI Stream a series of MIDI messages
Participant one end of an RTP-MIDI Session
Peer the other end of a Session
Port an abstraction identifying a UDP stream with a unique number
RTP Real-Time Protocol
RTP-MIDI Real-Time MIDI Protocol
Session a "virtual path" from one set of MIDI In/Out to another MIDI In/Out over a network
Session Controller an RTP-MIDI Engine instance
UDP User Datagram Protocol

The MMK RTP-MIDI Engine

The McLaren MIDI Kit makes it easy to launch an RTP-MIDI Engine and send and receive MIDI Events. It has one main class called MMKEngine and a helper class called MMKPartipant. An instance of an MMKEngine manages one pair of UDP Ports to send and receive a MIDI In/Out stream over one or more Sessions.

MMKEngine Introduction

A MIDI stream is mapped into ObjC by a pair of methods. One method sends a sequencer event to attached Participants, and another registers a callback to handle received MIDI Events.

Initializing an MMKEngine

The steps to instantiating and using an MMKEngine are the following.

  • Allocate an Initialize - this will obtain the UDP ports and allocate some data structures
  • Configure the Engine properties and set callbacks
  • Start the Engine running

A sketch is shown below.

#import "McLarenMidiKit/McLarenMidiKit.h"

int main(int argc, char *argv[]) {

  NSError *err;
  MMKEngine *engine = [[MMKEngine alloc] initWithPort:5004 error:&err];
  if (err != nil) {
    NSLog(@"Error configuring Engine:%@", err);
    exit(1);
  }

  // configure the session name and whether it is advertised
  engine.sessionname = "mmk:5004";
  engine.advertiseBonjour = YES;

  // configure callbacks ...
  [engine onEngineError:^(NSError *err) {
    NSLog(@"Engine Error:%@", err);
    }];

  // start the engine running
  [engine start];

}

Send and Receive MIDI Events

To send a MIDI event.

  ASKSeqEvent *evt = [[ASKSeqEvent alloc] init];
  // ... configure event

  [engine sequencerEvent:evt];

Register a callback to handle received events.

  [engine onSequencerEventReceived:^(ASKSeqEvent *evt) {
    NSLog(@"Received MIDI:%@", evt);
    }];

Stop an MMKEngine

Stop the engine

  [engine stop];

Accepting an Incoming INVITATION Request

As implemented, the MMKEngine accepts all incoming INVITATION requests that are well-formed.

Initiating a Call to a Remote Host

To launch a new connection with a remote host, simply use the call method with the IP Address and port that you wish to connect to.

  [engine call:@"10.0.0.44" onPort:5006 error:&err];
  if (err != nil) {
    NSLog(@"Error calling:%@", err);
  }

If the returned error is NIL, the call got launched, but that does not yet mean it is successful. The call proceeds by sending an INVITATION message and then doing the handshake that sets up a running connection. During any of this time, asynchronous errors may be emitted by the onEngineError: callback.

IPv4 and IPv6

By default, the Engine uses IPv6 and allocates an IPv6 socket for sending and receiving. The Engine recognizes an address of the form "192.168.1.2" as an IPv4 address and forms the UDP packet to be sent over an IPv6 socket.

In some cases, this technique does not work well with older hosts implementing only the IPv4 protocol. In these instances, you can direct the MMKEngine to use only IPv4 for all processing using the class method shown below.

   [MMKEngine shouldUseIpv4Only];

Obtaining the list of Participants

The MMKEngine maintains a list of Participants and makes it available for your code to inspect. For example, to list the current participants by Session Name and remote Port, the following loop suffices.

  NSArray<MMKParticipant> *participants = [engine getParticipants];
  for (MMKParticipant *p in participants) {
    NSLog(@"Participant %@:%lu", [p getHName], [p getPort]);
  }

The engine also provides callbacks to notify your code about modifications to this list. Note that the participant list returned is an alias to the same array that the MMKEngine is manipulating, so you can obtain it once and watch for changes with the onParticipantAdded: and onParticipantDeleted: callbacks.

  NSArray<MMKParticipant> *participants = [engine getParticipants];

  [engine onParticipantAdded:^(MMKParticipant *p) {
    NSLog(@"There is a new participant in the list");
    }];

  [engine onParticipantDeleted:^(MMKParticipant *p) {
    NSLog(@"This participant is about to be removed from the list.");
    }];

A note about onParticipantDeleted:: by the time this method is called, most of the information about the Participant has been erased. There will be no address or state anymore. The only information still valid is the address of the Participant itself. If your code is keeping a backing-store of participant information, this is enough to know which Participant is being deleted.

Obtaining information about a Participant

An MMKParticipant instance has methods that return information about its session. These are described below.

  • getState: - return the current state of the Session.
  • getHname: - return the Session Name of the (remote) Participant
  • getAddr: - return the (remote) IP Address that was used to establish the Session with this Participant
  • getPort: - return the Port of the (remote) Participant
  • getIsLocked: - return YES if the Session is fully established
  • getRtt: - return the last computed "round-trip time" between this Engine and the remote

Obtaining Notifications about changes to Participants

There are a number of methods for registering callback blocks to learn of state changes to participants.

- (void) onParticipantStateUpdated:(void(^)(MMKParticipant *part))block;
- (void) onParticipantMidiReceived:(void(^)(MMKParticipant* part, double delta))block;
- (void) onParticipantRttUpdated:(void(^)(MMKParticipant *part))block;
  • onParticipantStateUpdated: the state of the referenced participant has been updated.
  • onParticipantMidiReceived: the referenced participant received a MIDI message. The delta time is an estimate of the flight time from sender to receiver.
  • onParticipantRttUpdated: a clock synchronization sequence has occurred and the computed Round-trip Time between this Engine and the Participant has been updated.

Terminating the Session of a Participant

The bye method of an MMKParticipant can be used to send the BYE message to the remote peer to begin terminating the Session.

Dispatch Queues

The MMKEngine employs a number of GCD Dispatch Queues. One is allocated for UDP packet handling and another is allocated for the MMKEngines.

There are some simple rules that client code should know to ensure smooth operation.

  1. Calls to sequencerEvent: that send MIDI messages should all be serialized on ONE dispatch queue.
  2. Calls to the onSequencerEventReceived: callback block will be serialized on the MMKEngine shared dispatch queue.

Logging

The engine produces many log events, and these can be helpful for understanding what is happening. Alas, it can also be distracting in command-line programs. Log events are produced using NSLog, which goes to stderr by default. Redirecting stderr to a file and using stdin/stdout for command-line processing is helpful.

It is possible to redirect logging to Syslog by using the a feature of GNUStep itself. When you start your program, add the options -- -GSLogSyslog YES to the command line when you run it. This sends messages to /var/log/syslog on Ubuntu 22.