2003-03-10 22:23:14 +03:00
|
|
|
<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>
|
|
|
|
|
2003-03-18 01:21:10 +03:00
|
|
|
<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>
|
2003-03-10 22:23:14 +03:00
|
|
|
|
|
|
|
<LI><P>The MIDI spec allows for a number of shortcuts. A Note On event with
|
2003-03-18 01:21:10 +03:00
|
|
|
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>
|
2003-03-10 22:23:14 +03:00
|
|
|
|
|
|
|
<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
|
2003-03-18 01:21:10 +03:00
|
|
|
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>
|
2003-03-10 22:23:14 +03:00
|
|
|
|
|
|
|
<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>
|