Ben Denckla
MIT Media Lab
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.
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.
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.
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.
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_msg
s 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 ioref
s 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_msg
s: 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)
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.
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
a
scheduled 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.)
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 dat2
respectively, 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
.)