IRIX 5.3 » Books » Developer »
IRIS Digital Media Programming Guide
(document number: 007-1799-040 / published: 1994-11-14)
table of contents | additional info | download find in page
Chapter 10. Programming with the MIDI Library
The MIDI Library, libmd.so, provides an API for sending, receiving, and processing musical instrument digital interface (MIDI) messages through the serial interface of Silicon Graphics IRIS Indigo, Indigo2, and Indy workstations.
The MIDI Library features
timed input and output of MIDI data
buffered I/O with user-adjustable buffering time
active sensing and system-exclusive data handling
simultaneous access to MIDI devices from multiple programs
the ability to have multiple input and output streams open concurrently
a correlation to other media streams with unadjusted system time (UST)
sample applications online in /usr/people/4Dgifts/examples/dmedia/midi
Hands-on experiences are presented throughout this chapter:
In this chapter:
This section describes system configurations for MIDI development and the MIDI input and output interfaces.
Configuring Your System for MIDI Development
The most essential peripheral for MIDI development is a serial-to-MIDI converter. You can use any Apple Macintosh® compatible serial-to-MIDI converter. Many MIDI converters include additional features, including SMPTE-to-MIDI conversion (which is useful for synchronizing MIDI to tape and film), and built-in MIDI patchbays for switching between multiple MIDI inputs and outputs. You should choose your serial-to-MIDI converter based on the functionality you expect your users to require.
Once you have selected a MIDI converter, you will need some MIDI devices to attach to it. The kind and number of MIDI devices you choose to create your MIDI network depends largely on the scope of the application you are writing and your budget. A single keyboard synthesizer may be sufficient for your needs if you are writing a very simple sequencer, but for more complex programs, you should have a keyboard, several rack-mount synthesizer modules, an alternate MIDI controller (such as a wind controller), and a mixer with enough channels for all the instruments you have (so you can hear the results).
If you are not satisfied with listening through headphones or through your workstation's internal speaker, you should probably invest in an amplifier and/or a pair of powered speakers. A multi-track tape recorder may also be useful for testing your application if it will ultimately be used for recording music. You should plan on testing your application using the kind of equipment that you anticipate the end users of your application to have.
Figure 10-1 shows one possible MIDI setup.
Connecting Devices to MIDI I/O Interfaces
The MIDI Library is currently supported on the Indigo, Indigo2, and Indy workstations. The two serial ports on your workstation are configured for MIDI from the Port Setup tool, as described in “Configuring Serial Ports for MIDI WIth the Port Setup Tool”.
Any Apple Macintosh-compatible serial-to-MIDI interface operates when connected to either or both of the serial ports. Many of these interfaces offer additional useful features, such as SMPTE time-code conversion and integrated software-configurable MIDI patching.
Figure 10-2 shows the serial ports on the back panel of the Indigo workstation.
 | Note: Do not use the keyboard port for MIDI.
