MIDI Capture Program
by Robert Osness
Capture is a C-language program that will allow you to record, in your ST's memory, polyphonic output from your MIDI instrument. The stored data can then be displayed in tabular form and/or played back through your MIDI instrument.
The primary purpose of this program is to show how to get started in MIDI programming on the ST, providing a foundation on which to build. The concepts used in Capture can be readily expanded to add key transposition, tempo modification, note editing, disk storage for data files, and many other features that will come to mind as you explore the possibilities!
Those of you who are not interested in programming may skip ahead to the section titled "Run time!" where the program's operation is described. The rest of you, read on!
Data is transmitted on the MIDI bus in much the same way as the RS-232 serial data used for modem and printer interfaces. It is transmitted in bytes, at a rate of 31.25K baud. Handling I/O data for the MIDI ports is easy, thanks to the BIOS functions Bconin() and Bconout(). For input, we use Bconin(device), where Device 2 is the keyboard, and Device 3 is the MIDI port.
Availability of data at the input can be checked using x = Bconstat(device) which returns 0 if no data is available.
For output, we can use Bconout(device, data), where data is the byte to be transmitted.
Capture uses the concept of time-tagging to save and recreate the time base of the MIDI input data. The incoming data is grouped into command message blocks, and stored with a time tag indicating when it was received. On playback, the command blocks are read and transmitted at intervals determined by the difference in time tag values.
What is a command message block? It's a sequence of one to three bytes of MIDI data that has a common function or purpose. A command block begins with a "status" byte, in which the most significant bit is always a logic one. It may be followed by zero, one or two data bytes, in which the most significant bit is always a logic zero. The most significant four bits (nibble) of the status byte identifies the function of the command message block. The least significant nibble is the MIDI channel number to which the message block belongs.
Table 1 identifies, by command block length and function, the status byte headers. Type 8, 9 and C status words are used for nearly all MIDI bus traffic. Capture is designed to handle all types of message blocks, with one exception: System Exclusive block transmissions. These are usually reserved for transmitting instrument-specific data files such as patch data.
NOTE ON and NOTE OFF Commands
By far the most common command blocks are the NOTE ON and NOTE OFF commands. A NOTE ON command might contain the following hexadecimal byte sequence:
0x92 NOTE ON, Channel 2 0x3C Pitch = Middle C 0x40 Velocity = 0x40
The hexadecimal value 92 identifies a NOTE ON command for Channel 2. Next, hexadecimal data byte 3C is a pitch (frequency) value corresponding to Middle C on the piano. Notes are assigned integer pitch values, increasing with frequency. There are 12 intervals in an octave, so adding or subtracting 12 (decimal) to a sequence of pitch values would raise or lower the pitch by one octave.
The last byte value of 0x40 indicates a turn-on velocity of 64, decimal. Actually, many keyboards don't use the velocity value at all, but simply use a fixed turn-on and turn-off velocity.
Now let's look at the NOTE OFF command. A typical NOTE OFF command block might appear as:
0x81 NOTE OFF, Channel 1 0x3C Pitch = Middle C 0x40 Velocity = 0x40
Here the bytes represent a NOTE OFF status word on Channel 1, a note pitch of 0x3C (Middle C again), and a turn-off velocity of 0x40.
Implicit NOTE OFF commands
A special case of the NOTE ON command allows it to be interpreted as a NOTE OFF command. This is true whenever the velocity value is zero: a zero-velocity NOTE ON command always equals NOTE OFF for the indicated pitch value.
Program change commands
"Program Change" is the MIDI term used for changing instrument voices, such as from piano to vibraphone. A typical PROGRAM CHANGE command block might be:
This can be interpreted as change program on Channel 1 to Voice No. 2 (as defined in the MIDI instrument user's manual). For my Casio MT-540 keyboard, this causes a voice change to vibraphone for subsequent notes.
A modified command format is sometimes used in the MIDI command message structure, allowing the command blocks to be simplified. The general rule is that the status byte need not be transmitted if it is unchanged from the preceding command block. Combining this with the implicit NOTE OFF command allows a significant reduction in the bus traffic, as nearly everything can be transmitted as a NOTE ON command. For example, consider the following sequence:
0×90 NOTE ON, Channel 0 0×3C Pitch = Middle C 0×40 Velocity = 0×40 0×3C IMPLICIT NOTE OFF, Pitch = Middle C, Channel 0 0×00 Velocity = 0 = note off 0×3D NOTE ON, Pitch = C# Channel 0 0×40 Velocity = 0×40 0×3D IMPLICIT NOTE OFF, Pitch = C#, Channel 0 0×00 Velocity = 0 = note off
The code for Capture is shown in Listing 1. The program is designed simply to identify the incoming command message blocks and store them sequentially in an array, along with a value for the current time.
Array elements are organized according to the structure mdat, which has four data slots: three message bytes and a longword for time. The three message byte slots are identified with the abbreviations cm,fr and ve, for command, frequency and velocity. The longword is identified as ti for time. It should be noted that the data contained in fr and ve will actually be frequency (pitch) and velocity only in the case of NOTE ON/OFF commands.
In theory, time tags should have an accuracy of about plus or minus one millisecond, which is approximately the time required to transmit a three-byte command block on the MIDI bus. Although this might be considered excessively conservative, Capture closely approaches this goal by using a millisecond timer and an efficient processing loop.
Timing is accomplished by the ST's 68901 MFP Timer, in conjunction with the assembly-language interrupt subroutine shown in Listing 2. The timer setup is done by the function init__tmr(). (A detailed description of the timer is not possible here, but will be the subject of a future article.) Because the timer is interrupt driven, the assembly-language interrupt subroutine must be separately assembled and linked with the C program code. Listings 3 and 4 provide batch command files that will accomplish this with the Batch and Alycon Compiler/Linker programs supplied with the Atari Developers Kit.
Collecting MIDI data
The main processing loop for Capture begins by clearing the input data array and MIDI buffer, then starts the millisecond timer. Next, a for statement defines the input data loop where MIDI data is collected and processed.
A second for loop waits for input data, which can be either a MIDI byte or a q from the keyboard to signal the end of MIDI input. Once a data byte has been received, it is stored in one of three temporary storage cells: b0, b1 or b2. If it can be identified as a status byte it goes into b0. Otherwise, data bytes are successively stored in b1 and b2, using bnxt to keep track. The program logic is designed to correctly handle input data in either the standard or running status format.
The last step in the input data loop is to call the function end__c(). This function uses a switch statement to evaluate the current status byte, and then uses bnxt to determine whether the complete command message block has been received. If so, the complete command block is stored as an array element, using the function save(). The millisecond timer value is also stored in the array at this time.
The array entries are in the standard MIDI command format, even if the data was received in the running status format. This is desirable to simplify any further processing of the array data that may be undertaken.
Data entry into the array continues until either the array is filled or a q is entered from the ST's keyboard. If the array size of 2,000 entries (about 1,000 notes) is insufficient, the value of NMAX can be increased to any size desired.
Displaying the MIDI data
With the data safely stashed away in memory, it might be nice to see what it looks like. The user is prompted with the message "Display input data? (Y/N)." Typing a y causes the console to display the first ten array entries, with the MIDI bytes displayed in hexadecimal and the time in decimal milliseconds. Striking any key other than q will cause the next ten entries to be displayed. The display loop can be aborted by typing a q at any time.
Next the prompt message "Play it back? (Y/N)" is displayed. Entering a y starts the playback process, complete with any pauses in data entry. The function pbk() simply steps through the array and outputs the MIDI message command blocks, separating them by a time interval equal to the difference in time tags. Though the messages are not transmitted in running status mode, the standard format commands are equally acceptable to most MIDI instruments, and there should be no discernible difference.
Upon completion of the output sequence, the program exits to the desktop. Should it be desirable to save the array permanently, a disk storage option can be readily added at the end of the main program loop.
Building the program
Those who have the program disk for this issue may proceed with a smile to the next section, and run the program. All others, let's get the grungy work over!
Type in the program as shown in Listing 1. Omit the comments at your own risk: They sure make it easier to understand later!
|TABLE 1. MIDI STATUS BYTES|
|STATUS BYTE VALUE||NUMBER OF BYTES/BLOCK||FUNCTION|
|Bn||3||CONTROL CHANGE/CHANNEL MODE|
|En||3||PITCH BEND CHANGE|
|*EXCEPT SYSTEM EXCLUSIVE BLOCK MESSAGES|
|n = CHANNEL NUMBER|
|x = QUALIFIER BITS|
The following explanation assumes the use of the Alcyon Compiler/Linker, with compiler and linker files on a common disk in Drive A. Batch files, if used, should be moved to the compiler/linker disk. The program files are assumed to be on a separate disk in Drive B. If you prefer other arrangements, make adjustments accordingly.
Type in the interrupt subroutine INTR.S, from Listing 2. Assemble it using the batch file BI2.BAT from Listing 3, using the command BI2 B:INTR.
Now compile and link the two code segments using the batch file BINT.BAT shown in Listing 4, and the command BINT B:CAPTURE.
Now for the fun stuff! Hook up the MIDI cables, and double-click on CAPTURE.PRG. When the program prompts you, start playing your MIDI instrument; the data it will send through the MIDI cables will be stored in your ST's memory. (Even if you don't start playing immediately, the timer is going. Any "silence" will be recorded just as if it were musical data.)
When you're through playing, press the q key, and you will be asked whether you wish to view the MIDI data. If you answer "y," the data will be displayed ten bytes at a time. Pressing any key except q will display the next ten bytes.
Once all the data has been displayed, or you press q to abort the data display function, you will be asked if you wish to play back the data. Pressing y at this point will cause the stored MIDI data to be sent back to your instrument, after which the program will return to the desktop.
Keep in mind that your MIDI performance will be reproduced in precise detail, including all your blunders! Good luck, and have fun developing new and improved processing routines for your MIDI files!
Bob Osness, who has been programming his ST for 2½years, works as an electrical engineer for Boeing Aerospace in Kent, Washington. He and his wife, Georgia, spoil grandchildren as a hobby.