ON DISK
Programming In MIDI
MIDI PROGRAMMING TIPS
Secrets Of The Inner Circle Revealed!
BY STEVE JOHNS
AT A GLANCE | ||
|
START VOL. 4 NO. 12 / JULY 1990
ON DISKProgramming In MIDIMIDI PROGRAMMING TIPSSecrets Of The Inner Circle Revealed!BY STEVE JOHNS
|
||||||
MIDI programming on the Atari ST and Mega computers may seem like an arcane art, open only to ordained techno-musicological wizards. In fact, with a few exceptions, MIDI programming is no different than any other kind of applications programming. You spend the majority of your effort conceptualizing the program, creating nice displays, handling mouse and keyboard inputs, managing files, dealing with GEM and processing data. In addition, MIDI programmers deal with I/O from the MIDI In and MIDI Out ports.
To get you started, I will review the system calls (ROM subroutines) pertaining to MIDI and how MIDI data is captured and stored by the ST. This will include hints on how to avoid the subtle and disastrous system MIDI buffer overrun error. I will not cover basic MIDI commands and protocol. If you need to brush up on these, I suggest reading this issue's The Ins, Outs and Thrus of MIDIor the MIDI 1.0 Detailed Specification (version 4.1) available from the International MIDI Association (see START Bookshelf).
Before going any further, we need to agree on terminology. The system MIDI buffer is a memory region where incoming MIDI data is automatically stored by the operating system. Once placed in this buffer, the data is available to your program. This buffer is limited in size (default is 128 bytes) and can be overrun if too much MIDI data comes in before your program reads it out. Information about this buffer is found in the buffer descriptor structure, located through the IOREC(3) call mentioned below.
An interrupt is a hardware signal generated by a peripheral device--in our case, the ACIA or MIDI chip--which alerts the ST to the occurrence of an event, such as MIDI data arrival, requiring some action on its part.
When an interrupt occurs, a ROM-based interrupt service routine takes over. The MIDI interrupt's service routine places each incoming MIDI-data byte into the system MIDI buffer.
Six MIDI functions are built into the ST operating system. They are listed here under their common names, found in most C language compilers and ST reference books. Other languages (Hisoft BASIC, Personal Pascal, etc.) allow access to these calls, under these or other names.
BCONSTAT(3): Returns a nonzero value if bytes are available to be read from the system MIDI buffer, zero if the buffer is empty.
BCONIN(3): Returns the value of the next byte in the system MIDI buffer. You will typically call BCONIN(3) after detecting MIDI bytes in the buffer via BCONSTAT(3).
BCONOUT(3, int data): Sends a single byte to the MIDI Out port. Although data is a 16-bit integer variable, only the lower byte is sent.
MIDIWS(int count, char *pointer): Sends multiple bytes to the MIDI Out port.
IOREC(3): Returns a pointer to the system MIDI buffer descriptor. This is a data structure containing four elements describing the system MIDI buffer: a pointer to its start, its length and pointers to its head and tail.
KBDVBASE( ): Returns a pointer to a data structure containing nine system vector pointers, each pointing to an interrupt service routine.
The overall sequence of events is: A MIDI byte arrives at the MIDI In port and the hardware generates an interrupt. TOS acts on the interrupt by running the MIDI interrupt service routine, which places the MIDI byte into the system MIDI buffer. After the MIDI data is stored in the buffer, your MIDI program may access the data, via simple function calls, then use the data in your program.
Of the four system MIDI buffer descriptors returned by the IOREC(3) call, two, start and length, are used by the operating system to define the buffer's starting memory address and length, and two, head and tail, are used by the interrupt service routine to track the incoming MIDI data.
The head pointer flags the next byte of MIDI data to be read. It increments each time your program reads a byte. The tail pointer flags the last byte of MIDI data placed in the buffer. It increments before each incoming byte is placed in the system MIDI buffer. When the tail pointer reaches the end of the system MIDI buffer, it wraps around and writes data at the beginning of the buffer. The same with the head pointer. When the last byte of data is read from the end of the buffer, the head pointer starts reading from the beginning of the buffer. This behavior makes the system MIDI buffer a circular buffer.
Suppose, however, that the tail pointer is writing data twice as fast as the head pointer is reading data. Eventually the tail pointer catches up with the head pointer and the two are equal. Unfortunately, no data can be written at the tail pointer's location; to do so would overwrite MIDI data already in the buffer. This is the dreaded overrun condition--MIDI data coming in is lost because there is no room to store it.
This problem tends to rear its ugly head during a large system-exclusive dump where bytes are sent in a burst. Even if your program runs a tight loop to read data from the system MIDI buffer, a fast and furious dump can cause the tail pointer to circle around and bite the head pointer. The result is lost MIDI data. The system only needs to get 128 bytes ahead of your program to cause this disaster.
The solution is for you, the programmer, to define your own MIDI buffer to replace the default system buffer. You can declare this user MIDI buffer to be any size within the memory allocation bounds of your language. Make it big enough to accommodate the largest dump of MIDI data that your program will be required to handle. For example, if you are writing a synthesizer patch librarian which sends 10K blocks of MIDI data, define your user MIDI buffer to be just over 10K. Done properly, the MIDI interrupt service routine will automatically place incoming MIDI data in your enlarged buffer and you will avoid the overrun error.
SEEMIDI.C is a short example in C that captures and displays up to 32K of MIDI data. The overall logic is:
1) Save the system MIDI buffer information so we can restore it upon exit.
2) Set aside memory for a user MIDI buffer.
3) Clear the screen and prompt the user.
4) Enter a loop to read in MIDI data; immediately output each byte to the screen in hex.
5) Clear the screen when the user presses [c], or exit the loop when the user presses [Esc].
6) Restore the system MIDI buffer, restore the memory you used, and exit.
Although this simple program just displays incoming MIDI data, it is a fairly simple matter to add file capability in order to save the data to disk. Another enhancement is to store the incoming MIDI data in an array and implement scrolling so that any part of the data could be viewed at will. Yet a third possibility is to alter the MIDI data and pass it through to the ST's MIDI Out port. In fact, it only takes a few lines of C code to implement a MIDI-thru. Replace the display code inside the escape key loop with the following:
if(Bconstnt(3))){ midi=Bconin(3); /* Put your processing code here, if desired */ Bconout(3,midi); }
This is a high level MIDI-thru since it is written in C and involves function calls. Although it works fine and it is convenient to write processing code in C, it has two disadvantages. First, the function calls, which involve the stack, make it slow. Second, you must be constantly polling for incoming MIDI data.
Suppose you just wanted a MIDI-thru function that worked fast in the background while your program was busy doing other things. This calls for a low-level MIDI-thru.
A low-level MIDI-thru function involves replacing the built-in MIDI interrupt service routine with one of your own. SEEMIDI.C also shows an example of low-level MIDI-thru, written in assembler. Note the actual thru-function code only takes four instructions; the rest of the routine duplicates the action of the system MIDI interrupt service routine. By replacing the ST's routine with your own, you are not trapped in a polling loop and MIDI-thru takes place totally in the background.
Although interrupt-handling code can be complex, the technique for doing the swap (also called stealing the vector) is simple. The functions THRU_ON( ) and THRU_OFF( ) are used to swap your routine in and out respectively. The state of the variable THRUFLAG indicates when your function is in control. One important note: Always restore any swapped system vectors before exiting your program or subsequent programs will not find the interrupt service routines in place when they need them.
Well, that's it. You now possess the two (count 'em) secrets of MIDI programming on the ST: replacing the system MIDI buffer and swapping in a custom MIDI interrupt service routine. Everything else depends on your knowledge of MIDI specifications, your creativity as a programmer and perhaps a few tricks (some would say trade secrets) pertaining to time stamping, which will have to be covered another time. Congratulations, and welcome to the secret society!
Steve Johns is an electrical engineer and the founder of Johnsware, a MIDI software development company. He resides in Hyattsville, MD with his wife Erica, whose loving patience makes his fine GEM MIDI programs possible. He can be reached on GEnie as S.JOHNS or on CompuServe through the Johnsware category under the Worldmusic (MIDI) forum.