|
Figure 10-3 shows the serial ports on the back panel of the Indigo2 workstation.
Figure 10-4 shows the serial ports on the back panel of the Indy workstation.
Configuring Serial Ports for MIDI WIth the Port Setup Tool
Before you can run a MIDI application, you must first configure your workstation's serial ports for MIDI by using the Port Setup tool. The Port Setup tool provides a GUI for configuring connections to your system's serial ports. Once set up, a serial port remains configured for MIDI, even if the system reboots, until you reset it from the Port Setup tool.
To configure a serial port for MIDI:
Open the System Manager and select the System Administration tools from the Tools menu.
Click the Port Setup icon, shown in Figure 10-5.
Figure 10-6 shows the Port Setup tool.
Select the available serial port by clicking its icon.
Click Connect.
Figure 10-7 shows the device connections available from the Port Setup tool.
Select the MIDI device by clicking its icon.
Click Set Up.
The system displays the MIDI Port Configuration menu shown in Figure 10-8, asking you to confirm whether you want to start MIDI.
Click OK to start MIDI on the selected port.
This section discusses fundamental MIDI concepts and describes the primary data structures used by the MIDI Library.
MIDI is a control protocol, as opposed to a data protocol, meaning that a MIDI network does not carry audio signals, but rather instructions that tell MIDI instruments how to behave. MIDI information is transmitted in the form of an event: a control instruction (or message), combined with time information (called a timestamp). Typical messages are note on and note off (describing the beginning and ending time of a certain musical note) and values for continuous controllers such as sustain pedals, modulation wheels, and pitch bend controllers.
Initializing MIDI Library Programs
Before calling any MD Library routines, you must initialize the MIDI Library by calling mdInit(), which returns the number of available MIDI ports.
If the MIDI daemon is not running, mdInit() returns -1. See “Configuring Serial Ports for MIDI WIth the Port Setup Tool” for instructions on configuring the serial ports and starting and stopping the MIDI daemon.
Compiling and Linking MIDI Library Programs
To compile a MIDI Library program, enter:
cc –g MLsample.c -o MLsample –lmd
You should also link with any other libraries, such as the Audio Library, that your application uses.
MIDI Library Error Handling
All libmd functions return -1 on error, and set oserror(3C) appropriately.
MIDI Library Programming Model
The MIDI Library has two basic data structures:
| MDport | | An opaque structure containing information about the state of MIDI data and timing, as well as the state of all options for the port.
| | MDevent | | A public structure containing fields for regular and system-exclusive MIDI messages, timestamps, and message lengths.
|
Opening and Closing MIDI Ports
The MDport, or MIDI port, is the basic MIDI I/O structure in the MIDI Library. An MDport provides a one-way (input-only or output-only) connection to a MIDI device. Each port can transmit or receive on as many as 32 independent MIDI channels, 16 per serial port. The use of separate MIDI channels allows complex orchestration of MIDI instruments, when each MIDI device is “tuned” to a different channel.
Getting the Name of an Available MIDI Port
Once the MIDI Library has been initialized by calling mdInit(), you can get the name of an available port by calling mdGetName():
char *mdGetName(int portno)
|
Then you can build a menu of available MIDI ports for your application. mdGetName() returns the name string associated with portno or NULL, if portno does not refer to an existing port.
For example, if MIDI has been initialized with the following command:
startmidi -n ttyd2 -d /dev/ttyd2
|
mdInit() returns 1, and mdGetName(0) returns the "ttyd2" string.
Opening and Closing MIDI Input and Output Ports
The MIDI Library has task-specific calls for opening MDports: mdOpenInPort() opens an input port and mdOpenOutPort() opens an output port. Their function prototypes are:
MDport mdOpenInPort(char *name)
MDport mdOpenOutPort(char *name)
|
Each returns a handle to the appropriate type of port.
Use the name returned by mdGetName() to indicate a particular MIDI device to which a port is to be connected, as demonstrated in Example 10-1.
Example 10-1. Opening MIDI Input and Output Ports
#include "dmedia/md.h"
main()
{
int nports, x;
MDport inport, outport;
…
nports = mdInit();
printf("%d devices available\n", nports);
inport = mdOpenInPort(0);
if (inport == NULL)
printf("open failed\n");
outport = mdOpenOutPort(0);
if (outport == NULL)
printf("open failed\n");
…
}
|
The initial state of a newly-opened port is undefined, except for the timestamping mode, which is MD_DELTASTAMP.
You can open up to 64 MDports, less the number of devices. For example, if you have both serial ports configured for MIDI, you can open 62 MDports. When a port is no longer needed, call mdClosePort() to close the port and free its associated resources.
This section explains how to implement the most basic tasks for MIDI applications: sending and receiving MIDI messages.
Hands-On MIDI Output Experience
To begin, try playing a sound through the MIDI equipment connected to your system. The sample application plays a sound by sending a MIDI message. To test whether you can send MIDI output, try this:
If you have not already done so, connect your MIDI equipment to your workstation's serial port, and configure the port for MIDI, as described in “Connecting Devices to MIDI I/O Interfaces”.
Click on scale
to launch the scale program, which sends a musical scale through the MIDI output.
 | Note: The application will not launch if you don't have your MIDI equipment connected and set up.
