<HTML> 
<BODY> 

<H1>Midi Kit design</H1>

<P>The Midi Kit consists of the midi_server and two shared libraries, 
libmidi2.so and libmidi.so. The latter is the "old" pre-R5 Midi Kit and has 
been re-implemented using the facilities from libmidi2, which makes it fully 
compatible with the new kit. This document describes the design and 
implementation of the OpenBeOS midi_server and libmidi2.so.</P>

<P>The midi_server has two jobs: it keeps track of the endpoints that the 
client apps have created, and it publishes endpoints for the devices from 
/dev/midi. (This last task could have been done by any other app, but it was 
just as convenient to make the midi_server do that.) The libmidi2.so library 
also has two jobs: it assists the midi_server with the housekeeping stuff, and 
it allows endpoints to send and receive MIDI events. (That's right, the 
midi_server has nothing to do with the actual MIDI data.)</P>

<HR SIZE="1">

<H2>Ooh, pictures</H2>

<P>The following image shows the center of Midi Kit activity, the midi_server, 
and its data structures:</P>

<BLOCKQUOTE><IMG ALT="" SRC="midi_server.png"></BLOCKQUOTE>

<P>And here is the picture for libmidi2.so:</P>

<BLOCKQUOTE><IMG ALT="" SRC="libmidi2.png"></BLOCKQUOTE>

<P>Note that these diagrams give only a conceptual overview of who is 
responsible for which bits of data. The actual implementation details of the 
kit may differ.</P>

<HR SIZE="1">

<H2>Housekeeping</H2>

<UL>

<LI><P>The design for our implementation of the midi2 "housekeeping" protocol 
roughly follows <A HREF="oldprotocol.html">what Be did</A>, although there are 
some differences. In Be's implementation, the BMidiRosters only have 
BMidiEndpoints for remote endpoints if they are registered. In our 
implementation, the BMidiRosters have BMidiEndpoint objects for <I>all</I> 
endpoints, including remote endpoints that aren't published at all. If there 
are many unpublished endpoints in the system, our approach is less optimal. 
However, it made the implementation of the Midi Kit much easier ;-)</P></LI>

<LI><P>Be's libmidi2.so exports the symbols "midi_debug_level" and 
"midi_dispatcher_priority", both int32's. Our libmidi2 does not use either of 
these. But even though these symbols are not present in the headers, some apps 
may use them nonetheless. That's why our libmidi2 exports those symbols as 
well.</P></LI>

<LI><P>The name of the message fields in Be's implementation of the protocol 
had the "be:" prefix. Our fields have a "midi:" prefix instead. Except for the 
fields in the B_MIDI_EVENT notification messages, because that would break 
compatibility with existing apps.</P></LI>

</UL>

<H3>Initialization</H3>

<UL>

<LI><P>The first time an app uses a midi2 class, the BMidiRoster::MidiRoster() 
method sends an 'Mapp' message to the midi_server, and blocks (on a semaphore). 
This message includes a messenger to the app's BMidiRosterLooper object. The 
server adds the app to its list of registered apps. Then the server 
asynchronously sends back a series of 'mNEW' message notifications for all 
endpoints on the roster, and 'mCON' messages for all existing connections. The 
BMidiRosterLooper creates BMidiEndpoint objects for these endpoints and adds 
them to its local roster; if the app is watching, it also sends out 
corresponding B_MIDI_EVENT notifications. Finally, the midi_server sends an 
'mAPP' message to notify the app that it has been successfully registered. Upon 
receipt, BMidiRoster::MidiRoster() unblocks and returns control to the client 
code. This handshake is the only asynchronous message exchange; all the other 
requests have a synchronous reply.</P></LI> 

