Midi Kit design===============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 andhas been re-implemented using the facilities from libmidi2, which makesit fully compatible with the new kit. This document describes the designand implementation of the OpenBeOS midi_server and libmidi2.so.The midi_server has two jobs: it keeps track of the endpoints that theclient apps have created, and it publishes endpoints for the devicesfrom /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.) Thelibmidi2.so library also has two jobs: it assists the midi_server withthe housekeeping stuff, and it allows endpoints to send and receive MIDIevents. (That's right, the midi_server has nothing to do with the actualMIDI data.)--------------Ooh, pictures-------------The following image shows the center of Midi Kit activity, themidi_server, and its data structures:|image0|And here is the picture for libmidi2.so:|image1|Note that these diagrams give only a conceptual overview of who isresponsible for which bits of data. The actual implementation details ofthe kit may differ.--------------Housekeeping------------- The design for our implementation of the midi2 "housekeeping"protocol roughly follows `what Be did <oldprotocol.html>`__, althoughthere are some differences. In Be's implementation, the BMidiRostersonly have BMidiEndpoints for remote endpoints if they are registered.In our implementation, the BMidiRosters have BMidiEndpoint objectsfor *all* endpoints, including remote endpoints that aren't publishedat all. If there are many unpublished endpoints in the system, ourapproach is less optimal. However, it made the implementation of theMidi Kit much easier ;-)- Be's libmidi2.so exports the symbols "midi_debug_level" and"midi_dispatcher_priority", both int32's. Our libmidi2 does not useeither of these. But even though these symbols are not present in theheaders, some apps may use them nonetheless. That's why our libmidi2exports those symbols as well.- The name of the message fields in Be's implementation of the protocolhad 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.Initialization~~~~~~~~~~~~~~- The first time an app uses a midi2 class, theBMidiRoster::MidiRoster() method sends an 'Mapp' message to themidi_server, and blocks (on a semaphore). This message includes amessenger to the app's BMidiRosterLooper object. The server adds theapp to its list of registered apps. Then the server asynchronouslysends back a series of 'mNEW' message notifications for all endpointson the roster, and 'mCON' messages for all existing connections. TheBMidiRosterLooper creates BMidiEndpoint objects for these endpointsand adds them to its local roster; if the app is watching, it alsosends out corresponding B_MIDI_EVENT notifications. Finally, themidi_server sends an 'mAPP' message to notify the app that it hasbeen successfully registered. Upon receipt, BMidiRoster::MidiRoster()unblocks and returns control to the client code. This handshake isthe only asynchronous message exchange; all the other requests have asynchronous reply.- If the server detects an error during any of this (incorrect messageformat, delivery failure, etc.) it simply ignores the request anddoes not try to send anything back to the client (which is mostlikely impossible anyway). If the app detects an error (server sendsback meaningless info, cannot connect to server), it pretends thateverything is hunkey dorey. (The API has no way of letting the clientknow that the initialization succeeded.) Next time the app triessomething, the server either still does not respond, or it ignoresthe request (because this app isn't properly registered). However, ifthe app does not receive the 'mAPP' message, it will not unblock, andremains frozen for all eternity.- BMidiRoster's MidiRoster() method creates the one and onlyBMidiRoster instance on the heap the first time it is called. Thisinstance is automatically destroyed when the app quits.Error handling~~~~~~~~~~~~~~- If some error occurs, then the reply message is only guaranteed tocontain the "midi:result" field with some non- zero error code.libmidi2 can only assume that the reply contains other data onsuccess (i.e. when "midi:result" is B_OK).- The timeout for delivering and responding to a message is about 2seconds. If the client receives no reply within that time, it assumesthe request failed. If the server cannot deliver a message within 2seconds, it assumes the client is dead and removes it (and itsendpoints) from the roster. Of course, these assumptions may befalse. If the client wasn't dead and tries to send another request tothe server, then the server will now ignore it, since the client appis no longer registered.- Because we work with timeouts, we must be careful to avoidmisunderstandings between the midi_server and the client app. Bothsides must recognize the timeout, so they both can ignore theoperation. If, however, the server thinks that everything went okay,but the client flags an error, then the server and the client willhave two different ideas of the current state of the roster. Ofcourse, those situations must be avoided.- Although apps register themselves with the midi_server, there is nocorresponding "unregister" message. The only way the serverrecognizes that an app and its endpoints are no longer available iswhen it fails to deliver a message to that app. In that case, weremove the app and all its endpoints from the roster. To do this, theserver sends "purge endpoint" messages to itself for all of the app'sendpoints. This means we don't immediately throw the app away, but weschedule that for some time in the future. That makes the whole eventhandling mechanism much cleaner. There is no reply to the purgerequest. (Actually, we *do* immediately throw away the app_t object,since that doesn't really interfere with anything.) (If there areother events pending in the queue which also cause notifications,then the server may send multiple purge messages for the sameendpoints. That's no biggie, because a purge message will be ignoredif its endpoint no longer exists.)- As mentioned above, the midi_server ignores messages that do not comefrom a registered app, although it does send back an error reply. Inthe case of the "purge endpoint" message, the server makes sure themessage was local (i.e. sent by the midi_server itself).- Note: BMessage's SendReply() apparently succeeds even if you kill theapp that the reply is intended for. This is rather strange, and itmeans that you can't test delivery error handling for replies bykilling the app. (You *can* kill the app for testing the errorhandling on notifications, however.)Creating and deleting endpoints~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~- When client code creates a new BMidiLocalProducer orBMidiLocalConsumer endpoint, we send an 'Mnew' message to the server.Unlike Be's implementation, the "name" field is always present, evenif the name is empty. After adding the endpoint to the roster, theserver sends 'mNEW' notifications to all other applications. Uponreceipt of this notification, the BMidiRosterLoopers of these appscreate a new BMidiEndpoint for the endpoint and add it to theirinternal list of endpoints. The app that made the request receives areply with a single "midi:result" field.- When you "new" an endpoint, its refcount is 1, even if the creationfailed. (For example, if the midi_server does not run.) When youAcquire(), the refcount is bumped. When you Release(), it isdecremented. When refcount drops to 0, the endpoint object "deletes"itself. (So client code should never use an endpoint after havingRelease()'d it, because the object may have just been killed.) Whencreation succeeds, IsValid() returns true and ID() returns a valid ID(> 0). Upon failure, IsValid() is false and ID() returns 0.- After the last Release() of a local endpoint, we send 'Mdel' to letthe midi_server know the endpoint is now deleted. We don't expect areply back. If something goes wrong, the endpoint is deletedregardless. We do not send separate "unregistered" notifications,because deleting an endpoint implies that it is removed from theroster. For the same reason, we also don't send separate"disconnected" notifications.- The 'mDEL' notification triggers a BMidiRosterLooper to remove thecorresponding BMidiEndpoint from its internal list. This object isalways a proxy for a remote endpoint. The remote endpoint is gone,but whether we can also delete the proxy depends on its referencecount. If no one is still using the object, its refcount is zero, andwe can safely delete the object. Otherwise, we must defer destructionuntil the client Release()'s the object.- If you "delete" an endpoint, your app drops into the debugger.- If you Release() an endpoint too many times, your app *could* dropinto the debugger. It might also crash, because you are now using adead object. It depends on whether the memory that was previouslyoccupied by your endpoint object was overwritten in the mean time.- You are allowed to pass NULL into the constructors ofBMidiLocalConsumer and BMidiLocalProducer, in which case theendpoint's name is simply an empty string.Changing endpoint attributes~~~~~~~~~~~~~~~~~~~~~~~~~~~~- An endpoint can be "invalid". In the case of a proxy this means thatthe remote endpoint is unregistered or even deleted. Local endpointscan only be invalid if something went wrong during their creation (noconnection to server, for example). You can get the attributes ofinvalid objects, but you cannot set them. Any attempts to do so willreturn an error code.- For changing the name, latency, or properties of an endpoint,libmidi2 sends an 'Mchg' message with the fields that should bechanged, "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 anattribute, in "midi:registered". The message obviously also includesthe ID of the endpoint in question. Properties are sent using adifferent message, because the properties are not stored inside theBMidiEndpoints.- After handling the 'Mchg' request, the midi_server broadcasts an'mCHG' notification to all the other apps. This message has the samecontents as the original request.- If the 'Mchg' message contains an invalid "midi:id" (i.e. no suchendpoint exists or it does not belong to the app that sent therequest), the midi_server returns an error code, and it does notnotify the other apps.- If you try to Register() an endpoint that is already registered,libmidi2 does not send a message to the midi_server but simplyreturns B_OK. (Be's implementation *did* send a message, but ourlibmidi2 also keeps track whether an endpoint is registered or not.)Although registering an endpoint more than once doesn't make muchsense, it is not considered an error. Likewise for Unregister()ing anendpoint that is not registered.- If you try to Register() or Unregister() a remote endpoint, libmidi2immediately returns an error code, and does not send a message to theserver. Likewise for a local endpoints that are invalid (i.e. whoseIsValid() function returns false).- BMidiRoster::Register() and Unregister() do the same thing asBMidiEndpoint::Register() and Unregister(). If you pass NULL intothese functions, they return B_BAD_VALUE.- SetName() ignores NULL names. When you call it on a remote endpoint,SetName() does nothing. SetName() does not send a message if the newname is the same as the current name.- SetLatency() ignores negative values. SetLatency() does not send amessage if the new latency is the same as the current latency. (SinceSetLatency() lives in BMidiLocalConsumer, you can never use it onremote endpoints.)- We store a copy of the endpoint properties in each BMidiEndpoint. Theproperties of new endpoints are empty. GetProperties() copies thisBMessage into the client's BMessage. GetProperties() returns NULL ifthe message parameter is NULL.- SetProperties() returns NULL if the message parameter is NULL. Itreturns an error code if the endpoint is remote or invalid.SetProperties() does *not* compare the contents of the new BMessageto the old, so it will always send out the change request.Connections~~~~~~~~~~~- BMidiProducer::Connect() sends an 'Mcon' request to the midi_server.This request contains the IDs of the producer and the consumer youwant to connect. The server sends back a reply with a result code. Ifit is possible to make this connection, the server broadcasts an'mCON' notification to all other apps. In one of these apps theproducer is local, so that app's libmidi2 calls theBMidiLocalProducer::Connected() hook.- You are not allowed to connect the same producer and consumer morethan once. The midi_server checks for this. It also returns an errorcode if you try to disconnect two endpoints that were not connected.- Disconnect() sends an 'Mdis' request to the server, which containsthe IDs of the producer and consumer that you want to disconnect. Theserver replies with a result code. If the connection could be broken,it also sends an 'mDIS' notification to the other apps. libmidi2calls the local producer's BMidiLocalProducer::Disconnected() hook.- Connect() and Disconnect() immediately return an error code if youpass a NULL argument, or if the producer or consumer is invalid.- When you Release() a local consumer that is connected, all apps willgo through their producers, and throw away this consumer from theirconnection lists. If one of these producers is local, we call itsDisconnected() hook. If you release a local producer, this is notnecessary.Watching~~~~~~~~- When you call StartWatching(), the BMidiRosterLooper remembers theBMessenger, and sends it B_MIDI_EVENT notifications for allregistered remote endpoints, and the current connections betweenthem. It does not let you know about local endpoints. When you callStartWatching() a second time with the same BMessenger, you'llreceive the whole bunch of notifications again. StartWatching(NULL)is not allowed, and will be ignored (so it is not the same asStopWatching()).Thread safety~~~~~~~~~~~~~- Within libmidi2 there are several possible race conditions, becausewe are dealing with two threads: the one from BMidiRosterLooper and athread from the client app, most likely the BApplication's mainthread. Both can access the same data: BMidiEndpoint objects. Tosynchronize these threads, we lock the BMidiRosterLooper, which is anormal BLooper. Anything happening in BMidiRosterLooper's messagehandlers is safe, because BLoopers are automatically locked whenhandling a message. Any other operations (which run from a differentthread) must first lock the looper if they access the list ofendpoints or certain BMidiEndpoint attributes (name, properties,etc).- What if you obtain a BMidiEndpoint object from FindEndpoint() and atthe same time the BMidiRosterLooper receives an 'mDEL' request todelete that endpoint? FindEndpoint() locks the looper, and bumps theendpoint object before giving it to you. Now the looper sees that theendpoint's refcount is larger than 0, so it won't delete it (althoughit will remove the endpoint from its internal list). What if youAcquire() or Release() a remote endpoint while it is being deleted bythe looper? That also won't happen, because if you have a pointer tothat endpoint, its refcount is at least 1 and the looper won't deleteit.- It is not safe to use a BMidiEndpoint and/or the BMidiRoster frommore than one client thread at a time; if you want to do that, youshould synchronize access to these objects yourself. The onlyexception is the Spray() functions from BMidiLocalProducer, sincemost 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.- BMidiProducer objects keep a list of consumers they are connected to.This list can be accessed by several threads at a time: the client'sthread, the BMidiRosterLooper thread, and possibly a separate threadthat is spraying MIDI events. We could have locked the producer usingBMidiRosterLooper's lock, but that would freeze everything else whilethe producer is spraying events. Conversely, it would freeze allproducers while the looper is talking to the midi_server. To lockwith a finer granularity, each BMidiProducer has its own BLocker,which is used only to lock the list of connected consumers.Misc remarks~~~~~~~~~~~~- BMidiEndpoint keeps track of its local/remote state with an "isLocal"variable, and whether it is a producer/consumer with "isConsumer". Italso has an "isRegistered" field to remember whether this endpoint isregistered or not. Why not lump all these different states togetherinto one "flags" bitmask? The reason is that isLocal only makes senseto this application, not to others. Also, the values of isLocal andisConsumer never change, but isRegistered does. It made more sense(and clearer code) to separate them out. Finally, isRegistered doesnot need to be protected by a lock, even though it can be accessed bymultiple threads at a time. Reading and writing a bool is atomic, sothis can't get messed up.The messages~~~~~~~~~~~~::Message: Mapp (MSG_REGISTER_APP)BMessenger midi:messengerReply:(no reply)Message: mAPP (MSG_APP_REGISTERED)(no fields)Message: Mnew (MSG_CREATE_ENDPOINT)bool midi:consumerbool midi:registeredchar[] midi:nameBMessage midi:propertiesint32 midi:port (consumer only)int64 midi:latency (consumer only)Reply:int32 midi:resultint32 midi:idMessage: mNEW (MSG_ENPOINT_CREATED)int32 midi:idbool midi:consumerbool midi:registeredchar[] midi:nameBMessage midi:propertiesint32 midi:port (consumer only)int64 midi:latency (consumer only)Message: Mdel (MSG_DELETE_ENDPOINT)int32 midi:idReply:(no reply)Message: Mdie (MSG_PURGE_ENDPOINT)int32 midi:idReply:(no reply)Message: mDEL (MSG_ENDPOINT_DELETED)int32 midi:idMessage: Mchg (MSG_CHANGE_ENDPOINT)int32 midi:idint32 midi:registered (optional)char[] midi:name (optional)int64 midi:latency (optional)BMessage midi:properties (optional)Reply:int32 midi:resultMessage: mCHG (MSG_ENDPOINT_CHANGED)int32 midi:idint32 midi:registered (optional)char[] midi:name (optional)int64 midi:latency (optional)BMessage midi:properties (optional)--------------MIDI events------------ MIDI events are always sent from a BMidiLocalProducer to aBMidiLocalConsumer. Proxy endpoint objects have nothing to do withthis. During its construction, the local consumer creates a kernelport. 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 sendsto the ports of all connected consumers.- This means that the Midi Kit considers MIDI messages as discreteevents. Hardware drivers chop the stream of incoming MIDI data intoseparate 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.- Each BMidiLocalConsumer has a (realtime priority) thread associatedwith it that waits for data to arrive at the port. As soon as a newMIDI message comes in, the thread examines it and feeds it to theData() hook. The Data() hook ignores the message if the "atomic" flagis false, or passes it on to one of the other hook functionsotherwise. Incoming messages are also ignored if their contents arenot valid; for example, if they have too few or too many bytes for acertain type of MIDI event.- Unlike the consumer, BMidiLocalProducer has no thread of its own. Asa result, spraying MIDI events always happens in the thread of thecaller. Because the consumer port's queue is only 1 message deep,spray functions will block if the consumer thread is already busyhandling another MIDI event. (For this reason, the Midi Kit does notsupport interleaving of real time messages with lower prioritymessages such as sysex dumps, except at the driver level.)- The producer does not just send MIDI event data to the consumer, italso sends a 20-byte header describing the event. The total messagelooks like this:+---------+------------------------------+| 4 bytes | ID of the producer |+---------+------------------------------+| 4 bytes | ID of the consumer |+---------+------------------------------+| 8 bytes | performance time |+---------+------------------------------+| 1 byte | atomic (1 = true, 0 = false) |+---------+------------------------------+| 3 bytes | padding (0) |+---------+------------------------------+| x bytes | MIDI event data |+---------+------------------------------+- In the case of a sysex event, the SystemExclusive() hook is onlycalled 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 lastbyte even when it is not 0xF7. According to the MIDI spec, 0xF7 isnot really required; any non-realtime status byte ends a sysexmessage.- SprayTempoChange() sends 0xFF5103tttttt, where tttttt is60,000,000/bpm. This feature is not really part of the MIDI spec, butan extension from the SMF (Standard MIDI File) format. Of course, theTempoChange() hook is called in response to this message.- The MIDI spec allows for a number of shortcuts. A Note On event withvelocity 0 is supposed to be interpreted as a Note Off, for example.The Midi Kit does not concern itself with these shortcuts. In thiscase, it still calls the NoteOn() hook with a velocity parameter of0.- The purpose of BMidiLocalConsumer's AllNotesOff() function is notentirely 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 sendsan All Notes Off event to all channels, and possible Note Off eventsto all keys on all channels as well. I suspect someone at Be wasconfused by AllNotesOff() being declared "virtual", and thought itwas a hook function. Only that would explain it being inBMidiLocalConsumer as opposed to BMidiLocalProducer, where it wouldhave made sense. The disassembly for Be's libmidi2.so shows thatAllNotesOff() is empty, so to cut a long story short, ourAllNotesOff() simply does nothing and is never invoked either.- There are several types of System Common events, each of which takesa different number of data bytes (0, 1, or 2). ButSpraySystemCommon() and the SystemCommon() hook are always given 2data parameters. The Midi Kit simply ignores the extra data bytes; infact, in our implementation it doesn't even send them. (The Beimplementation always sends 2 data bytes, but that will confuse theMidi Kit if the client does a SprayData() of a common event instead.In our case, that will still invoke the SystemCommon() hook, becausewe are not as easily fooled.)- Handling of timeouts is fairly straightforward. When reading from theport, we specify an absolute timeout. When the port function returnswith a B_TIMED_OUT error code, we call the Timeout() hook. Then wereset the timeout value to -1, which means that timeouts are disabled(until the client calls SetTimeout() again). This design means that acall to SetTimeout() only takes effect the next time we read from theport, i.e. after at least one new MIDI event is received (or theprevious timeout is triggered). Even though BMidiLocalConsumer'stimeout and timeoutData values are accessed by two different threads,I did not bother to protect this. Both values are int32's andreading/writing them should be an atomic operation on most processorsanyway... |image0| image:: midi_server.png.. |image1| image:: libmidi2.png