4 <H1>Midi Kit design
</H1>
6 <P>The Midi Kit consists of the midi_server and two shared libraries,
7 libmidi2.so and libmidi.so. The latter is the
"old" pre-R5 Midi Kit and has
8 been re-implemented using the facilities from libmidi2, which makes it fully
9 compatible with the new kit. This document describes the design and
10 implementation of the OpenBeOS midi_server and libmidi2.so.
</P>
12 <P>The midi_server has two jobs: it keeps track of the endpoints that the
13 client apps have created, and it publishes endpoints for the devices from
14 /dev/midi. (This last task could have been done by any other app, but it was
15 just as convenient to make the midi_server do that.) The libmidi2.so library
16 also has two jobs: it assists the midi_server with the housekeeping stuff, and
17 it allows endpoints to send and receive MIDI events. (That's right, the
18 midi_server has nothing to do with the actual MIDI data.)
</P>
22 <H2>Ooh, pictures
</H2>
24 <P>The following image shows the center of Midi Kit activity, the midi_server,
25 and its data structures:
</P>
27 <BLOCKQUOTE><IMG ALT=
"" SRC=
"midi_server.png"></BLOCKQUOTE>
29 <P>And here is the picture for libmidi2.so:
</P>
31 <BLOCKQUOTE><IMG ALT=
"" SRC=
"libmidi2.png"></BLOCKQUOTE>
33 <P>Note that these diagrams give only a conceptual overview of who is
34 responsible for which bits of data. The actual implementation details of the
43 <LI><P>The design for our implementation of the midi2
"housekeeping" protocol
44 roughly follows
<A HREF=
"oldprotocol.html">what Be did
</A>, although there are
45 some differences. In Be's implementation, the BMidiRosters only have
46 BMidiEndpoints for remote endpoints if they are registered. In our
47 implementation, the BMidiRosters have BMidiEndpoint objects for
<I>all
</I>
48 endpoints, including remote endpoints that aren't published at all. If there
49 are many unpublished endpoints in the system, our approach is less optimal.
50 However, it made the implementation of the Midi Kit much easier ;-)
</P></LI>
52 <LI><P>Be's libmidi2.so exports the symbols
"midi_debug_level" and
53 "midi_dispatcher_priority", both int32's. Our libmidi2 does not use either of
54 these. But even though these symbols are not present in the headers, some apps
55 may use them nonetheless. That's why our libmidi2 exports those symbols as
58 <LI><P>The name of the message fields in Be's implementation of the protocol
59 had the
"be:" prefix. Our fields have a
"midi:" prefix instead. Except for the
60 fields in the B_MIDI_EVENT notification messages, because that would break
61 compatibility with existing apps.
</P></LI>
65 <H3>Initialization
</H3>
69 <LI><P>The first time an app uses a midi2 class, the BMidiRoster::MidiRoster()
70 method sends an 'Mapp' message to the midi_server, and blocks (on a semaphore).
71 This message includes a messenger to the app's BMidiRosterLooper object. The
72 server adds the app to its list of registered apps. Then the server
73 asynchronously sends back a series of 'mNEW' message notifications for all
74 endpoints on the roster, and 'mCON' messages for all existing connections. The
75 BMidiRosterLooper creates BMidiEndpoint objects for these endpoints and adds
76 them to its local roster; if the app is watching, it also sends out
77 corresponding B_MIDI_EVENT notifications. Finally, the midi_server sends an
78 'mAPP' message to notify the app that it has been successfully registered. Upon
79 receipt, BMidiRoster::MidiRoster() unblocks and returns control to the client
80 code. This handshake is the only asynchronous message exchange; all the other
81 requests have a synchronous reply.
</P></LI>
83 <LI><P>If the server detects an error during any of this (incorrect message
84 format, delivery failure, etc.) it simply ignores the request and does not try
85 to send anything back to the client (which is most likely impossible anyway).
86 If the app detects an error (server sends back meaningless info, cannot connect
87 to server), it pretends that everything is hunkey dorey. (The API has no way of
88 letting the client know that the initialization succeeded.) Next time the app
89 tries something, the server either still does not respond, or it ignores the
90 request (because this app isn't properly registered). However, if the app does
91 not receive the 'mAPP' message, it will not unblock, and remains frozen for all
94 <LI><P>BMidiRoster's MidiRoster() method creates the one and only BMidiRoster
95 instance on the heap the first time it is called. This instance is
96 automatically destroyed when the app quits.
</P></LI>
100 <H3>Error handling
</H3>
104 <LI><P>If some error occurs, then the reply message is only guaranteed to
105 contain the
"midi:result" field with some non- zero error code. libmidi2 can
106 only assume that the reply contains other data on success (i.e. when
107 "midi:result" is B_OK).
</P></LI>
109 <LI><P>The timeout for delivering and responding to a message is about
2
110 seconds. If the client receives no reply within that time, it assumes the
111 request failed. If the server cannot deliver a message within
2 seconds, it
112 assumes the client is dead and removes it (and its endpoints) from the roster.
113 Of course, these assumptions may be false. If the client wasn't dead and tries
114 to send another request to the server, then the server will now ignore it,
115 since the client app is no longer registered.
</P></LI>
117 <LI><P>Because we work with timeouts, we must be careful to avoid
118 misunderstandings between the midi_server and the client app. Both sides must
119 recognize the timeout, so they both can ignore the operation. If, however, the
120 server thinks that everything went okay, but the client flags an error, then
121 the server and the client will have two different ideas of the current state of
122 the roster. Of course, those situations must be avoided.
</P></LI>
124 <LI><P>Although apps register themselves with the midi_server, there is no
125 corresponding
"unregister" message. The only way the server recognizes that an
126 app and its endpoints are no longer available is when it fails to deliver a
127 message to that app. In that case, we remove the app and all its endpoints from
128 the roster. To do this, the server sends
"purge endpoint" messages to itself
129 for all of the app's endpoints. This means we don't immediately throw the app
130 away, but we schedule that for some time in the future. That makes the whole
131 event handling mechanism much cleaner. There is no reply to the purge request.
132 (Actually, we
<I>do
</I> immediately throw away the app_t object, since that
133 doesn't really interfere with anything.) (If there are other events pending in
134 the queue which also cause notifications, then the server may send multiple
135 purge messages for the same endpoints. That's no biggie, because a purge
136 message will be ignored if its endpoint no longer exists.)
</P></LI>
138 <LI><P>As mentioned above, the midi_server ignores messages that do not come
139 from a registered app, although it does send back an error reply. In the case
140 of the
"purge endpoint" message, the server makes sure the message was local
141 (i.e. sent by the midi_server itself).
</P></LI>
143 <LI><P>Note: BMessage's SendReply() apparently succeeds even if you kill the
144 app that the reply is intended for. This is rather strange, and it means that
145 you can't test delivery error handling for replies by killing the app. (You
146 <I>can
</I> kill the app for testing the error handling on notifications,
151 <H3>Creating and deleting endpoints
</H3>
155 <LI><P>When client code creates a new BMidiLocalProducer or BMidiLocalConsumer
156 endpoint, we send an 'Mnew' message to the server. Unlike Be's implementation,
157 the
"name" field is always present, even if the name is empty. After adding the
158 endpoint to the roster, the server sends 'mNEW' notifications to all other
159 applications. Upon receipt of this notification, the BMidiRosterLoopers of
160 these apps create a new BMidiEndpoint for the endpoint and add it to their
161 internal list of endpoints. The app that made the request receives a reply with
162 a single
"midi:result" field.
</P></LI>
164 <LI><P>When you
"new" an endpoint, its refcount is
1, even if the creation
165 failed. (For example, if the midi_server does not run.) When you Acquire(), the
166 refcount is bumped. When you Release(), it is decremented. When refcount drops
167 to
0, the endpoint object
"deletes" itself. (So client code should never use an
168 endpoint after having Release()'d it, because the object may have just been
169 killed.) When creation succeeds, IsValid() returns true and ID() returns a
170 valid ID (
> 0). Upon failure, IsValid() is false and ID() returns
0.
</P></LI>
172 <LI><P>After the last Release() of a local endpoint, we send 'Mdel' to let the
173 midi_server know the endpoint is now deleted. We don't expect a reply back. If
174 something goes wrong, the endpoint is deleted regardless. We do not send
175 separate
"unregistered" notifications, because deleting an endpoint implies
176 that it is removed from the roster. For the same reason, we also don't send
177 separate
"disconnected" notifications.
</P></LI>
179 <LI><P>The 'mDEL' notification triggers a BMidiRosterLooper to remove the
180 corresponding BMidiEndpoint from its internal list. This object is always a
181 proxy for a remote endpoint. The remote endpoint is gone, but whether we can
182 also delete the proxy depends on its reference count. If no one is still using
183 the object, its refcount is zero, and we can safely delete the object.
184 Otherwise, we must defer destruction until the client Release()'s the
187 <LI><P>If you
"delete" an endpoint, your app drops into the debugger.
</P></LI>
189 <LI><P>If you Release() an endpoint too many times, your app
<I>could
</I> drop
190 into the debugger. It might also crash, because you are now using a dead
191 object. It depends on whether the memory that was previously occupied by your
192 endpoint object was overwritten in the mean time.
</P></LI>
194 <LI><P>You are allowed to pass NULL into the constructors of BMidiLocalConsumer
195 and BMidiLocalProducer, in which case the endpoint's name is simply an empty
200 <H3>Changing endpoint attributes
</H3>
204 <LI><P>An endpoint can be
"invalid". In the case of a proxy this means that the
205 remote endpoint is unregistered or even deleted. Local endpoints can only be
206 invalid if something went wrong during their creation (no connection to server,
207 for example). You can get the attributes of invalid objects, but you cannot set
208 them. Any attempts to do so will return an error code.
</P></LI>
210 <LI><P>For changing the name, latency, or properties of an endpoint, libmidi2
211 sends an 'Mchg' message with the fields that should be changed,
"midi:name",
212 "midi:latency", or
"midi:properties". Registering or unregistering an endpoint
213 also sends such an 'Mchg' message, because we consider the
"registered" state
214 also an attribute, in
"midi:registered". The message obviously also includes
215 the ID of the endpoint in question. Properties are sent using a different
216 message, because the properties are not stored inside the
217 BMidiEndpoints.
</P></LI>
219 <LI><P>After handling the 'Mchg' request, the midi_server broadcasts an 'mCHG'
220 notification to all the other apps. This message has the same contents as the
221 original request.
</P></LI>
223 <LI><P>If the 'Mchg' message contains an invalid
"midi:id" (i.e. no such
224 endpoint exists or it does not belong to the app that sent the request), the
225 midi_server returns an error code, and it does not notify the other
228 <LI><P>If you try to Register() an endpoint that is already registered,
229 libmidi2 does not send a message to the midi_server but simply returns B_OK.
230 (Be's implementation
<I>did
</I> send a message, but our libmidi2 also keeps
231 track whether an endpoint is registered or not.) Although registering an
232 endpoint more than once doesn't make much sense, it is not considered an error.
233 Likewise for Unregister()ing an endpoint that is not registered.
</P></LI>
235 <LI><P>If you try to Register() or Unregister() a remote endpoint, libmidi2
236 immediately returns an error code, and does not send a message to the server.
237 Likewise for a local endpoints that are invalid (i.e. whose IsValid() function
238 returns false).
</P></LI>
240 <LI><P>BMidiRoster::Register() and Unregister() do the same thing as
241 BMidiEndpoint::Register() and Unregister(). If you pass NULL into these
242 functions, they return B_BAD_VALUE.
</P></LI>
244 <LI><P>SetName() ignores NULL names. When you call it on a remote endpoint,
245 SetName() does nothing. SetName() does not send a message if the new name is
246 the same as the current name.
</P></LI>
248 <LI><P>SetLatency() ignores negative values. SetLatency() does not send a
249 message if the new latency is the same as the current latency. (Since
250 SetLatency() lives in BMidiLocalConsumer, you can never use it on remote
253 <LI><P>We store a copy of the endpoint properties in each BMidiEndpoint. The
254 properties of new endpoints are empty. GetProperties() copies this BMessage
255 into the client's BMessage. GetProperties() returns NULL if the message
256 parameter is NULL.
</P></LI>
258 <LI><P>SetProperties() returns NULL if the message parameter is NULL. It
259 returns an error code if the endpoint is remote or invalid. SetProperties()
260 does
<I>not
</I> compare the contents of the new BMessage to the old, so it will
261 always send out the change request.
</P></LI>
269 <LI><P>BMidiProducer::Connect() sends an 'Mcon' request to the midi_server.
270 This request contains the IDs of the producer and the consumer you want to
271 connect. The server sends back a reply with a result code. If it is possible to
272 make this connection, the server broadcasts an 'mCON' notification to all other
273 apps. In one of these apps the producer is local, so that app's libmidi2 calls
274 the BMidiLocalProducer::Connected() hook.
</P></LI>
276 <LI><P>You are not allowed to connect the same producer and consumer more than
277 once. The midi_server checks for this. It also returns an error code if you try
278 to disconnect two endpoints that were not connected.
</P></LI>
280 <LI><P>Disconnect() sends an 'Mdis' request to the server, which contains the
281 IDs of the producer and consumer that you want to disconnect. The server
282 replies with a result code. If the connection could be broken, it also sends an
283 'mDIS' notification to the other apps. libmidi2 calls the local producer's
284 BMidiLocalProducer::Disconnected() hook.
</P></LI>
286 <LI><P>Connect() and Disconnect() immediately return an error code if you pass
287 a NULL argument, or if the producer or consumer is invalid.
</P></LI>
289 <LI><P>When you Release() a local consumer that is connected, all apps will go
290 through their producers, and throw away this consumer from their connection
291 lists. If one of these producers is local, we call its Disconnected() hook. If
292 you release a local producer, this is not necessary.
</P></LI>
300 <LI><P>When you call StartWatching(), the BMidiRosterLooper remembers the
301 BMessenger, and sends it B_MIDI_EVENT notifications for all registered remote
302 endpoints, and the current connections between them. It does not let you know
303 about local endpoints. When you call StartWatching() a second time with the
304 same BMessenger, you'll receive the whole bunch of notifications again.
305 StartWatching(NULL) is not allowed, and will be ignored (so it is not the same
306 as StopWatching()).
</P></LI>
310 <H3>Thread safety
</H3>
314 <LI><P>Within libmidi2 there are several possible race conditions, because we
315 are dealing with two threads: the one from BMidiRosterLooper and a thread from
316 the client app, most likely the BApplication's main thread. Both can access the
317 same data: BMidiEndpoint objects. To synchronize these threads, we lock the
318 BMidiRosterLooper, which is a normal BLooper. Anything happening in
319 BMidiRosterLooper's message handlers is safe, because BLoopers are
320 automatically locked when handling a message. Any other operations (which run
321 from a different thread) must first lock the looper if they access the list of
322 endpoints or certain BMidiEndpoint attributes (name, properties, etc).
</P></LI>
324 <LI><P>What if you obtain a BMidiEndpoint object from FindEndpoint() and at the
325 same time the BMidiRosterLooper receives an 'mDEL' request to delete that
326 endpoint? FindEndpoint() locks the looper, and bumps the endpoint object before
327 giving it to you. Now the looper sees that the endpoint's refcount is larger
328 than
0, so it won't delete it (although it will remove the endpoint from its
329 internal list). What if you Acquire() or Release() a remote endpoint while it
330 is being deleted by the looper? That also won't happen, because if you have a
331 pointer to that endpoint, its refcount is at least
1 and the looper won't
334 <LI><P>It is not safe to use a BMidiEndpoint and/or the BMidiRoster from more
335 than one client thread at a time; if you want to do that, you should
336 synchronize access to these objects yourself. The only exception is the Spray()
337 functions from BMidiLocalProducer, since most producers have a separate thread
338 to spray their MIDI events. This is fine, as long as that thread isn't used for
339 anything else, and it is the only one that does the spraying.
</P></LI>
341 <LI><P>BMidiProducer objects keep a list of consumers they are connected to.
342 This list can be accessed by several threads at a time: the client's thread,
343 the BMidiRosterLooper thread, and possibly a separate thread that is spraying
344 MIDI events. We could have locked the producer using BMidiRosterLooper's lock,
345 but that would freeze everything else while the producer is spraying events.
346 Conversely, it would freeze all producers while the looper is talking to the
347 midi_server. To lock with a finer granularity, each BMidiProducer has its own
348 BLocker, which is used only to lock the list of connected consumers.
</P></LI>
352 <H3>Misc remarks
</H3>
356 <LI><P>BMidiEndpoint keeps track of its local/remote state with an
"isLocal"
357 variable, and whether it is a producer/consumer with
"isConsumer". It also has
358 an
"isRegistered" field to remember whether this endpoint is registered or not.
359 Why not lump all these different states together into one
"flags" bitmask? The
360 reason is that isLocal only makes sense to this application, not to others.
361 Also, the values of isLocal and isConsumer never change, but isRegistered does.
362 It made more sense (and clearer code) to separate them out. Finally,
363 isRegistered does not need to be protected by a lock, even though it can be
364 accessed by multiple threads at a time. Reading and writing a bool is atomic,
365 so this can't get messed up.
</P></LI>
369 <H3>The messages
</H3>
372 Message: Mapp (MSG_REGISTER_APP)
373 BMessenger midi:messenger
377 Message: mAPP (MSG_APP_REGISTERED)
380 Message: Mnew (MSG_CREATE_ENDPOINT)
384 BMessage midi:properties
385 int32 midi:port (consumer only)
386 int64 midi:latency (consumer only)
391 Message: mNEW (MSG_ENPOINT_CREATED)
396 BMessage midi:properties
397 int32 midi:port (consumer only)
398 int64 midi:latency (consumer only)
400 Message: Mdel (MSG_DELETE_ENDPOINT)
405 Message: Mdie (MSG_PURGE_ENDPOINT)
410 Message: mDEL (MSG_ENDPOINT_DELETED)
413 Message: Mchg (MSG_CHANGE_ENDPOINT)
415 int32 midi:registered (optional)
416 char[] midi:name (optional)
417 int64 midi:latency (optional)
418 BMessage midi:properties (optional)
422 Message: mCHG (MSG_ENDPOINT_CHANGED)
424 int32 midi:registered (optional)
425 char[] midi:name (optional)
426 int64 midi:latency (optional)
427 BMessage midi:properties (optional)
436 <LI><P>MIDI events are always sent from a BMidiLocalProducer to a
437 BMidiLocalConsumer. Proxy endpoint objects have nothing to do with this. During
438 its construction, the local consumer creates a kernel port. The ID of this port
439 is published, so everyone knows what it is. When a producer sprays an event, it
440 creates a message that it sends to the ports of all connected consumers.
</P></LI>
442 <LI><P>This means that the Midi Kit considers MIDI messages as discrete events.
443 Hardware drivers chop the stream of incoming MIDI data into separate events
444 that they send out to one or more kernel ports. Consumers never have to worry
445 about parsing a stream of MIDI data, just about handling a bunch of separate
448 <LI><P>Each BMidiLocalConsumer has a (realtime priority) thread associated with
449 it that waits for data to arrive at the port. As soon as a new MIDI message
450 comes in, the thread examines it and feeds it to the Data() hook. The Data()
451 hook ignores the message if the
"atomic" flag is false, or passes it on to one
452 of the other hook functions otherwise. Incoming messages are also ignored if
453 their contents are not valid; for example, if they have too few or too many
454 bytes for a certain type of MIDI event.
</P></LI>
456 <LI><P>Unlike the consumer, BMidiLocalProducer has no thread of its own. As a
457 result, spraying MIDI events always happens in the thread of the caller.
458 Because the consumer port's queue is only
1 message deep, spray functions will
459 block if the consumer thread is already busy handling another MIDI event. (For
460 this reason, the Midi Kit does not support interleaving of real time messages
461 with lower priority messages such as sysex dumps, except at the driver
464 <LI><P>The producer does not just send MIDI event data to the consumer, it also
465 sends a
20-byte header describing the event. The total message looks like
468 <BLOCKQUOTE><TABLE BORDER=
"1">
470 <TR><TD>4 bytes
</TD><TD>ID of the producer
</TD></TR>
471 <TR><TD>4 bytes
</TD><TD>ID of the consumer
</TD></TR>
472 <TR><TD>8 bytes
</TD><TD>performance time
</TD></TR>
473 <TR><TD>1 byte
</TD><TD>atomic (
1 = true,
0 = false)
</TD></TR>
474 <TR><TD>3 bytes
</TD><TD>padding (
0)
</TD></TR>
475 <TR><TD>x bytes
</TD><TD>MIDI event data
</TD></TR>
477 </TABLE></BLOCKQUOTE></LI>
479 <LI><P>In the case of a sysex event, the SystemExclusive() hook is only called
480 if the first byte of the message is
0xF0. The sysex end marker (
0xF7) is
481 optional; only if the last byte is
0xF7 we strip it off. This is unlike Be's
482 implementation, which all always strips the last byte even when it is not
0xF7.
483 According to the MIDI spec,
0xF7 is not really required; any non-realtime
484 status byte ends a sysex message.
</P></LI>
486 <LI><P>SprayTempoChange() sends
0xFF5103tttttt, where tttttt is
60,
000,
000/bpm.
487 This feature is not really part of the MIDI spec, but an extension from the SMF
488 (Standard MIDI File) format. Of course, the TempoChange() hook is called in
489 response to this message.
</P></LI>
491 <LI><P>The MIDI spec allows for a number of shortcuts. A Note On event with
492 velocity
0 is supposed to be interpreted as a Note Off, for example. The Midi
493 Kit does not concern itself with these shortcuts. In this case, it still calls
494 the NoteOn() hook with a velocity parameter of
0.
</P></LI>
496 <LI><P>The purpose of BMidiLocalConsumer's AllNotesOff() function is not
497 entirely clear. All Notes Off is a so-called
"channel mode message" and is
498 generated by doing a SprayControlChange(channel, B_ALL_NOTES_OFF,
0). BMidi has
499 an AllNotesOff() function that sends an All Notes Off event to all channels,
500 and possible Note Off events to all keys on all channels as well. I suspect
501 someone at Be was confused by AllNotesOff() being declared
"virtual", and
502 thought it was a hook function. Only that would explain it being in
503 BMidiLocalConsumer as opposed to BMidiLocalProducer, where it would have made
504 sense. The disassembly for Be's libmidi2.so shows that AllNotesOff() is empty,
505 so to cut a long story short, our AllNotesOff() simply does nothing and is
506 never invoked either.
</P></LI>
508 <LI><P>There are several types of System Common events, each of which takes a
509 different number of data bytes (
0,
1, or
2). But SpraySystemCommon() and the
510 SystemCommon() hook are always given
2 data parameters. The Midi Kit simply
511 ignores the extra data bytes; in fact, in our implementation it doesn't even
512 send them. (The Be implementation always sends
2 data bytes, but that will
513 confuse the Midi Kit if the client does a SprayData() of a common event
514 instead. In our case, that will still invoke the SystemCommon() hook, because
515 we are not as easily fooled.)
</P></LI>
517 <LI><P>Handling of timeouts is fairly straightforward. When reading from the
518 port, we specify an absolute timeout. When the port function returns with a
519 B_TIMED_OUT error code, we call the Timeout() hook. Then we reset the timeout
520 value to -
1, which means that timeouts are disabled (until the client calls
521 SetTimeout() again). This design means that a call to SetTimeout() only takes
522 effect the next time we read from the port, i.e. after at least one new MIDI
523 event is received (or the previous timeout is triggered). Even though
524 BMidiLocalConsumer's timeout and timeoutData values are accessed by two
525 different threads, I did not bother to protect this. Both values are int32's
526 and reading/writing them should be an atomic operation on most processors