Report on the Rogus McBogus MIDI Application Framework

Ben Denckla
MIT Media Lab

Acknowledgements

Rogus McBogus was written primarily by Ben Denckla, with lots of help from Patrick Pelletier, Pete Rice, and Herb Yang. Thanks to Doug Wyatt of Opcode Systems, Inc., and Charlie Steinberg of Steinberg GmbH for answering my questions about OMS and OMS Timing respectively. Thanks to Damon Horowitz for helping me understand loop vs. interrupt time and thanks to Eric Metois for sharing his experience in implementing his HyperCC MIDI library for the SGI.

Pre-Introduction

This document describes version 1.1 alpha of the Rogus McBogus MIDI Application Framework (henceforth referred to as "Rogus"). Rogus is a collection of source code designed to provide high-level MIDI functionality to the MIDI application programmer. It is intended for use on a Macintosh with Metrowerks Code Warrior, OMS version 2.0, and the OMS 2.0 Software Developer's Kit (SDK). OMS 2.0 is currently in beta and the OMS 2.0 SDK is in alpha, but both are publicly available.

Herein are described both the interface and implementation of Rogus, but an effort has been made to separate out these two subjects through the use of sections labeled "Implementation Note."

Here are notes on some pending issues in the project.

Introduction

Rogus is more than a library: it is an application framework. Roughly speaking, this means that it contains main(). This means that some of the flexibility that a library would offer is lost, but what is gained is that the user need only insert a small amount of code into Rogus to get a complete MIDI application. The idea is that it is easier to modify the way Rogus functions as an application (adding menus, etc.), than to write an application from scratch. Or, if you like, think of Rogus as a library that includes a rather general-purpose example application.

The basic idea of Rogus is to provide a high-level interface to OMS. OMS provides an excellent low-level MIDI API, but it is not convenient to write applications at such a low level. Rogus provides a number of high-level abstractions for MIDI programmers. These include an OMS_app object that knows how to sign into and out of OMS properly and maintains the necessary environmental variables that are needed to use OMS. Also included is a MIDI_msg object that has many useful member functions for setting& getting the data in a MIDI message. Rogus includes a simple mechanism for filtering MIDI input so that only messages of the desired type will be passed to the user's input functions.

Perhaps the most important abstraction Rogus provides is its scheduler. OMS includes a scheduler, but it is a bit tricky to use. Rogus' interface to the OMS scheduler allows functions, MIDI messages, and note on/note off pairs to be scheduled in a simple way. Rogus schedules events using a unit of time of half a millisecond (hms) and typically encounters timing errors of no more than 8 milliseconds. In addition, future versions of Rogus will know how to play scores (sequences of MIDI messages). It will be able to play multiple scores at the same time and stop, loop, or tempo-adjust them while playing. Scores will be read in from type 0 or 1 MIDI files.

An important advantage of OMS over other MIDI drivers is its expanded address space. In OMS, MIDI messages are identified not only by their channel but also by their OMS node. This expanded address space can be taken advantage of in conjunction with advanced MIDI routers such as Opcode's Studio 4. Rogus' score player will be able to take advantage of this expanded address space when the input MIDI file has the data intended for separate OMS nodes on separate tracks and has the tracks named to correspond with the appropriate OMS node.

Where do I put my code into Rogus?