|
See scale.c
, in /usr/people/4Dgifts/examples/dmedia/midi/simple to view the code that plays the musical scale.
A tone is played through the MIDI output by specifying the note to play and its duration, and then sending a note on event to sound the tone, followed by another event to end the sound (often, a note on with zero velocity is used to silence a note).
Example 10-2 shows the playnote() routine from scale.c, which creates a MIDI event structure (mdEvent) named mdv. The mdv structure contains a 3-byte message, a timestamp, and the message length. The 3-part message consists of the MD_NOTEON event, or'ed to a channel, followed by the note and its velocity. See “About MIDI Events” for a description of the mdEvent structure.
The message is given an initial timestamp of 0. After the specified time interval has elapsed, the note's velocity is set to zero and then a zero velocity note on message is sent to silence the output.
Example 10-2. Sending a MIDI Message
#include "dmedia/midi.h"
…
playnote ( MDport port, char note, unsigned long long time,
char channel, char velocity )
{
mdEvent mdv;
mdv.msg[0] = MD_NOTEON | (channel & 0xf);
mdv.msg[1] = note;
mdv.msg[2] = velocity;
mdv.stamp = 0;
mdv.msglen = 3;
if (mdSend(port, &mdv, 1) < 0) {
exit(-1);
}
mdv.stamp = time;
mdv.msg[2] = 0;
if (mdSend(port, &mdv, 1) < 0) {
exit(-1);
}
}
|
MIDI event structures are described in “About MIDI Events”.
MIDI events are contained in the MIDI Library mdEvent data structure:
typedef struct __mdevent {
char msg[4]; /* channel message data */
char *sysexmsg; /* sysex message data */
unsigned long long stamp; /* time stamp in nanosecs */
int msglen; /* length of data, sysex only */
} mdEvent;
|
| msg | | is an array of characters representing the data of a non-system-exclusive message, which can include status, note, and controller information and is from 1 to 3 bytes in length.
| | sysexmsg | | is a pointer to a string of characters representing a block of system-exclusive (SYSEX) data, which can include bulk data such as instrument patch configuration parameters and can be of arbitrary length. When SYSEX data is received, msg[0] is set to MD_SysEx (0xf0), and then the actual data storage is allocated with mdMalloc(); similarly, it must be released with mdFree().
| | stamp | | is the timestamp of the event, in nanoseconds or in ticks, if you are using one of the tick modes.
| | msglen | | is used by system-exclusive messages to indicate the length of the SYSEX packet, or when sending multiple messages in a single event. For single events, msglen should be set to 0.
|
The timestamp for the MIDI event, which is the time at which the event did or should occur, is reckoned from either a fixed time or the previous event's time. The MIDI Library supports two types of timestamping:
relative
stamping, in which time is reckoned for all events as an interval from an initial specified time. This is useful for sequencers.
delta stamping, in which time for each individual event is reckoned as the interval since the last event occurred. This is useful for insertions of events in lists.
See “Controlling MIDI Timing” for information about setting the timestamping mode and other parameters.
Sending and Receiving MIDI Events
This section explains how to send and receive MIDI events.
To send a MIDI event from a MIDI output port, call mdSend(). Its function prototype is:
int mdSend(MDport port, MDevent *buf, int count)
|
Depending on the port's timestamping mode, MIDI events have either relative or delta timestamps, or no timestamps at all. If temporal buffering is used, mdSend() blocks, waiting for the output to catch up, until the amount of time represented by the sum of the timestamps in the event buffer exceeds the timeout value set by mdSetTemporalBuffering().
If no errors occur, mdSend() returns the number of messages successfully sent; otherwise, it returns either 0, indicating that an error occurred and no messages were sent, or -1 times the number of messages not sent. mdSend() sleeps if the output queue size limit is exceeded.
To receive a MIDI event into a MIDI input port, call mdReceive(). Its function prototype is:
int mdReceive(MDport port, MDevent *buf, int count)
|
mdReceive() allocates storage for messages coming into the designated port, except for system-exclusive messages; these require the application to allocate and free the necessary storage. mdReceive() copies the message(s) and timestamp(s) into a buffer, and returns either the number of messages read from the given port or -1, if an error occurred.
Handling System-Exclusive MIDI Events
SYSEX messages are received in chunks of up to 1 kilobyte. To check for SYSEX messages, scan for the EOX marker in buf.sysexmsg[buf.msglen-1]. When receiving sysex messages, buf.msg[0] is set to 0xf0 for each chunk received, while the actual data is stored in buf.sysexmsg.
To print the messages in the MIDI event buffer, you must first convert them to a human-readable format by calling mdPrintEvent(). Its function prototype is:
int mdPrintEvent(char *buf, mdEvent *evbuf, int count)
|
| buf | | is a pointer to buffer allocated by the application. It should be large enough to contain the formatted representation of all the events in evbuf. Eighty bytes per message is sufficient.
| | evbuf | | is a pointer to the event buffer
| | count | | is the number of events to format
|
The message format is:
timestamp : channel : status type (string) : byte 1 : byte 2
|
If the message is a note on or note off, then the note name (for example, A3) is printed in the field occupied by byte 1; otherwise, the numeric value is printed.
Processing MIDI Event Messages
A MIDI message is an array of 2 or 3 bytes. The first byte contains the status in the high nibble, and the channel in the low nibble. The remaining 1 or 2 bytes contain the values.
Setting and Getting MIDI Message Status
The status byte determines the type of message and its length. Table 10-1 lists the name, length, and purpose of each status byte.
Table 10-1. MIDI Message Status Bytes
Status
| Lengt
h
| Byte 1
| Byte 2
|
|---|
MD_CHANNELMODESELECT
| 2
|
|
| MD_CHANNELPRESSURE
| 2
|
|
| MD_CONTROLCHANGE
| 2
| Controller number
| Controller value
| MD_NOTEOFF
| 3
|
|
| MD_NOTEON
| 3
| Note number
| Velocity
| MD_PITCHBENDCHANGE
| 3
| MSB
| LSB
| MD_POLYKEYPRESSURE
| 3
|
|
| MD_PROGRAMCHANGE
| 2
| Program number
| Unused
|
To set the status, call mdSetStatus(); to get the status, call mdGetStatus().
Setting and Getting MIDI Message Channel
The channel is the low nibble of the high byte. It takes a value of 0 through 15, which corresponds to a MIDI channel range of 1 through 16.
The functions for setting and getting the channel are:
int mdGetChannel(char *msg)
void mdSetChannel(char *msg, int x)
|
There are sixteen channels for each serial port (device 0 and device 1). Channels 0–15 are sent on device 0, and channels 16–31 are sent on device 1.
Setting and Getting MIDI Message Value
Byte1 and Byte2 are the values associated with the message. Message-specific information (for example, note names for note on and note off messages and channel numbers for channel messages), is contained in these bytes. The functions that set and get MIDI message values are:
void mdSetByte1(char *msg, int x)
void mdSetByte2(char *msg, int x)
int mdGetByte1(char *msg)
int mdGetByte2(char *msg)
|
See the following references for MIDI message codes:
MIDI 1.0 Detailed Specification and Standard MIDI Files 1.0, International MIDI Association, 5316 W. 57th St., Los Angeles, CA 90056.
MIDI Sequencing in C, by Jim Conger, ISBN 1-55851-045-1, M & T Books, 1989. Available from:
M&T Books
A Division of M&T Publishing, Inc.
501 Galveston Drive
Redwood City, CA 94063
Multiplexing MIDI I/O with File Descriptors
You can multiplex MIDI input and output by using the IRIX select(2) system call to wait on MD file descriptors. Using this technique allows your workstation to function as a MIDI thru box.
Hands-On Multiplexed MIDI I/O Experience
To send and receive MIDI messages through your workstation:
If you have not already done so, connect your MIDI equipment to your workstation's serial port, and configure the port for MIDI, as described in “Connecting Devices to MIDI I/O Interfaces”.
Click on thru
to launch the thru sample program, which receives MIDI events and in turn sends them out through an mdOutport.
 | Note: The application will not launch if you don't have your MIDI equipment connected and set up.