<LI><P>If the server detects an error during any of this (incorrect message 
format, delivery failure, etc.) it simply ignores the request and does not try 
to send anything back to the client (which is most likely impossible anyway). 
If the app detects an error (server sends back meaningless info, cannot connect 
to server), it pretends that everything is hunkey dorey. (The API has no way of 
letting the client know that the initialization succeeded.) Next time the app 
tries something, the server either still does not respond, or it ignores the 
request (because this app isn't properly registered). However, if the app does 
not receive the 'mAPP' message, it will not unblock, and remains frozen for all 
eternity.</P></LI>

<LI><P>BMidiRoster's MidiRoster() method creates the one and only BMidiRoster 
instance on the heap the first time it is called. This instance is 
automatically destroyed when the app quits.</P></LI>

</UL>

<H3>Error handling</H3>

<UL>

<LI><P>If some error occurs, then the reply message is only guaranteed to 
contain the "midi:result" field with some non- zero error code. libmidi2 can 
only assume that the reply contains other data on success (i.e. when 
"midi:result" is B_OK).</P></LI>

<LI><P>The timeout for delivering and responding to a message is about 2 
seconds. If the client receives no reply within that time, it assumes the 
request failed. If the server cannot deliver a message within 2 seconds, it 
assumes the client is dead and removes it (and its endpoints) from the roster. 
Of course, these assumptions may be false. If the client wasn't dead and tries 
to send another request to the server, then the server will now ignore it, 
since the client app is no longer registered.</P></LI>

<LI><P>Because we work with timeouts, we must be careful to avoid 
misunderstandings between the midi_server and the client app. Both sides must 
recognize the timeout, so they both can ignore the operation. If, however, the 
server thinks that everything went okay, but the client flags an error, then 
the server and the client will have two different ideas of the current state of 
the roster. Of course, those situations must be avoided.</P></LI>

<LI><P>Although apps register themselves with the midi_server, there is no 
corresponding "unregister" message. The only way the server recognizes that an 
app and its endpoints are no longer available is when it fails to deliver a 
message to that app. In that case, we remove the app and all its endpoints from 
the roster. To do this, the server sends "purge endpoint" messages to itself 
for all of the app's endpoints. This means we don't immediately throw the app 
away, but we schedule that for some time in the future. That makes the whole 
event handling mechanism much cleaner. There is no reply to the purge request. 
(Actually, we <I>do</I> immediately throw away the app_t object, since that 
doesn't really interfere with anything.) (If there are other events pending in 
the queue which also cause notifications, then the server may send multiple 
purge messages for the same endpoints. That's no biggie, because a purge 
message will be ignored if its endpoint no longer exists.)</P></LI>

<LI><P>As mentioned above, the midi_server ignores messages that do not come 
from a registered app, although it does send back an error reply. In the case 
of the "purge endpoint" message, the server makes sure the message was local 
(i.e. sent by the midi_server itself).</P></LI>

<LI><P>Note: BMessage's SendReply() apparently succeeds even if you kill the 
app that the reply is intended for. This is rather strange, and it means that 
you can't test delivery error handling for replies by killing the app. (You 
<I>can</I> kill the app for testing the error handling on notifications, 
however.)</P></LI>

</UL>

<H3>Creating and deleting endpoints</H3>

<UL>

<LI><P>When client code creates a new BMidiLocalProducer or BMidiLocalConsumer 
endpoint, we send an 'Mnew' message to the server. Unlike Be's implementation, 
the "name" field is always present, even if the name is empty. After adding the 
endpoint to the roster, the server sends 'mNEW' notifications to all other 
applications. Upon receipt of this notification, the BMidiRosterLoopers of 
these apps create a new BMidiEndpoint for the endpoint and add it to their 
internal list of endpoints. The app that made the request receives a reply with 
a single "midi:result" field.</P></LI>

<LI><P>When you "new" an endpoint, its refcount is 1, even if the creation 
failed. (For example, if the midi_server does not run.) When you Acquire(), the 
refcount is bumped. When you Release(), it is decremented. When refcount drops 
to 0, the endpoint object "deletes" itself. (So client code should never use an 
endpoint after having Release()'d it, because the object may have just been 
killed.) When creation succeeds, IsValid() returns true and ID() returns a 
valid ID (> 0). Upon failure, IsValid() is false and ID() returns 0.</P></LI>

