This document describes a set of portable C utilities for MIDI input and output. The purpose is to provide a simple, portable API for MIDI applications. This code is intended for use in interactive and general purpose systems, and should be portable to virtually any computer system that supports C and has a MIDI hardware. This API is not intended to replace MIDI drivers and MIDI systems. The goal is to provide a uniform access method to various hardware and software systems.
There is basically one interesting data type: midi_type is a pointer to a descriptor for a MIDI stream, which is either being read from or written to a MIDI interface. The midi_type contains a structure that describes the device and other options.
Routines exist to initialize MIDI I/O (midi_open()), perform transfers (midi_read(), midi_write()) and to finalize a transfer (midi_close()). A companion library supports an internal format for midi-like data and for file I/O.
#define MIDI_STRING_MAX 258 typedef struct midi_struct { short write_flag; /* MIDI_IN, or MIDI_OUT */ char interfacename[midi_string_max]; /* (optional) to specify interface */ char devicename[midi_string_max]; /* (optional) to specify device */ time_get_proc_type get_time; /* where to get the time */ void *time_info; /* pass this to get_time() */ void (*callback)(void *instance); /* if non-null, this routine * gets called every granularity ms. It must poll to get MIDI input */ void *callback_info; /* pass this to callback() */ long granularity; /* period in ms with which callback is called */ long *buffer; /* input or output buffer */ long buffer_len; /* how big is the buffer */ long latency; /* time delay in ms between timestamps and actual output */ /* set to zero to get immediate, simple blocking output */ /* if latency is zero, timestamps will be ignored */ int overflow; /* set to non-zero if input is dropped */ struct midi_struct *thru; /**************************/ /* for internal use only: */ long head; long tail; midi_fns_type dictionary; /* implementation functions */ void *descriptor; /* system-dependent state */ } midi_node, *midi_type;
The meanings of fields are as follows:
After calling midi_open(), all fields should be considered read-only at best except for overflow, which may be cleared. Be aware that this flag may be set asynchronously by another thread. Results are undefined if you write any other fields while a midi_type is open.
All routines described here measure data in units of frames (not samples, not bytes).
One exception is snd_seek()
, which measures file position in seconds of time,
expressed as a double.
int midi_open(midi_type midi);
To open MIDI input or output, fill in fields of a midi_type and call midi_open(). If buffer is NULL, a buffer is allocated using malloc().
Returns MIDI_SUCCESS iff successful.
int midi_close(midi_type midi);
Closes an open MIDI input or output.
Returns MIDI_SUCCESS iff successful.
long midi_read(midi_type midi, long *buffer, long length);
Read up to length longs into buffer. MIDI data is encoded as a sequence of pairs of longs as follows:
timestamp
port number first byte second byte third byte fourth byte fifth byte etc. ... The first long is a timestamp indicating milliseconds. The next long begins with a port number in the low-order byte. A port number is used to distinguish which port the message came from in the case of an input device with multiple MIDI ports. With the exception of system exclusive messages, MIDI messages take at most 3 bytes, which are in successively higher-order bytes. With system exclusive messages, the data may take multiple longs, in which case the data will consist of 4 byte units of data until the end-of-sysex byte. In any case, the last long of the full message is padded to fill out a multiple of 4 bytes.
Returns the number of longs actually read.
int midi_write(midi_type snd, void *buffer, long length);
Writes length longs from buffer to MIDI output. The data should be a well-formatted MIDI message, including a port number as shown above.
Returns number of longs actually written.
int midi_write_short(midi_type snd, unsigned long when, unsigned long msg);
Writes a short MIDI message with the given timestamp and data. This cannot be used for a sysex message.
long midi_poll(midi_type snd);
Returns the number of pending longs to be read or the number of longs that can possibly be written. Many implementations will not actually buffer output data, so the result may be a large constant. There is no guarantee that writes will not block even if they are shorter than the result of midi_poll().
int midi_flush(midi_type snd);
After the last write, call midi_flush() to transfer data from the buffer to the output device. midi_flush() returns immediately, but it only returns MIDI_SUCCESS after the data has been output to the MIDI output device. Since calling midi_close() may terminate output, the proper way to finish MIDI output is to call midi_flush() repeatedly until it returns MIDI_SUCCESS. Then call midi_close() to close the audio device and free buffers.
In some implementations, data is flushed immediately, so this call has no effect.
int midi_in_device(int n, char *interf, char *device);
Sets strings describing the n-th audio device.
interf
is set to the interface name anddevice
is set to the device name. Both should be allocated to be at leastmidi_string_max
bytes in length. Returns NULL if n is greater or equal to the number of MIDI devices. Available devices are numbered, starting with the default device at n=0. Before opening a MIDI device, an application can use this to enumerate all possible devices, select one (e.g. by presenting a list to the user), and then copy the strings into the devicename andinterfacename
fields of the midi_type structure. If the devicename field is the empty string, device 0 will be opened.Note that there are separate calls for MIDI input and output because, often, MIDI output can be directed to a local synthesizer and other options that do not apply to MIDI input.
There is no reset command because an asynchronous process might be filling the buffer with data at the same time the application is trying to empty it. To flush incoming data, either read complete messages until none are left, being careful to not leave the system in a state where more system exclusive data is arriving, or close the interface and reopen the device.
See testout.c and latency.c for examples of:
This library supports different application styles, some of which are described here:
The simplest application sends MIDI data immediately without accurate timing. Set latency to zero and callback to NULL. When MIDI data is written, it will be sent immediately. This style of interface might also be best for applications running their own high-priority thread that takes care of all MIDI timing.
Applications can compute MIDI data ahead of time and send it with time stamps. Set latency to a positive number of milliseconds, e.g. 100, set callback to NULL, and set get_time to a routine, typically time_get() (see section "Time" below). Call time_start() to initialize time and send MIDI data with desired timestamps. The library will attempt to output MIDI messages at their timestamp plus latency ms. It is best not to try to compute too far ahead of real time, as there are normally latency/2 message buffers allocated to queue up future messages. The intended strategy to send a message at time t is to wait until time_get() returns at least t, and then send the message with timestamp t. The message will actually be output at t + latency. The application may block if it tries to get too far ahead and the library runs out of buffer space.
If the application must run with minimal latency, you must either schedule a high-priority thread to poll for MIDI input, or you must use a callback. Callbacks are preferred on the Macintosh and may be simpler on any platform. Set latency to zero, and set callback to your own application routine. Set granularity to the time interval with which callback should be called, e.g. 2 (milliseconds). Set callback_info to any pointer. In general, there should only be one callback function, even if you have both MIDI input and MIDI output open. Just pick one to provide callbacks and set callback to NULL on the other. Within the callback, you should test for incoming MIDI, do any time-based processing, and send MIDI output. Callbacks are asynchronous and the library is not reentrant, so if you read/write MIDI from the callback, you must not read/write MIDI from elsewhere in the application. No provision is made for communication between the application thread and the callback, but a single reader/single writer circular buffer is recommended. See the enqueue and dequeue routines in midi.c for an example implementation.
Callbacks and buffering (non-zero latency) may be combined; however, this may or may not reduce jitter.
To modify or extend the Midilib code, it is important to understand the architecture and design. The main issues are the structure used to obtain portability, and the support for multiple device interfaces within a given system.
The include file midi.h declares most of the library structures and routines. midi.h includes midiconfig.h, which handles system dependencies.
System-dependent code is selected using conditional compilation. The following compile-time symbols are defined:
midiconfig.h is responsible for defining a number of routines and/or macros, including the macro that selects the system. E.g. under Visual C++, the macro _WIN32 is defined, so midiconfig.h defines WIN32. If _WIN32 is defined. The other routines and macros to be defined are described in midiconfig.h itself. To avoid too many conditional compilation statements that make code hard to read, midiconfig.h works by conditionally including another .h file. The files are named midiwin32.h, midilinux.h, midiirix.h, and midimac.h, and other systems should implement include files in the same manner.
To support multiple interfaces, the library has the call midi_xx_devicename() which returns the name of the nth audio device. Where do these names come from? System-specific parts of the library call a non-system specific function as follows:
/* these types are for internal use: */ typedef int (*midi_reset_fn)(midi_type midi); typedef long (*midi_poll_fn)(midi_type midi); typedef long (*midi_read_fn)(midi_type midi, void *buffer, long length); typedef long (*midi_write_fn)(midi_type midi, void *buffer, long length); typedef int (*midi_open_fn)(midi_type midi, long *flags); typedef int (*midi_close_fn)(midi_type midi); typedef int (*midi_reset_fn)(midi_type midi); typedef int (*midi_flush_fn)(midi_type midi); typedef struct { midi_reset_fn reset; midi_poll_fn poll; midi_read_fn read; midi_write_fn write; midi_open_fn open; midi_close_fn close; midi_reset_fn reset; midi_flush_fn flush; } midi_fns_node, *midi_fns_type; void midi_add_in_device(char *devicename, midi_fns_type dictionary); void midi_add_out_device(char *devicename, midi_fns_type dictionary);
This is called for each different device or interface. In the general case, there might be several physical devices, each supporting several logical devices, and there might be several different system APIs that access these (MM and DirectSound). The system-specific code provides a string name for each of these and a dictionary of function pointers for each.
When does the system-specific code call midi_add_xx_device()? When either midi_open() or midi_devicename() is called for the first time, a call is made to midi_init(), which is defined in system-specific code. midi_init() is responsible for calling detection and initialization code for each supported device.
The midi_fns_node structure contains function pointers that implement the library functions. A pointer to this structure is found in the midi_type structure which is passed to nearly every library function. These library functions are implemented by making indirect calls through these function pointers.
Most implementations will receive MIDI data from another thread, callback, or interrupt routine and place it in the buffer. To avoid locks and other system calls, this interface is designed so that data passes through a circular buffer with a single producer and consumer, and the buffer code is lock-free and safe for separate producer and consumer threads. To keep this implementation safe, there is no call to clear or reset the buffer (the producer might be inserting at the time the buffer is cleared).
Access to time is provided by a separate library. The calls are:
void time_start(int resolution)
Initializes a timer. The parameter specifies resolution in milliseconds.
int time_started()
Returns true iff
time_start()
was called already.
unsigned long time_get()
Accesses the timer. The result is in milliseconds.