|
See thru.c
, in /usr/people/4Dgifts/examples/dmedia/midi/simple to view the code that implements MIDI-thru capability.
Getting a File Descriptor for a MIDI Port
File descriptors can be used with the IRIX select(2) or poll(2) system calls to multiplex input and output of MIDI messages with other I/O devices. To get a file descriptor for an MDport, call mdGetFd(), which returns a file descriptor associated with the port:
Example 10-3 is an excerpt from thru.c that demonstrates putting the file descriptor returned by mdGetFd() into a file descriptor set and using select to wait on the file descriptor set. Using select requires including the
sys/select.h header file.
Example 10-3. Using MIDI File Descriptors
#include "dmedia/md.h"
#include "sys/select.h"
main()
{
int nports, x;
…
fd_set inports, outports;
int nfds, highfd;
nports = mdInit();
…
FD_SET(mdGetFd(inport),&inports);
FD_SET(mdGetFd(outport),&outports);
highfd = mdGetFd(outport) + 1;
while(1) {
nfds = select(highfd,&inports,0,0,0);
…
}
}
|
The MIDI Library provides for timed input and output of MIDI data. Messages are timestamped on input and are scheduled for output on the basis of their timestamp. Output scheduling can be disabled.
You can synchronize I/O for MIDI streams with other media streams by correlating timestamps in terms of unadjusted system time (UST).
Controlling MIDI Timing Mode
You can choose to time MIDI events using either actual time or musical beats, also called ticks. The default mode of an MDport is delta timestamping, which measures the time elapsed from the previous event, but you can reset it to use any mode. To get a port's timestamping mode, call mdGetStampMode(); to set a port's timestamping mode, call mdSetStampMode().
The MIDI Library has three modes in which timing is controlled by actual time:
The MIDI Library has two modes in which timing is controlled by beats:
Time is measured from the time the MDport was opened. You can reset the reference time by calling mdSetOrigin(), which sets the start time to the 64-bit UST that you specify. The result depends on the value used:
| 0 | | Sets the start time to the system's current UST.
| | < 0 | | Sets the start time to the number of nanoseconds before the current UST. This allows streams of files to be restarted in the middle of the data
| | > UST | | Sets the start time to some time in the future.
|
Upon successful completion, mdSetOrigin returns 0; otherwise it returns –1 and sets an error code that you can retrieve with oserror(3C). To get the start time, call mdGetOrigin().
Setting the reference time to match a UST value is useful in MIDI recording, for setting the recording start time to correspond to the arrival of an audio signal at the input jacks. See “Hands-On MIDI and Audio Synchronization Experience” for a demonstration of this technique.
You can vary the tempo for ports whose timestamping mode is in ticks. Tempo is expressed in microseconds per beat, as in Standard MIDI Files (SMF). Tempo and division values specify the conversion from MIDI clock ticks to real time values for the MIDI driver. Divisions represent the subsamples per beat, or pulses per quarter note (PPQ). This is useful for MIDI sequencing.
To set the tempo, call mdSetTempo(); to get the tempo, call mdGetTempo().
To set the divisions per beat (pulses per quarter note), call mdSetDivision(); to get the divisions per beat, call mdGetDivision().
To convert a timestamp from ticks to nanoseconds, taking into account the port's tempo, call mdTicksToNanos(). Similarly, to convert from nanoseconds to ticks, call mdNanosToTicks().
Sometimes you need to adjust the tempo when audio is not synchronized or to compensate for a slow tape deck when recording MIDI. You can specify a tempo scale factor by calling mdSettemposcale(). When messages whose timestamps are expressed in ticks are written to a port that has a tempo scale factor, the timestamps are multiplied by the scale factor before being queued for output. This allows for nondestructive tempo matching.
Controlling MIDI Output Buffering
The MIDI driver buffers data according to time. An application that responds to user interaction must compensate for the fact that events can be sent to the MIDI port faster than they can actually be transmitted. By default, the MIDI library does not allow a process to get more than 2 seconds ahead of the actual output.
You can control how much playback can get ahead of data transmission by specifying the amount of temporal buffering. To set the number of milliseconds an application can get ahead of its output, that is, the time to drain an MDport, call mdSetTemporalBuffering().
When the event timestamps exceed the specified timeout value, mdSend() sleeps until the output catches up.
To determine the amount of time an application can get ahead of its output, call mdGetTemporalBuffering().
You can pause output momentarily or even completely silence output when necessary.
To stop pending output on a port and return the UST value or the tick of the last message sent to or from a port, call mdPause(). mdPause() immediately halts output.
Sometimes you may need to do more than simply pause the output. You can send a panic event for a given port by calling mdPanic(), which sends an all notes off message and a reset controllers message on each channel.
Hands-On MIDI File Player Experience
The MIDI file player sample application illustrates the use of the timing concepts presented in this section—it lets the user change the tempo of a MIDI file, pause playback, or drag a slider to start playback at a random location.
To play MIDI files using the MIDI file player:
If you have not already done so, connect your MIDI equipment to your workstation's serial port, and configure the port for MIDI, as described in “Connecting Devices to MIDI I/O Interfaces”.
Click on Mfp
Enter Mfp to launch the MIDI file player.
 | Note: The application will not launch if you don't have your MIDI equipment connected and set up.