<LI><P>After the last Release() of a local endpoint, we send 'Mdel' to let the 
midi_server know the endpoint is now deleted. We don't expect a reply back. If 
something goes wrong, the endpoint is deleted regardless. We do not send 
separate "unregistered" notifications, because deleting an endpoint implies 
that it is removed from the roster. For the same reason, we also don't send 
separate "disconnected" notifications.</P></LI>

<LI><P>The 'mDEL' notification triggers a BMidiRosterLooper to remove the 
corresponding BMidiEndpoint from its internal list. This object is always a 
proxy for a remote endpoint. The remote endpoint is gone, but whether we can 
also delete the proxy depends on its reference count. If no one is still using 
the object, its refcount is zero, and we can safely delete the object. 
Otherwise, we must defer destruction until the client Release()'s the 
object.</P></LI>

<LI><P>If you "delete" an endpoint, your app drops into the debugger.</P></LI>

<LI><P>If you Release() an endpoint too many times, your app <I>could</I> drop 
into the debugger. It might also crash, because you are now using a dead 
object. It depends on whether the memory that was previously occupied by your 
endpoint object was overwritten in the mean time.</P></LI>

<LI><P>You are allowed to pass NULL into the constructors of BMidiLocalConsumer 
and BMidiLocalProducer, in which case the endpoint's name is simply an empty 
string.</P></LI>

</UL>

<H3>Changing endpoint attributes</H3>

<UL>

<LI><P>An endpoint can be "invalid". In the case of a proxy this means that the 
remote endpoint is unregistered or even deleted. Local endpoints can only be 
invalid if something went wrong during their creation (no connection to server, 
for example). You can get the attributes of invalid objects, but you cannot set 
them. Any attempts to do so will return an error code.</P></LI>

<LI><P>For changing the name, latency, or properties of an endpoint, libmidi2 
sends an 'Mchg' message with the fields that should be changed, "midi:name", 
"midi:latency", or "midi:properties". Registering or unregistering an endpoint 
also sends such an 'Mchg' message, because we consider the "registered" state 
also an attribute, in "midi:registered". The message obviously also includes 
the ID of the endpoint in question. Properties are sent using a different 
message, because the properties are not stored inside the 
BMidiEndpoints.</P></LI>

<LI><P>After handling the 'Mchg' request, the midi_server broadcasts an 'mCHG' 
notification to all the other apps. This message has the same contents as the 
original request.</P></LI>

<LI><P>If the 'Mchg' message contains an invalid "midi:id" (i.e. no such 
endpoint exists or it does not belong to the app that sent the request), the 
midi_server returns an error code, and it does not notify the other 
apps.</P></LI>

<LI><P>If you try to Register() an endpoint that is already registered, 
libmidi2 does not send a message to the midi_server but simply returns B_OK. 
(Be's implementation <I>did</I> send a message, but our libmidi2 also keeps 
track whether an endpoint is registered or not.) Although registering an 
endpoint more than once doesn't make much sense, it is not considered an error. 
Likewise for Unregister()ing an endpoint that is not registered.</P></LI>

<LI><P>If you try to Register() or Unregister() a remote endpoint, libmidi2 
immediately returns an error code, and does not send a message to the server. 
Likewise for a local endpoints that are invalid (i.e. whose IsValid() function 
returns false).</P></LI>

<LI><P>BMidiRoster::Register() and Unregister() do the same thing as 
BMidiEndpoint::Register() and Unregister(). If you pass NULL into these 
functions, they return B_BAD_VALUE.</P></LI>

<LI><P>SetName() ignores NULL names. When you call it on a remote endpoint, 
SetName() does nothing. SetName() does not send a message if the new name is 
the same as the current name.</P></LI>

<LI><P>SetLatency() ignores negative values. SetLatency() does not send a 
message if the new latency is the same as the current latency. (Since 
SetLatency() lives in BMidiLocalConsumer, you can never use it on remote 
endpoints.)</P></LI>

<LI><P>We store a copy of the endpoint properties in each BMidiEndpoint. The 
properties of new endpoints are empty. GetProperties() copies this BMessage 
into the client's BMessage. GetProperties() returns NULL if the message 
parameter is NULL.</P></LI>

