Music U can C However implausible it might sound, writing your own MIDI software is the best way to compose exciting music. Danny McAleer sets Cubase aside in favour of a C compiler... The Atari doubtless owes its success as a musician-friendly computer to its built-in MIDI ports. Whether their inclusion was a carefully conceived plan, or (more likely) a happy accident caused by over-zealous designers hell-bent on adding as many peripheral ports as space permitted, matters not. What is important is the wealth of musical potential this interface offers, and how easy it is to capitalise on it. Like most things, there is still a small learning curve to climb in order to understand both the fundamental principles of Atari programming, and the MIDI communications protocol, but these aren't nearly as difficult as they sound. Once you have grasped the basics anything is possible, from simple utilities to fully-featured sequencers, and you'll never have to settle for using the available compositional tools ever again. Reading data from the MIDI port is insanely simple, because their commands are built into the Atari's BIOS (Basic Input Output System) they can be handled directly from a high-level programming language like C. To read data from a device, there's a function called Bconin(short device). Bconin() returns a long integer with the data read from the device. Although MIDI is 8-bit and so only uses the lower eight bits of this, the other bits are important for other devices (the keyboard for example, uses the two upper bits for non-ASCII key states such as [Shift] or [Control]). Its single argument specifies the device to be read, where 3 is the MIDI port, 0 and 1 for the parallel and serial ports respectively, and 2 for the keyboard. When calling the Bconin() function, you must be certain that there's something waiting to be read, else the computer happily twiddles its thumbs until infinity (or until something does arrive) before relinquishing control back to the operating system. Since this is rather undesirable behaviour, the function Bconstat(short device) should always be used first. This function polls the device number (as above) passed in its argument, returning a long integer value of zero if nothing is there, or -1 if a some information is ready to be read. To write data to a device, there's a function called Bconout(short device, short value). This accepts two arguments: the device number, which is mapped the same as Bconin(), and the value to send. Bconout() returns zero if successful, or any non-zero value otherwise - told you it was easy. After negotiating the Atari's BIOS, there's still the additional obstacle of the MIDI protocol itself, since simply sending anything through the MIDI wires may result in a very unhappy synthesizer (or at the very least, it'll just ignore you). To complicate things further, MIDI messages can be any number of bytes in length, depending on what they're describing, and so it is up to the program to decipher the "header" byte, and read in the required number of subsequent bytes to complete the message. MIDI is a serial communication protocol and so only one byte at a time can be sent, unlike the printer port which is omni-directional and lots of bytes can be moved around in parallel. There are two types of MIDI messages: channel, and system. The former is peculiar to only one channel (or instrument part), whereas the latter does not concern itself with channels (things like system exclusive messages, MIDI timecode and other synchronisation messages, and sample dump packets are examples of these). Channel-specific messages include note information, and other performance controller attributes, like pitch bend, modulation, panning, volume, and so on. The note-on message is perhaps the most important channel message, and this comprises three bytes of information. The first byte contains the message ID in the upper four bits, and the channel (0 to 15) in the lower four bits: ** tab these ** Hexadecimal: 9 0 Binary: 1001 0000 ** /tabs ** To find the channel in a note-on message, the byte is logically ANDed with $0f (binary 1111). If you're only looking for a note-on, regardless of channel, you can set a conditional operator: ** np on ** if(message >= 0x90 && message <= 0x9f)/* then process the MIDI note... */ ** /np ** The second byte contains the note number itself, and this ranges from 0 to 127, where 60 is equal to middle C. The note's velocity, which is how hard the note is struck and not necessarily associated with the volume (since it can also be used to open filters, and all sorts of other things on modern synthesizers), is stored in the final byte. Therefore, if middle C is played firmly (velocity 100) on channel ten, then the bytes $99, $3C, $64 are sent (in that order). The first example (MIDIMON.C) is a simple program that reads incoming MIDI data and provides information about what sort of message it is. This should help clarify a few things - even though the interface is barely functional (to be polite about it); in particular, how the MIDI ports are read, and how to decipher valid MIDI messages. The second example (MINISEQ.C) illustrates how to send and receive MIDI note information (as well as other controllers), in the form of a fairly modest sequencer. The sequencer basically comprises three sections: note receive, note send, and the user interface gubbins. To check for a note-on, the code uses the BIOS commands, Bconstat(3) and Bconin(3): ** np on ** BYTE m, velocity, note; BYTE channel = 0; /* MIDI channel 1*/ if(Bconstat(3)!=0) { m = Bconin(3); if(m >=0x90 + channel { note = Bconin(3); velocity = Bconin(3); } } ** /np ** In the above example it is quite safe to call Bconin(3) since we know that a message is waiting, and after checking its header for a valid note-on message, that another two bytes are expected, and that these will be the note number and its velocity respectively. These can then be stored in a variable (or array) ready to replay using the note send part of the code: ** np ** void note_on(BYTE channel, BYTE note, BYTE vel) { Bconout(3, 0x90+channel); Bconout(3, note); Bconout(3, vel); } ** /np ** With this function, all you need do is to pass it the note number and velocity and the channel to send a note-on, and it take cares of the details, transmitting it in the right order, and without any spurious interruptions. Of course, once a note-on has been sent, it must inevitably be followed by a note-off message, otherwise the synthesizer will drone on forever. Note-offs can either be sent as a note-on with a velocity of zero, or using the note-off specific message: $80 + channel. Although two bytes to describe the note's number and velocity are sent the same as a note-on, the velocity in this case is how quickly the note is depressed - this is used to good effect on some synthesizers. There is a sensible reason for having two methods of sending a note off message: running status. This is a system where if the same type of message is repeatedly sent, then it removes the header byte in subsequent packets, thus effectively reducing the number of bytes needed to send data. For example, if a note-on is played and then released, and if the keyboard supports active sensing, and sends note-offs as note-ons with zero velocity, then the following shortened message is sent: $90, $3C, $64, $3C, $00. This also works for other messages such as pitch bend and modulation, where lots of continuous messages are needed. Naturally, the note-off shouldn't be sent immediately after the note-on, else all we'll get are little blips instead of long notes, so the program sends the note-off, when the next note stored in the sequencer is about to be played (one sixteenth-note later). The timing is calculated using the system clock using this formula: ticks = ((200/tempo)/60)/4 200 is the clock speed in Hertz (this is fixed), tempo is the value in beats per minute, 60 is the number of seconds in a minute (obviously), and 4 is the division of beats into sixteenth notes. Should you wish to vary the step rate into other measures, the last value can be changed to two for eighth notes, eight for thirty-second notes, and so on. Once note data has been read from the MIDI input, and stored in an array (the example program has an eight note capacity, but this can quite easily be extended), it is ludicrously simple to start creatively manipulating it. As in the example sequencer program, you could make it play back the notes in any order that the user chooses, increase or decrease the rate at which it plays through the notes, or even non-destructively transpose the sequence (by adding or subtracting a user-defined amount to the value sent to the functions to send note-ons and offs). You could also manipulate the velocity, or even add performance controllers such as patch change, panning or volume. To send a program change, only two bytes are needed: the first is $Cn, where n is the channel number ($0 to $f); the second is the patch number (0 - 127). Control changes need three bytes, and the most common are detailed in the boxout. For each of the MIDI messages that the mini-sequencer supports, there is a separate function to make things easier. Most of the other code in the example concerns itself with the user interface (redrawing parameters and the menu, moving the cursor, and checking for keyboard input), but this is all fairly basic C code, and certainly would be covered in the first two days of any breeze-block sized guide to C programming. Perhaps the worst deterrent for doing anything is that it's too hard, especially when no-one's prepared to help. However, if you're determined to write some C programs for creative music writing, and you're stumped, then please do send questions, however silly (I won't laugh - honestly) to me at: ** BC ** electronic_cow@dial.pipex.com ** /BC ** I'll do my best to help. Example C code, project files for Lattice C, executable programs, and GEM versions (just for fun) are included on this month's Reader Disk to try out... ** Boxout ** ** CTRLTBLE.GIF here ** ** /boxout ** ** Images ** ** PRG_MMON.GIF ** PRG_SEQT.GIF ** PRG_SEQG.GIF ** caption ** Example GEM sequencing program ** /caption **