If your code only acts upon reception of MIDI input, you should put your code into one or more message handlers as part of a parser. A parser does no parsing in the grammatical sense; it is just a data structure that contains message handlers, one for each of the 8 major message types (enumerated in num MIDI_msg::msgt). Once you set oa.lp (the OMS application's loop-time parser) to your parser and open an input port using oa.open_input(), any MIDI input will be sent to the appropriate message handler in your parser. Any message handlers that you did not set default to NULL which is the parser's way of telling Rogus that it should discard that message type on input. You could use oa.ip (the OMS application's interrupt-time parser) instead, but this is not recommended unless you are absolutely crazy about how quickly your application should respond to input. If you do use the interrupt-time parser, you should do only something that is very simple and quick like echoing input. You can in fact have an interrupt-time and loop-time parser running simultaneously.

If you want some of your code to execute when Rogus starts, rather than in response to MIDI input, a good thing to do would be to call your code in Rogus'Startup() function.

What services does oa provide?

	ioref  open_input( char* );
	ioref  output_refnum( char* );
	void   write( const MIDI_msg *m );
	static long time();

To open a connection to an OMS node, call open_input() on its OMS name or some substring thereof (case doesn't matter). (The easiest way to determine an OMS node's name is to look at your studio setup in the OMS Setup Application.) If no match is found by open_input(), an input is opened to the first OMS input node. Which node is "first" is determined by OMS and can be seen in the listing of nodes in the OMS Setup application. A warning message will appear to inform you that the match failed. If there are no OMS input nodes at all, an error dialog comes up. open_input()returns the ioref of the node it opened an input to. Thisioref can be used to distinguish MIDI_msgs from multiple inputs by their node fields. Note that a single physical MIDI device typically has two OMS nodes associated with it: an input node and an output node. Note also that input and output are reckoned with respect to OMS, so an input node corresponds to the MIDI OUT jack of a device.

To obtain the ioref of an OMS output node, call output_refnum() on its OMS name. output_refnum() has an identical behavior to open_input() (substrings are okay, it is case insensitive, it defaults to first output node, and it signals an error if there are no output nodes). Output iorefs are used as the node field of MIDI_msg objects that are to be sent to that particular node.

write() is used to send MIDI output. It is imperative that the node field of the MIDI_msg passed to it be a valid output ioref. An important consequence of this is that you cannot echo input to output by merely echoing MIDI_msgs: you must change the node field of theMIDI_msg to a valid output node since when it comes in, its node field indicates its source.

time() gives the time since the application signed into OMS. The units are hms (2000 per second).

Implementation Note: OMS also provides time in a variety of formats, including metrical time, but I decided that it would be simpler to provide an absolute time base and let applications determine their notion of metrical time if necessary. The unit of hms comes from the fact that there are 2000 subframes per second in 25 fps SMPTE (80 subframes per frame)

What services do tw and ew provide?

	void ew.e( const char *format, ... );
	void ew.a( const char *format, ... );
	void ew.w( const char *format, ... );
ew.e() posts its printf-style argument(s) in an alert, prepending "Error: ", and causes Rogus to terminate. ew.a() simply posts its printf-style argument(s) in an alert. ew.w() writes its printf-style argument(s) to Rogus' text output window, prepending "Warning: ".

	void tw.pr(  const char *format, ... );
	void tw.spr( const char *format, ... );
tw.pr() prints its printf-style argument(s) to Rogus' text output window. tw.spr() is an interrupt-time safe version of tw.pr().

Implementation Note: tw.spr() is interrupt-time safe because it prints its argument(s) to a buffer which is only dumped to Rogus' text output window at loop time.

What services does sched provide?

	err_ind play_note( const MIDI_msg& on, long dur );
	err_ind insert( const task_data& td );

play_note() plays its MIDI_msg argument immediately (hopefully a note on!) and schedules its corresponding note off, using the note on, velocity zero representation of note off. insert() does what you might guess: it inserts a task into the scheduler. But what, you might ask, is a task. Well, it certainly is not a flask. And it is most definitely not a cask (of Amontillado) or a new way to bask (in the sun). Actually, in the spirit of OOP, you're not really supposed to know what a task is, so I'll just show you its constructors.

	task_data( my_wake_funcp f, void *a, long t );
	task_data( const MIDI_msg&  m, long t );
	task_data( const MIDI_msg& on, long dur, long t );

A task can be a function f with void* argument ascheduled for time t, a MIDI_msg m scheduled for time t, or a note on/note off pair with note on on and duration dur scheduled for time t. The note off will be generated by Rogus, just as it does for playnote(). Rogus has ZOR(TM) (Zero Overhead Rescheduling) for functions. This means that you can reschedule a function very quickly by having it return the number of hms until its next invocation. If you don't want to reschedule the function, just return the OMS constant NEVER.

Implementation Note: ZOR uses relative time for rescheduling: is this good? ZOR also assumes that what needs to be scheduled is this function (with the same argument!), not some arbitrary other function (as in OMS) or some arbitrary task. Is this loss of generality useful? (After all, you can always just schedule the normal way.)

What services does MIDI_msg provide?

	int msgt()  const;
	int smsgt() const;
	int chan()  const;
	int dat14() const;
	int bend()  const;
	int key()   const;
	int vel()   const;
	int len()   const;

	void sprint( char *s ) const;

	bool is_note_end() const;
	bool is_on_for( const MIDI_msg& m ) const;
	bool is_ext() const;

	void set_stat_byt( int msgt, int lsn );
	void set( int msgt, int lsn, int dat1, int dat2, int node );
	void set_sys_ex(            uchar *r, long len );
	void set_meta( int meta_ty, uchar *r, long len );
	void set_bend( float v );
	void set_data14( int v );

msgt() returns the basic MIDI message type as stored in the high nibble of the status byte. The basic MIDI message types appear as enumerated constants within MIDI_msg. smsgt() returns the system message type as stored in the low nibble of the status byte. The MIDI system message types appear as enumerated constants in the smsgt class. Note that smsgt() is only applicable to system messages (i.e. those for which msgt()==system). chan() returns the channel as stored in the low nibble of the status byte. Note that this function is only applicable to channel messages (i.e. those for which msgt()!=system). dat14() returns the interpretation of the data bytes as an unsigned14-bit integer. bend() returns the interpretation of the data bytes as a signed 14-bit integer. key() returns the key number andvel() returns the velocity (applicable to note on or note off messages only). len() returns the length of the entire MIDI message.

sprint() puts a textual representation of the MIDI_msg into the string pointed to by s. is_note_end() tells whether theMIDI_msg is a note end. But what, you may ask, is a note end? Here I seek to introduce an unambiguous vocabulary to deal with the somewhat confusing issue of the representation of notes in MIDI. There are two ways to end a note in MIDI: a note off message with any release velocity or a note on message with a velocity of zero. I introduce the term "note end" to refer to either of these types of messages. As long as we're at it, I'll introduce the term "note on/end pair" to refer to the messages associated with an entire note.is_on_for() tells whether the object and its argument form a note on/end pair. The object itself is assumed to be a note on. is_ext()tells whether the message contains extended data, i.e. whether theext_data pointer ed points to valid data.

set_stat_byt() sets the object's status byte to contain message typemsgt and least significant nibble lsn. For channel messages,the least significant nibble is the channel and for system messages, it is the system message type. set() sets the object's status byte as inset_stat_byt(), sets the data bytes to dat1 and dat2respectively, and sets the OMS node to node. This method sets all of the data necessary to send a non-sysex message out, so it is useful for creating a MIDI_msg from scratch. set_sys_ex() andset_meta() are used to set the extended data in a MIDI_msg.r is a pointer to the raw data, len is the length of this data, and for set_meta(), meta_ty is the type of metamessage. Meta message types are enumerated in the class meta.Implementation Note: extended data is implemented by stealing an undefined system message type (see smsgt::ext) to flag that an extended message is pointed to by ed.

set_bend() sets the object's data bytes as if they were for a pitch bend, but allows the amount of bend to be specified as a float in [-1.0 ...1.0] which may be more convenient than as an integer. This function will never produce the maximum negative pitch bend, 0x0000 (interpreted as -8192) since it scales symmetrically about zero and the maximum positive pitch bend is only 0xF7F7 (interpreted as 8191). Note that this function only manipulates the two data bytes, i.e. if the status byte doesn't already indicate a pitch bend it needs to be separately set to do so. set_data14() sets the object's data bytes to contain the 14-bit unsigned integer passed to it.

new_handler()

Implementation Note: There should be a version of bend() that returns a -1...1 float. How to make sure s is big enough in sprint()? (Only a problem for ext_data.)