<LI><P>SetProperties() returns NULL if the message parameter is NULL. It 
returns an error code if the endpoint is remote or invalid. SetProperties() 
does <I>not</I> compare the contents of the new BMessage to the old, so it will 
always send out the change request.</P></LI>

</UL>

<H3>Connections</H3>

<UL>

<LI><P>BMidiProducer::Connect() sends an 'Mcon' request to the midi_server. 
This request contains the IDs of the producer and the consumer you want to 
connect. The server sends back a reply with a result code. If it is possible to 
make this connection, the server broadcasts an 'mCON' notification to all other 
apps. In one of these apps the producer is local, so that app's libmidi2 calls 
the BMidiLocalProducer::Connected() hook.</P></LI>

<LI><P>You are not allowed to connect the same producer and consumer more than 
once. The midi_server checks for this. It also returns an error code if you try 
to disconnect two endpoints that were not connected.</P></LI>

<LI><P>Disconnect() sends an 'Mdis' request to the server, which contains the 
IDs of the producer and consumer that you want to disconnect. The server 
replies with a result code. If the connection could be broken, it also sends an 
'mDIS' notification to the other apps. libmidi2 calls the local producer's 
BMidiLocalProducer::Disconnected() hook.</P></LI>

<LI><P>Connect() and Disconnect() immediately return an error code if you pass 
a NULL argument, or if the producer or consumer is invalid.</P></LI>

<LI><P>When you Release() a local consumer that is connected, all apps will go 
through their producers, and throw away this consumer from their connection 
lists. If one of these producers is local, we call its Disconnected() hook. If 
you release a local producer, this is not necessary.</P></LI>

</UL>

<H3>Watching</H3>

<UL>

<LI><P>When you call StartWatching(), the BMidiRosterLooper remembers the 
BMessenger, and sends it B_MIDI_EVENT notifications for all registered remote 
endpoints, and the current connections between them. It does not let you know 
about local endpoints. When you call StartWatching() a second time with the 
same BMessenger, you'll receive the whole bunch of notifications again. 
StartWatching(NULL) is not allowed, and will be ignored (so it is not the same 
as StopWatching()).</P></LI>

</UL>

<H3>Thread safety</H3>

<UL>

<LI><P>Within libmidi2 there are several possible race conditions, because we 
are dealing with two threads: the one from BMidiRosterLooper and a thread from 
the client app, most likely the BApplication's main thread. Both can access the 
same data: BMidiEndpoint objects. To synchronize these threads, we lock the 
BMidiRosterLooper, which is a normal BLooper. Anything happening in 
BMidiRosterLooper's message handlers is safe, because BLoopers are 
automatically locked when handling a message. Any other operations (which run 
from a different thread) must first lock the looper if they access the list of 
endpoints or certain BMidiEndpoint attributes (name, properties, etc).</P></LI>

<LI><P>What if you obtain a BMidiEndpoint object from FindEndpoint() and at the 
same time the BMidiRosterLooper receives an 'mDEL' request to delete that 
endpoint? FindEndpoint() locks the looper, and bumps the endpoint object before 
giving it to you. Now the looper sees that the endpoint's refcount is larger 
than 0, so it won't delete it (although it will remove the endpoint from its 
internal list). What if you Acquire() or Release() a remote endpoint while it 
is being deleted by the looper? That also won't happen, because if you have a 
pointer to that endpoint, its refcount is at least 1 and the looper won't 
delete it.</P></LI>

<LI><P>It is not safe to use a BMidiEndpoint and/or the BMidiRoster from more 
than one client thread at a time; if you want to do that, you should 
synchronize access to these objects yourself. The only exception is the Spray() 
functions from BMidiLocalProducer, since most producers have a separate thread 
to spray their MIDI events. This is fine, as long as that thread isn't used for 
anything else, and it is the only one that does the spraying.</P></LI>

<LI><P>BMidiProducer objects keep a list of consumers they are connected to. 
This list can be accessed by several threads at a time: the client's thread, 
the BMidiRosterLooper thread, and possibly a separate thread that is spraying 
MIDI events. We could have locked the producer using BMidiRosterLooper's lock, 
but that would freeze everything else while the producer is spraying events. 
Conversely, it would freeze all producers while the looper is talking to the 
midi_server. To lock with a finer granularity, each BMidiProducer has its own 
BLocker, which is used only to lock the list of connected consumers.</P></LI>