|
See player.c++
, in /usr/people/4Dgifts/examples/dmedia/midi/mfp to view the playback source code.
The MIDI file player application uses three threads: one to manage the user interface (UI), one to manage the playback, and one to update the song position. These threads use shared memory; semaphores are used to protect critical regions so that both processes don't try to access the same data simultaneously.
The playback thread waits on the semaphore. Processes that are waiting on a semaphore are queued on a first-come, first-served basis. When the UI process acquires the semaphore, playback stops; when it releases the semaphore, playback starts. An important point to note is that the playback loop uses uscpsema(), which tests the semaphore and returns immediately if it can't be acquired. This provides an opportunity where it is known to be safe to pause the output. Without doing this, it is possible to send the pause command without having the playback thread acknowledge it, because it is busy sending data.
Another interesting point to note is that error checking is performed to determine whether the application is sending more data than can be handled; if so, the playback thread releases the semaphore and polls the MIDI port until enough data has drained to allow more data to be sent.
The song position thread loops until either the stop button is hit or the song finishes. If a sequence is paused, the position resets to 0, so you need to save the starting position, then add it back in when resuming playback.
The MIDI file player uses non-blocking I/O so that stopping a sequence is possible without flushing currently queued data. If mdSend() is waiting for either room or time to send data, that data is sent as soon as the currently waiting data is flushed.
When setting the division and tempo, the division must always be set first, because the constants set in the driver by the tempo change, depending on the division.
When setting the origin time, putting the position of the file in a signed quantity avoids a compiler warning. Multiplying the start time by -1 allows the file to start playing in the middle without waiting for the first timestamp to expire.
When pausing playback, keep track of the timestamp of the MIDI message that was most recently sent and add it to the previous pause time, so that playback will resume from the proper location in the file.
Synchronizing MIDI I/O with Other Media
You can synchronize I/O for MIDI streams with other media streams by correlating timestamps in terms of unadjusted system time (UST).
One technique is to set the MIDI port to use relative timestamping, and then use mdSetOrigin() to set the port's origin time to match the UST of the media stream to which you want to synchronize.
Alternatively, you can obtain the UST for a MIDI event and compare it to the UST of another media stream counter. To return the UST or tick of the last event sent out, call mdTell().
Hands-On MIDI and Audio Synchronization Experience
To try a synchronized audio and MIDI application:
If you have not already done so, connect your MIDI equipment to your workstation's serial port, and configure the port for MIDI, as described in “Connecting Devices to MIDI I/O Interfaces”.
Click on syncrecord
to launch a sample application that demonstrates synchronized audio and MIDI recording.
 | Note: The application will not launch if you don't have your MIDI equipment connected and set up.
|
See recordmidi.c++
and playmidi.c++
, in /usr/people/4Dgifts/examples/dmedia/midi/syncrecord to view the code that implements the synchronized MIDI record and play application.
IRIS Digital Media Programming Guide
(document number: 007-1799-040 / published: 1994-11-14)
table of contents | additional info | download
Front Matter
About This Guide
Part I. Digital Media Programming
Part II. Digital Audio and MIDI Programming
Part III. Video Programming
Part IV. IndigoVideo Programming
Part V. Compression Programming
Part VI. Movie Programming
Appendix A. Audio Specifications
Appendix B. Aware Scalable Audio Compression Software
Glossary
Chapter 33.
Glossary
Chapter 34.
Glossary
Index
home/search |
what's new |
help
|