</UL>

<H3>Misc remarks</H3>

<UL>

<LI><P>BMidiEndpoint keeps track of its local/remote state with an "isLocal" 
variable, and whether it is a producer/consumer with "isConsumer". It also has 
an "isRegistered" field to remember whether this endpoint is registered or not. 
Why not lump all these different states together into one "flags" bitmask? The 
reason is that isLocal only makes sense to this application, not to others. 
Also, the values of isLocal and isConsumer never change, but isRegistered does. 
It made more sense (and clearer code) to separate them out. Finally, 
isRegistered does not need to be protected by a lock, even though it can be 
accessed by multiple threads at a time. Reading and writing a bool is atomic, 
so this can't get messed up.</P></LI>

</UL>

<H3>The messages</H3>

<BLOCKQUOTE><PRE>
Message: Mapp (MSG_REGISTER_APP)
    BMessenger midi:messenger
Reply: 
    (no reply)

Message: mAPP (MSG_APP_REGISTERED)
    (no fields)

Message: Mnew (MSG_CREATE_ENDPOINT)
    bool     midi:consumer
    bool     midi:registered
    char[]   midi:name
    BMessage midi:properties
    int32    midi:port       (consumer only)
    int64    midi:latency    (consumer only)
Reply:
    int32    midi:result
    int32    midi:id

Message: mNEW (MSG_ENPOINT_CREATED)
    int32    midi:id
    bool     midi:consumer
    bool     midi:registered
    char[]   midi:name
    BMessage midi:properties
    int32    midi:port       (consumer only)
    int64    midi:latency    (consumer only)

Message: Mdel (MSG_DELETE_ENDPOINT)
    int32 midi:id
Reply: 
    (no reply)

Message: Mdie (MSG_PURGE_ENDPOINT)
    int32 midi:id
Reply: 
    (no reply)

Message: mDEL (MSG_ENDPOINT_DELETED)
    int32 midi:id

Message: Mchg (MSG_CHANGE_ENDPOINT)
    int32    midi:id
    int32    midi:registered (optional)
    char[]   midi:name       (optional)
    int64    midi:latency    (optional)
    BMessage midi:properties (optional)
Reply:
    int32 midi:result

Message: mCHG (MSG_ENDPOINT_CHANGED)
    int32 midi:id
    int32    midi:registered (optional)
    char[]   midi:name       (optional)
    int64    midi:latency    (optional)
    BMessage midi:properties (optional)
</PRE></BLOCKQUOTE>

<HR SIZE="1">

<H2>MIDI events</H2>

<UL>

<LI><P>MIDI events are always sent from a BMidiLocalProducer to a 
BMidiLocalConsumer. Proxy endpoint objects have nothing to do with this. During 
its construction, the local consumer creates a kernel port. The ID of this port 
is published, so everyone knows what it is. When a producer sprays an event, it 
creates a message that it sends to the ports of all connected consumers.</P></LI>

<LI><P>This means that the Midi Kit considers MIDI messages as discrete events. 
Hardware drivers chop the stream of incoming MIDI data into separate events 
that they send out to one or more kernel ports. Consumers never have to worry 
about parsing a stream of MIDI data, just about handling a bunch of separate 
events.</P></LI>

<LI><P>Each BMidiLocalConsumer has a (realtime priority) thread associated with 
it that waits for data to arrive at the port. As soon as a new MIDI message 
comes in, the thread examines it and feeds it to the Data() hook. The Data() 
hook ignores the message if the "atomic" flag is false, or passes it on to one 
of the other hook functions otherwise. Incoming messages are also ignored if 
their contents are not valid; for example, if they have too few or too many 
bytes for a certain type of MIDI event.</P></LI>

<LI><P>Unlike the consumer, BMidiLocalProducer has no thread of its own. As a 
result, spraying MIDI events always happens in the thread of the caller. 
Because the consumer port's queue is only 1 message deep, spray functions will 
block if the consumer thread is already busy handling another MIDI event. (For 
this reason, the Midi Kit does not support interleaving of real time messages 
with lower priority messages such as sysex dumps, except at the driver 
level.)</P></LI>

<LI><P>The producer does not just send MIDI event data to the consumer, it also 
sends a 20-byte header describing the event. The total message looks like 
this:</P>

<BLOCKQUOTE><TABLE BORDER="1">

<TR><TD>4 bytes</TD><TD>ID of the producer</TD></TR>
<TR><TD>4 bytes</TD><TD>ID of the consumer</TD></TR>
<TR><TD>8 bytes</TD><TD>performance time</TD></TR>
<TR><TD>1 byte</TD><TD>atomic (1 = true, 0 = false)</TD></TR>
<TR><TD>3 bytes</TD><TD>padding (0)</TD></TR>
<TR><TD>x bytes</TD><TD>MIDI event data</TD></TR>

</TABLE></BLOCKQUOTE></LI>

<LI><P>In the case of a sysex event, the SystemExclusive() hook is only called 
if the first byte of the message is 0xF0. The sysex end marker (0xF7) is 
optional; only if the last byte is 0xF7 we strip it off. This is unlike Be's 
implementation, which all always strips the last byte even when it is not 0xF7. 
According to the MIDI spec, 0xF7 is not really required; any non-realtime 
status byte ends a sysex message.</P></LI>

<LI><P>SprayTempoChange() sends 0xFF5103tttttt, where tttttt is 60,000,000/bpm.
This feature is not really part of the MIDI spec, but an extension from the SMF
(Standard MIDI File) format. Of course, the TempoChange() hook is called in
response to this message.</P></LI>

<LI><P>The MIDI spec allows for a number of shortcuts. A Note On event with 
velocity 0 is supposed to be interpreted as a Note Off, for example. The Midi
Kit does not concern itself with these shortcuts. In this case, it still calls
the NoteOn() hook with a velocity parameter of 0.</P></LI>

<LI><P>The purpose of BMidiLocalConsumer's AllNotesOff() function is not 
entirely clear. All Notes Off is a so-called "channel mode message" and is 
generated by doing a SprayControlChange(channel, B_ALL_NOTES_OFF, 0). BMidi has
an AllNotesOff() function that sends an All Notes Off event to all channels,
and possible Note Off events to all keys on all channels as well. I suspect
someone at Be was confused by AllNotesOff() being declared "virtual", and
thought it was a hook function. Only that would explain it being in
BMidiLocalConsumer as opposed to BMidiLocalProducer, where it would have made
sense. The disassembly for Be's libmidi2.so shows that AllNotesOff() is empty,
so to cut a long story short, our AllNotesOff() simply does nothing and is
never invoked either.</P></LI>

<LI><P>There are several types of System Common events, each of which takes a 
different number of data bytes (0, 1, or 2). But SpraySystemCommon() and the 
SystemCommon() hook are always given 2 data parameters. The Midi Kit simply 
ignores the extra data bytes; in fact, in our implementation it doesn't even 
send them. (The Be implementation always sends 2 data bytes, but that will 
confuse the Midi Kit if the client does a SprayData() of a common event 
instead. In our case, that will still invoke the SystemCommon() hook, because 
we are not as easily fooled.)</P></LI>

<LI><P>Handling of timeouts is fairly straightforward. When reading from the 
port, we specify an absolute timeout. When the port function returns with a 
B_TIMED_OUT error code, we call the Timeout() hook. Then we reset the timeout 
value to -1, which means that timeouts are disabled (until the client calls 
SetTimeout() again). This design means that a call to SetTimeout() only takes 
effect the next time we read from the port, i.e. after at least one new MIDI 
event is received (or the previous timeout is triggered). Even though 
BMidiLocalConsumer's timeout and timeoutData values are accessed by two 
different threads, I did not bother to protect this. Both values are int32's 
and reading/writing them should be an atomic operation on most processors 
anyway.</P></LI>

</UL>

</BODY>
</HTML>