New design docs
git-svn-id: file:///srv/svn/repos/haiku/trunk/current@2880 a95241bf-73f2-0310-859d-f6bbb57e9c96
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
|
||||
All of the Midi Kit documentation currently resides
|
||||
on the MIDI team website:
|
||||
|
||||
<http://home.concepts.nl/~hollies/midi>
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
<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() does nothing and TempoChange() is never called, just
|
||||
like with the BeOS R5 Midi Kit. The Midi2Defs.h header defines the symbol
|
||||
B_TEMPO_CHANGE, which is equal to "general purpose controller 6". In theory,
|
||||
SprayTempoChange() would send a Control Change event for the B_TEMPO_CHANGE
|
||||
controller, and TempoChange() would be invoked in response of such an event.
|
||||
But the R5 Midi Kit doesn't do this, and neither do we.</P></LI>
|
||||
|
||||
<LI><P>The MIDI spec allows for a number of shortcuts. A Note On event with
|
||||
velocity 0 is to 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). One would
|
||||
assume that the AllNotesOff() hook is called in response to the receipt of such
|
||||
an event, but no, it is not. What's odd is that the documentation for the midi1
|
||||
kit's BMidi class does not describe this AllNotesOff() as a hook at all, but as
|
||||
a spray function that sends an All Notes Off event to all 16 channels, and
|
||||
optionally sends Note Off events for all notes too. Even stranger, BMidi's
|
||||
AllNotesOff() is declared "virtual", just like all other hooks, and unlike any
|
||||
of the other spray functions. Since AllNotesOff() is also part of
|
||||
BMidiLocalConsumer, it really seems to be a hook function. The disassembly for
|
||||
Be's libmidi2.so shows that AllNotesOff() is empty, which only supports this
|
||||
hypothesis. Somebody at Be must have been very confused when they designed the
|
||||
midi2 kit. To cut a long story short, AllNotesOff(), like TempoChange() does
|
||||
nothing and is never invoked in our implementation.</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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,660 @@
|
||||
<HTML>
|
||||
<BODY>
|
||||
|
||||
<H1>The BeOS R5 Midi Kit protocol</H1>
|
||||
|
||||
<P>In the course of writing the OpenBeOS Midi Kit, I spent some time looking at
|
||||
how BeOS R5's libmidi2.so and midi_server communicate. Not out of a compulsion
|
||||
to clone this protocol, but to learn from it. After all, the Be engineers spent
|
||||
a lot of time thinking about this already, and it would be foolish not to build
|
||||
on their experience. Here is what I have found out.</P>
|
||||
|
||||
<P>Two kinds of communication happen: administrative tasks and MIDI events. The
|
||||
housekeeping stuff is done by sending BMessages between the BMidiRoster and the
|
||||
midi_server. MIDI events are sent between producers and consumers using ports,
|
||||
without intervention from the server.</P>
|
||||
|
||||
<P>This document describes the BMessage protocol. The protocol appears to be
|
||||
asynchronous, which means that when BMidiRoster sends a message to the
|
||||
midi_server, it does not wait around for a reply, even though the midi_server
|
||||
replies to all messages. The libmidi2 functions <I>do</I> block until the reply
|
||||
is received, though, so client code does not have to worry about any of
|
||||
this.</P>
|
||||
|
||||
<P>Both BMidiRoster and the midi_server can initiate messages. BMidiRoster
|
||||
typically sends a message when client code calls one of the functions from a
|
||||
libmidi2 class. When the midi_server sends messages, it is to keep BMidiRoster
|
||||
up-to-date about changes in the roster. BMidiRoster never replies to messages
|
||||
from the server. The encoding of the BMessage 'what' codes indicates their
|
||||
direction. The 'Mxxx' messages are sent from libmidi2 to the midi_server. The
|
||||
'mXXX' messages go the other way around: from the server to a client.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Who does what?</H2>
|
||||
|
||||
<P>The players here are the midi_server, which is a normal BApplication, and
|
||||
all the client apps, also BApplications. The client apps have loaded a copy of
|
||||
libmidi2 into their own address space. The main class from libmidi2 is
|
||||
BMidiRoster. The BMidiRoster has a BLooper that communicates with the
|
||||
midi_server's BLooper.</P>
|
||||
|
||||
<P>The midi_server keeps a list of <I>all</I> endpoints in the system, even
|
||||
local, nonpublished, ones. Each BMidiRoster instance keeps its own list of
|
||||
remote published endpoints, and all endpoints local to this application. It
|
||||
does not know about remote endpoints that are not published yet.</P>
|
||||
|
||||
<P>Whenever you make a change to one of your own endpoints, your BMidiRoster
|
||||
notifies the midi_server. If your endpoint is published, the midi_server then
|
||||
notifies all of the other BMidiRosters, so they can update their local rosters.
|
||||
It does <I>not</I> notify your own app! (Sometimes, however, the midi_server
|
||||
also notifies everyone else even if your local endpoint is <I>not</I>
|
||||
published. The reason for this escapes me, because the other BMidiRosters have
|
||||
no access to those endpoints anyway.)</P>
|
||||
|
||||
<P>By the way, "notification" here means the internal communications between
|
||||
server and libmidi, not the B_MIDI_EVENT messages you receive when you call
|
||||
BMidiRoster::StartWatching().</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiRoster::MidiRoster()</H2>
|
||||
|
||||
<P>The first time it is called, this function creates the one-and-only instance
|
||||
of BMidiRoster. Even if you don't explicitly call it yourself, it is used
|
||||
behind-the-scenes anyway by any of the other BMidiRoster functions.
|
||||
MidiRoster() constructs a BLooper and gets it running. Then it sends a
|
||||
BMessenger with the looper's address to the midi_server:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mapp (0x4d617070, or 1298231408)
|
||||
entry be:msngr, type='MSNG', c=1, size=24,
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The server now responds with mOBJ messages for all <I>remote</I>
|
||||
<I>published</I> producers and consumers. (Obviously, this list only contains
|
||||
remote objects because by now you can't have created any local endpoints
|
||||
yet.)</P>
|
||||
|
||||
<P>For a consumer this message looks like:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mOBJ (0x6d4f424a, or 1833910858)
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x1 (1, '')
|
||||
entry be:latency, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry be:port, type='LONG', c=1, size= 4, data[0]: 0x1dab (7595, '')
|
||||
entry be:name, type='CSTR', c=1, size=16, data[0]: "/dev/midi/vbus0"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>(Oddness: why is be:latency a LONG and not a LLNG? Since latency is
|
||||
expressed in microseconds using a 64-bit bigtime_t, you'd expect the
|
||||
midi_server to send all 64 of those bits... In the 'Mnew' message, on the other
|
||||
hand, be:latency <I>is</I> a LLGN.)</P>
|
||||
|
||||
<P>And for a producer:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mOBJ (0x6d4f424a, or 1833910858)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x2 (2, '')
|
||||
entry be:name, type='CSTR', c=1, size=16, data[0]: "/dev/midi/vbus0"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Note that the be:name field is not present if the endpoint has no name. That
|
||||
is, if the endpoint was constructed by passing a NULL name into the
|
||||
BMidiLocalConsumer() or BMidiLocalProducer() constructor.</P>
|
||||
|
||||
<P>Next up are notifications for <I>all</I> connections, even those between
|
||||
endpoints that are not registered:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mCON (0x6d434f4e, or 1833127758)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x13 (19, '')
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x14 (20, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>These messages are followed by an Msyn message:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = Msyn (0x4d73796e, or 1299413358)
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>And finally the (asynchronous) reply:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Only after this reply is received, MidiRoster() returns.</P>
|
||||
|
||||
<P>The purpose of the Msyn message is not entirely clear. (Without it, Be's
|
||||
libmidi2 blocks in the MidiRoster() call.) Does it signify the end of the list
|
||||
of endpoints? Why doesn't libmidi2 simply wait for the final reply?</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiLocalProducer constructor</H2>
|
||||
|
||||
<P>BMidiRoster, on behalf of the constructor, sends the following to the
|
||||
midi_server:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mnew (0x4d6e6577, or 1299080567)
|
||||
entry be:type, type='CSTR', c=1, size=9, data[0]: "producer"
|
||||
entry be:name, type='CSTR', c=1, size=21, data[0]: "MIDI Keyboard output"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The be:name field is optional.</P>
|
||||
|
||||
<P>The reply includes the ID for the new endpoint. This means that the
|
||||
midi_server assigns the IDs, and any endpoint gets an ID whether it is
|
||||
published or not.</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x11 (17, '')
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Unlike many other Be API classes, BMidiLocalProducer and BMidiLocalConsumer
|
||||
don't have an InitCheck() method. But under certain odd circumstances (such as
|
||||
the midi_server not running), creating the endpoint might fail. How does client
|
||||
code check for that? Well, it turns out that upon failure, the endpoint is
|
||||
assigned ID 0, so you can check for that. In that case, the endpoint's refcount
|
||||
is 0 and you should not Release() it. (That is stupid, actually, because
|
||||
Release() is the only way that you can destroy the object. Our implementation
|
||||
should bump the endpoint to 1 even on failure!)</P>
|
||||
|
||||
<P>If another app creates a new endpoint, your BMidiRoster is not notified. The
|
||||
remote endpoint is not published yet, so your app is not supposed to see
|
||||
it.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiLocalConsumer constructor</H2>
|
||||
|
||||
<P>This is similar to the BMidiLocalProducer constructor, although the contents
|
||||
of the message differ slightly. Again, be:name is optional.</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mnew (0x4d6e6577, or 1299080567)
|
||||
entry be:type, type='CSTR', c=1, size=9, data[0]: "consumer"
|
||||
entry be:latency, type='LLNG', c=1, size= 8, data[0]: 0x0 (0, '')
|
||||
entry be:port, type='LONG', c=1, size= 4, data[0]: 0x4c0 (1216, '')
|
||||
entry be:name, type='CSTR', c=1, size=13, data[0]: "InternalMIDI"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>And the reply:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x11 (17, '')
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Before it sends the message to the server, the constructor creates a new
|
||||
port with the name "MidiEventPort" and a queue length (capacity) of 1.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::Register()<BR>
|
||||
BMidiRoster::Register()</H2>
|
||||
|
||||
<P>Sends the same message for producers and consumers:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mreg (0x4d726567, or 1299342695)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The reply:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>If you try to Register() an endpoint that is already registered, libmidi2
|
||||
still sends the message. (Which could mean that BMidiRoster does not keep track
|
||||
of this registered state.) The midi_server simply ignores that request, and
|
||||
sends back error code 0 (B_OK). So the API does not flag this as an error.</P>
|
||||
|
||||
<P>If you send an invalid be:id, the midi_server returns error code -1 (General
|
||||
OS Error, B_ERROR). If you try to Register() a remote endpoint, libmidi2
|
||||
immediately returns error code -1, and does not send a message to the
|
||||
server.</P>
|
||||
|
||||
<P>If another app Register()'s a producer, your BMidiRoster receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mOBJ (0x6d4f424a, or 1833910858)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x17 (23, '')
|
||||
entry be:name, type='CSTR', c=1, size=7, data[0]: "a name"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>If the other app registers a consumer, your BMidiRoster
|
||||
receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mOBJ (0x6d4f424a, or 1833910858)
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x19 (25, '')
|
||||
entry be:latency, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry be:port, type='LONG', c=1, size= 4, data[0]: 0xde9 (3561, '')
|
||||
entry be:name, type='CSTR', c=1, size=7, data[0]: "a name"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>These are the same messages you get when your BMidiRoster instance is
|
||||
constructed. In both messages, the be:name field is optional again.</P>
|
||||
|
||||
<P>If the other app Register()'s the endpoint more than once, you still get
|
||||
only one notification. So the midi_server simply ignores that second publish
|
||||
request.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::Unregister()<BR>
|
||||
BMidiRoster::Unregister()</H2>
|
||||
|
||||
<P>Sends the same message for producers and consumers:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Munr (0x4d756e72, or 1299541618)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The reply:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>If you try to Unregister() and endpoint that is already unregistered,
|
||||
libmidi2 still sends the message. The midi_server simply ignores that request,
|
||||
and sends back error code 0 (B_OK). So the API does not flag this as an error.
|
||||
If you try to Unregister() a remote endpoint, libmidi2 immediately returns
|
||||
error code -1, and does not send a message to the server.</P>
|
||||
|
||||
<P>When another app Unregister()'s one of its own endpoints, your BMidiRoster
|
||||
receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mDEL (0x6d44454c, or 1833190732)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17 (23, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>When the other app deletes that endpoint (refcount is now 0) and it is not
|
||||
unregistered yet, your BMidiRoster also receives that mDEL message. Multiple
|
||||
Unregisters() are ignored again by the midi_server.</P>
|
||||
|
||||
<P>If an app quits without properly cleaning up, i.e. it does not Unregister()
|
||||
and Release() its endpoints, then the midi_server's roster contains a stale
|
||||
endpoint. As soon as the midi_server recognizes this (for example, when an
|
||||
application tries to connect that endpoint), it sends all BMidiRosters an mDEL
|
||||
message for this endpoint. (This message is sent whenever the midi_server feels
|
||||
like it, so libmidi2 can receive this message while it is still waiting for a
|
||||
reply to some other message.) If the stale endpoint is still on the roster and
|
||||
you (re)start your app, then you receive an mOBJ message for this endpoint
|
||||
during the startup handshake. A little later you will receive the mDEL.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::Release()</H2>
|
||||
|
||||
<P>Only sends a message if the refcount of local objects (published or not)
|
||||
becomes 0:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mdel (0x4d64656c, or 1298425196)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The corresponding reply:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>If you did not Unregister() a published endpoint before you Release()'d it,
|
||||
no 'Munr' message is sent. Of course, the midi_server is smart enough to
|
||||
realize that this endpoint should be wiped from the roster now. Likewise, if
|
||||
this endpoint is connected to another endpoint, Release() will not send a
|
||||
separate 'Mdis' message, but the server <I>will</I> disconnect them. (This, of
|
||||
course, only happens when you Release() local objects. Releasing a proxy has no
|
||||
impact on the connection with the real endpoint.)</P>
|
||||
|
||||
<P>When you Release() a proxy (a remote endpoint) and its refcount becomes 0,
|
||||
libmidi2 does not send an 'Mdel' message to the server. After all, the object
|
||||
is not deleted, just your proxy. If the remote endpoint still exists (i.e.
|
||||
IsValid() returns true), the BMidiRoster actually keeps a cached copy of the
|
||||
proxy object around, just in case you need it again. This means you can do
|
||||
this: endp = NextEndpoint(); endp->Release(); (now refcount is 0) endp-
|
||||
>Acquire(); (now refcount is 1 again). But I advice against that since it
|
||||
doesn't work for all objects; local and dead remote endpoints <I>will</I> be
|
||||
deleted when their refcount reaches zero.</P>
|
||||
|
||||
<P>In Be's implementation, if you Release() a local endpoint that already has a
|
||||
zero refcount, libmidi still sends out the 'Mdel' message. It also drops you
|
||||
into the debugger. (I think it should return an error code instead, it already
|
||||
has a status_t.) However, if you Release() proxies a few times too many, your
|
||||
app does not jump into the debugger. (Again, I think the return result should
|
||||
be an error code here -- for OpenBeOS R1 I think we should jump into the
|
||||
debugger just like with local objects). Hmm, actually, whether you end up in
|
||||
the debugger depends on the contents of memory after the object is deleted,
|
||||
because you perform the extra Release() on a dead object. Don't do that.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::SetName()</H2>
|
||||
|
||||
<P>For local endpoints, both unpublished and published, libmidi2 sends:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mnam (0x4d6e616d, or 1299079533)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
entry be:name, type='CSTR', c=1, size=7, data[0]: "b name"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>And receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>You cannot rename remote endpoints. If you try, libmidi2 will simply ignore
|
||||
your request. It does not send a message to the midi_server.</P>
|
||||
|
||||
<P>If another application renames one of its own endpoints, all other
|
||||
BMidiRosters receive:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mREN (0x6d52454e, or 1834108238)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x5 (5, '')
|
||||
entry be:name, type='CSTR', c=1, size=7, data[0]: "b name"
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>You receive this message even if the other app did not publish its endpoint.
|
||||
This seems rather strange, because your BMidiRoster has no knowledge of this
|
||||
particular endpoint yet, so what is it to do with this message? Ignore it, I
|
||||
guess.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::GetProperties()</H2>
|
||||
|
||||
<P>For <I>any</I> kind of endpoint (local non-published, local published,
|
||||
remote) libmidi2 sends the following message to the server:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mgpr (0x4d677072, or 1298624626)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x2b2 (690, '')
|
||||
entry be:props, type='MSGG', c=1, size= 0,
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>(Why this "get properties" request includes a BMessage is a mistery to me.
|
||||
The midi_server does not appear to copy its contents into the reply, which
|
||||
would have made at least some sense. The BMessage from the client is completely
|
||||
overwritten with the endpoint's properties.)</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:props, type='MSGG', c=1, size= 0,
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>This means that endpoint properties are stored in the server only, not
|
||||
inside the BMidiEndpoints, and not by the local BMidiRosters.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiEndpoint::SetProperties()</H2>
|
||||
|
||||
<P>For local endpoints, published or not, libmidi2 sends the following message
|
||||
to the server:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mspr (0x4d737072, or 1299411058)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
entry be:props, type='MSGG', c=1, size= 0,
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>And expects this back:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>You cannot change the properties of remote endpoints. If you try, libmidi2
|
||||
will ignore your request. It does not send a message to the midi_server, and it
|
||||
returns the -1 error code (B_ERROR).</P>
|
||||
|
||||
<P>If another application changes the properties of one of its own endpoints,
|
||||
all other BMidiRosters receive:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mPRP (0x6d505250, or 1833980496)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x13 (19, '')
|
||||
entry be:properties, type='MSGG', c=1, size= 0,
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>You receive this message even if the other app did not publish its
|
||||
endpoint.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiLocalConsumer::SetLatency()</H2>
|
||||
|
||||
<P>For local endpoints, published or not, libmidi2 sends the following message
|
||||
to the server:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mlat (0x4d6c6174, or 1298948468)
|
||||
entry be:latency, type='LLNG', c=1, size= 8, data[0]: 0x3e8 (1000, '')
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x14f (335, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>And receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>If another application changes the latency of one of its own consumers, all
|
||||
other BMidiRosters receive:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mLAT (0x6d4c4154, or 1833714004)
|
||||
entry be:id, type='LONG', c=1, size= 4, data[0]: 0x15 (21, '')
|
||||
entry be:latency, type='LLNG', c=1, size= 8, data[0]: 0x3e8 (1000, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>You receive this message even if the other app did not publish its
|
||||
endpoint.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiProducer::Connect()</H2>
|
||||
|
||||
<P>The message:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mcon (0x4d636f6e, or 1298362222)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x376 (886, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The answer:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The server sends back a B_ERROR result if you specify wrong ID's. When you
|
||||
try to connect a producer and consumer that are already connected to each
|
||||
other, libmidi2 still sends the 'Mcon' message to the server (even though it
|
||||
could have known these endpoints are already connected). In that case, the
|
||||
server responds with a B_ERROR code as well.</P>
|
||||
|
||||
<P>When another app makes the connection, your BMidiRoster receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mCON (0x6d434f4e, or 1833127758)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x13 (19, '')
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x14 (20, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Note: your BMidiRoster receives this notification even if the producer or
|
||||
the consumer (or both) are not registered endpoints.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>BMidiProducer::Disconnect()</H2>
|
||||
|
||||
<P>The message:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
OUT BMessage: what = Mdis (0x4d646973, or 1298426227)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x309 (777, '')
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x393 (915, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The answer:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = (0x0, or 0)
|
||||
entry be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
|
||||
entry _previous_, ...
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>The server sends back a B_ERROR result if you specify wrong ID's. When you
|
||||
try to disconnect a producer and consumer that are not connected to each other,
|
||||
libmidi2 still sends the 'Mdis' message to the server (even though it could
|
||||
have known these endpoints are not connected). In that case, the server
|
||||
responds with a B_ERROR code as well.</P>
|
||||
|
||||
<P>When another app breaks the connection, your BMidiRoster receives:</P>
|
||||
|
||||
<PRE><SMALL>
|
||||
IN BMessage: what = mDIS (0x6d444953, or 1833191763)
|
||||
entry be:producer, type='LONG', c=1, size= 4, data[0]: 0x13 (19, '')
|
||||
entry be:consumer, type='LONG', c=1, size= 4, data[0]: 0x14 (20, '')
|
||||
</SMALL></PRE>
|
||||
|
||||
<P>Note: your BMidiRoster receives this notification even if the producer or
|
||||
the consumer (or both) are not registered endpoints.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Watchin'</H2>
|
||||
|
||||
<P>BMidiRoster::StartWatching() and StopWatching() do not send messages to the
|
||||
midi_server. This means that the BMidiRoster itself, and not the midi_server,
|
||||
sends the notifications to the messenger. It does this whenever it receives a
|
||||
message from the midi_server.</P>
|
||||
|
||||
<P>The relationship between midi_server messages and B_MIDI_EVENT notifications
|
||||
is as follows:</P>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
<TABLE BORDER="1">
|
||||
<TR><TH>message</TH><TH>notification</TH></TR>
|
||||
<TR><TD>mOBJ</TD><TD>B_MIDI_REGISTERED</TD></TR>
|
||||
<TR><TD>mDEL</TD><TD> B_MIDI_UNREGISTERED </TD></TR>
|
||||
<TR><TD>mCON</TD><TD>B_MIDI_CONNECTED</TD></TR>
|
||||
<TR><TD>mDIS</TD><TD>B_MIDI_DISCONNECTED</TD></TR>
|
||||
<TR><TD>mREN</TD><TD>B_MIDI_CHANGED_NAME</TD></TR>
|
||||
<TR><TD>mLAT</TD><TD>B_MIDI_CHANGED_LATENCY</TD></TR>
|
||||
<TR><TD>mPRP</TD><TD>B_MIDI_CHANGED_PROPERTIES</TD></TR>
|
||||
</TABLE>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
<P>For each message on the left, the watcher will receive the corresponding
|
||||
notification on the right.</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Other observations</H2>
|
||||
|
||||
<P>Operations that do not send messages to the midi_server:</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>BMidiEndpoint::Acquire(). This means reference counting is done locally
|
||||
by BMidiRoster. Release() doesn't send a message either, unless the refcount
|
||||
becomes 0 and the object is deleted. (Which suggests that it is actually the
|
||||
destructor and not Release() that sends the message.)</P></LI>
|
||||
|
||||
<LI><P>BMidiRoster::NextEndpoint(), NextProducer(), NextConsumer(),
|
||||
FindEndpoint(), FindProducer(), FindConsumer(). None of these functions send
|
||||
messages to the midi_server. This means that each BMidiRoster instance keeps
|
||||
its own list of available endpoints. This is why it receives 'mOBJ' messages
|
||||
during the startup handshake, and whenever a new remote endpoint is registered,
|
||||
and 'mDEL' messages for every endpoint that disappears. Even though the
|
||||
NextXXX() functions do not return locally created objects, this "local roster"
|
||||
<I>does</I> keep track of them, since FindXXX() <I>do</I> return local
|
||||
endpoints.</P></LI>
|
||||
|
||||
<LI><P>BMidiEndpoint::Name(), ID(), IsProducer(), IsConsumer(), IsRemote(),
|
||||
IsLocal() IsPersistent(). BMidiConsumer::Latency().
|
||||
BMidiLocalConsumer::GetProducerID(), SetTimeout(). These all appear to consult
|
||||
BMidiRoster's local roster.</P></LI>
|
||||
|
||||
<LI><P>BMidiEndpoint::IsValid(). This function simply looks at BMidiRoster's
|
||||
local roster to see whether the remote endpoint is still visible, i.e. not
|
||||
unregistered. It does not determine whether the endpoint's application is still
|
||||
alive, or "ping" the endpoint or anything fancy like that.</P></LI>
|
||||
|
||||
<LI><P>BMidiProducer::IsConnected(), Connections(). This means that
|
||||
BMidiRoster's local roster, or maybe the BMidiProducers themselves (including
|
||||
the proxies) keep track of the various connections.</P></LI>
|
||||
|
||||
<LI><P>BMidiLocalProducer::Connected(), Disconnected(). These methods are
|
||||
invoked when any app (including your own) makes or breaks a connection on one
|
||||
of your local producers. These hooks are invoked before the B_MIDI_EVENT
|
||||
messages are sent to any watchers.</P></LI>
|
||||
|
||||
<LI><P>Quitting your app. Even though the BMidiRoster instance is deleted when
|
||||
the app quits, it does not let the midi_server know that the application in
|
||||
question is now gone. Any endpoints you have registered are not automatically
|
||||
unregistered. This means that the midi_server is left with some stale
|
||||
information. Undoubtedly, there is a mechanism in place to clean this up. The
|
||||
same mechanism would be used to clean up apps that did not exit cleanly, or
|
||||
that crashed.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<P>Other stuff:</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>libmidi2.so exports an int32 symbol called "midi_debug_level". If you
|
||||
set it to a non-zero value, libmidi2 will dump a lot of interesting debug info
|
||||
on stdout. To do this, declare the variable in your app with "extern int32
|
||||
midi_debug_level;", and then set it to some high value later: "midi_debug_level
|
||||
= 0x7FFFFFFF;" Now run your app from a Terminal and watch libmidi2 do its
|
||||
thing.</P></LI>
|
||||
|
||||
<LI><P>libmidi2.so also exports an int32 symbol called
|
||||
"midi_dispatcher_priority". This is the runtime priority of the thread that
|
||||
fields MIDI events to consumers.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
</BODY>
|
||||
</HTML>
|
||||
@@ -0,0 +1,566 @@
|
||||
<HTML>
|
||||
<BODY>
|
||||
|
||||
<H1>Testing the Midi Kit</H1>
|
||||
|
||||
<P>Most of the OpenBeOS source code has unit tests in the current/src/tests
|
||||
directory. I looked into building CppUnit tests for the midi2 kit, but decided
|
||||
that it doesn't really make much sense. Unit tests work best if you can test
|
||||
something in isolation, but in the case of the midi2 kit this is very hard to
|
||||
achieve. Because the classes from libmidi2.so always need to talk to the
|
||||
midi_server, the tests depend on too many external factors. The available
|
||||
endpoints, for example, will differ from system to system. The spray and hook
|
||||
functions are difficult to test this way, too.</P>
|
||||
|
||||
<P>So instead of a CppUnit test suite, here is a list of manual tests that I
|
||||
performed when developing the midi2 kit:</P>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Registering the application</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that calls BMidiRoster::MidiRoster()</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>When a client app starts, it should first receive mNEW notifications for
|
||||
all endpoints in the system (even unregistered remotes), followed by mCON
|
||||
notifications for all connections in the system (even those between two
|
||||
unregistered local endpoints from another app).</P></LI>
|
||||
|
||||
<LI><P>Send invalid Mapp message (without messenger). The midi_server ignores
|
||||
the request, and the client app blocks forever.</P></LI>
|
||||
|
||||
<LI><P>Fake a delivery error for the mNEW notifications and the mAPP reply.
|
||||
(Add a snooze() in the midi_server's OnRegisterApplication(). While it is
|
||||
snoozing, Ctrl-C the client app. Now the server can't deliver the message and
|
||||
will unregister the application again.)</P></LI>
|
||||
|
||||
<LI><P>Kill the server. Start the client app. It should realize that the server
|
||||
is not running, and return from MidiRoster(); it does not block
|
||||
forever.</P></LI>
|
||||
|
||||
<LI><P>Note: The server does not protect against sending two or more Mapp
|
||||
messages; it will add a new app_t object to the roster and it will also send
|
||||
out the mNEW and mCON notifications again.</P></LI>
|
||||
|
||||
<LI><P>Verify that when the client app quits, the BMidiRoster instance is
|
||||
destroyed by the BMidiRosterKiller. The BMidiRosterLooper is also destroyed,
|
||||
along with any endpoint objects from its list. We don't destroy endpoints with
|
||||
a refcount > 0, but print a warning message on stderr instead.</P></LI>
|
||||
|
||||
<LI><P>When the app quits before it has created a BMidiRoster instance, the
|
||||
BMidiRosterKiller should do nothing.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Creating endpoints</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that creates a new BMidiLocalProducer and/or
|
||||
BMidiLocalConsumer</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Send invalid Mnew message (missing fields). The server will return an
|
||||
error code.</P></LI>
|
||||
|
||||
<LI><P>Don't send reply from midi_server. The client receives a B_NO_REPLY
|
||||
error.</P></LI>
|
||||
|
||||
<LI><P>If something goes wrong creating a new local endpoint, you still get a
|
||||
new BMidiEndpoint object (but it is not added to BMidiRosterLooper's internal
|
||||
list of endpoints). Verify that its ID() function returns 0, and IsValid()
|
||||
returns false. Verify that you can Release() it without crashing into the
|
||||
debugger (i.e. the reference count of the new object should be 1).</P></LI>
|
||||
|
||||
<LI><P>Snooze in midi_server's OnCreateEndpoint() before sending reply to
|
||||
client to simulate heavy processor load. Client should timeout. When done
|
||||
snoozing, server fails to deliver the reply because the client is no longer
|
||||
listening, and it unregisters the app.</P></LI>
|
||||
|
||||
<LI><P>Note: if you kill the client app with Ctrl-C before the server has sent
|
||||
its reply, SendReply() still returns okay, and the midi_server adds the
|
||||
endpoint, even though the corresponding app is dead. There is not much we can
|
||||
do to prevent that (but it is not really a big deal).</P></LI>
|
||||
|
||||
<LI><P>Start the test app from two different Terminals. Verify that the new
|
||||
local endpoint of app1 is added to the BMidiRosterLooper's list of endpoints,
|
||||
and that its "isLocal" flag is true. Verify that when you start the second app,
|
||||
it immediately receives mNEW notifications for the first app's endpoints. It
|
||||
should also create BMidiEndpoint proxy objects for these endpoints with
|
||||
"isLocal" set to false, and add them its own list. Vice versa for the endpoints
|
||||
that app2 creates. Verify that the "registered" field in the mNEW notification
|
||||
is false, because newly created endpoints are not registered yet. The
|
||||
"properties" field should contain an empty message.</P></LI>
|
||||
|
||||
<LI><P>Start server. Start client app. The app makes new endpoints and the
|
||||
server adds them to the roster. Ctrl-C the app. Start client app again. The new
|
||||
client first receives mNEW notifications for the old app's endpoints. When the
|
||||
new app tries to create its own endpoints, the server realizes that the old app
|
||||
is dead, and sends mDEL notifications for the now-defunct endpoints.</P></LI>
|
||||
|
||||
<LI><P>The test app should now create 2 endpoints. Let the midi_server snooze
|
||||
during the second create message, so the app times out. The server now
|
||||
unregisters the app and purges its first endpoint (which was successfully
|
||||
created).</P></LI>
|
||||
|
||||
<LI><P>The test app should now create 3 endpoints. Let the midi_server snooze
|
||||
during the second create message, so the app times out. (It also times out when
|
||||
sending the create request for the 3rd endpoint, because the server is still
|
||||
snoozing.) Because it cannot send a reply for the 2nd create message, the
|
||||
server now unregisters the app and purges its first endpoint (which was
|
||||
successfully created). Then it processes the create request for the 3rd
|
||||
endpoint, but ignores it because the app is now no longer registered with the
|
||||
server.</P></LI>
|
||||
|
||||
<LI><P>Purging endpoints. The test app should now create 2 endpoints. Let the
|
||||
midi_server snooze during the _fourth_ create message. Run the server. Run the
|
||||
test app. Run the test app again in a second Terminal. The server times out,
|
||||
and unregisters the second app. The first app should receive an mDEL
|
||||
notification. Repeat, but now the test app should make 3 endpoints and the
|
||||
server fails on the _sixth_ endpoint. The first app now receives 2 mDEL
|
||||
notifications.</P></LI>
|
||||
|
||||
<LI><P>You should be allowed to pass NULL into the BMidiLocalProducer and
|
||||
BMidiLocalConsumer constructor.</P></LI>
|
||||
|
||||
<LI><P>Let the midi_server assign random IDs to new endpoints; the
|
||||
BMidiRosterLooper should sort the endpoints by their IDs when it adds them to
|
||||
its internal list.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Deleting endpoints</H2>
|
||||
|
||||
<P><I>Required:</I> client app that creates one or more endpoints and
|
||||
Release()'s them</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Verify that Acquire() increments the endpoint's refcount and Release()
|
||||
decrements it. When you Release() a local endpoint so its refcount becomes
|
||||
zero, the client sends an Mdel request to the server. When you Release() a
|
||||
local endpoint too many times, your app jumps into the debugger.</P></LI>
|
||||
|
||||
<LI><P>Send an Mdel request with an invalid ID to the server. Examples of
|
||||
invalid IDs: -1, 0, 1000 (or any other large number).</P></LI>
|
||||
|
||||
<LI><P>Start the test app from two different Terminals. Note that when one of
|
||||
the apps Release()'s its endpoints, the other receives corresponding mDEL
|
||||
notifications.</P></LI>
|
||||
|
||||
<LI><P>Snooze in midi_server's OnCreateEndpoint() before sending reply to
|
||||
"create endpoint" request. The client will timeout and the server will
|
||||
unregister the app. Now have the client Release() the endpoint. This sends a
|
||||
"delete endpoint" request to the server, which ignores the request because the
|
||||
app is no longer registered.</P></LI>
|
||||
|
||||
<LI><P>Override BMidiLocalProducer and BMidiLocalConsumer, and provide a public
|
||||
destructor. Call "delete prod; delete cons;" from your code, instead of using
|
||||
Release(). Your app should drop into the debugger.</P></LI>
|
||||
|
||||
<LI><P>Start the client app and let it make its endpoints. Kill the server.
|
||||
Release() the endpoints. The server doesn't run, so the Mdel request never
|
||||
arrives, but the BMidiEndpoint objects should be deleted regardless.</P></LI>
|
||||
|
||||
<LI><P>Start the test app from two different Terminals, and let them make their
|
||||
endpoints. Quit the apps (using the Deskbar's "Quit Application" menu item).
|
||||
Verify that both clean up and exit correctly. App1 removes its own endpoint
|
||||
from the BMidiRosterLooper's list of endpoints and sends an 'mDEL' message to
|
||||
the server, which passes it on to app2. In response, app2 removes the proxy
|
||||
object from its own list and deletes it. Again, vice versa for the endpoint
|
||||
from app2.</P></LI>
|
||||
|
||||
<LI><P>Start both apps again and wait until they have notified each other about
|
||||
the endpoints. Ctrl-C app1, and restart it. Verify that app1 receives the
|
||||
'mNEW' messages and creates proxies for these remote endpoints. Both apps
|
||||
should receive an 'mDEL' message for app1's old endpoint (because the
|
||||
midi_server realizes it no longer exists and purges it), and remove it from
|
||||
their lists accordingly.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Changing attributes</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that creates an endpoint and calls Register(),
|
||||
Unregister(), SetName(), and SetLatency()</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Send an Mchg request with an invalid ID to the server.</P></LI>
|
||||
|
||||
<LI><P>Register() a local endpoint that is already registered. This does not
|
||||
send a message to the server and always returns B_OK. Likewise for
|
||||
Unregister()ing a local endpoint that is not registered.</P></LI>
|
||||
|
||||
<LI><P>Register() or Unregister() a remote endpoint, or an invalid local
|
||||
endpoint. That should immediately return an error code.</P></LI>
|
||||
|
||||
<LI><P>Verify that BMidiRoster::Register() does the same thing as
|
||||
BMidiEndpoint::Register(). Also for BMidiRoster::Unregister() and
|
||||
BMidiEndpoint::Unregister().</P></LI>
|
||||
|
||||
<LI><P>If you pass NULL into BMidiRoster::Register() or Unregister(), the
|
||||
functions immediately return with an error code.</P></LI>
|
||||
|
||||
<LI><P>SetName() should ignore NULL names. When you call it on a remote
|
||||
endpoint, SetName() should do nothing. SetName() does not send a message if the
|
||||
new name is the same as the current name.</P></LI>
|
||||
|
||||
<LI><P>SetLatency() should ignore 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>Kill the server after making the new endpoint, and call Register(). The
|
||||
client app should return an error code. Also for Unregister(), SetName(),
|
||||
SetLatency(), and SetProperties().</P></LI>
|
||||
|
||||
<LI><P>Snooze in the midi_server's OnChangeEndpoint() before sending the reply
|
||||
to the client. Both sides will flag an error. No mCHG notifications will be
|
||||
sent. The server unregisters the app and purges its endpoints.</P></LI>
|
||||
|
||||
<LI><P>Verify that other apps will receive mCHG notifications when the test app
|
||||
successfully calls Register(), Unregister(), SetName(), and SetLatency(), and
|
||||
that they modify the corresponding BMidiEndpoint objects accordingly. Since
|
||||
clients are never notified when they change their own endpoints, they should
|
||||
ignore the notifications that concern local endpoints. Latency changes should
|
||||
be ignored if the endpoint is not a consumer.</P></LI>
|
||||
|
||||
<LI><P>Send an Mchg request with only the "midi:id" field, so no "midi:name",
|
||||
"midi:registered", "midi:latency", or "midi:properties". The server will still
|
||||
notify the other apps, although they will obviously ignore the notification,
|
||||
because it doesn't contain any useful data.</P></LI>
|
||||
|
||||
<LI><P>The Mchg request is overloaded to change several attributes. Verify that
|
||||
changing one of these attributes, such as the latency, does not overwrite/wipe
|
||||
out the others.</P></LI>
|
||||
|
||||
<LI><P>Start app1. Wait until it has created and registered its endpoint. Start
|
||||
app2. During the initial handshake, app2 should receive an 'mNEW' message for
|
||||
app1's endpoint. Verify that the "refistered" field in this message is already
|
||||
true, and that this is passed on correctly to the new BMidiEndpoint proxy
|
||||
object.</P></LI>
|
||||
|
||||
<LI><P>GetProperties() should return NULL if the message parameter is
|
||||
NULL.</P></LI>
|
||||
|
||||
<LI><P>The properties of new endpoints are empty. Create a new endpoint and
|
||||
call GetProperties(). The BMessage that you receive should contain no
|
||||
fields.</P></LI>
|
||||
|
||||
<LI><P>SetProperties() should return NULL if the message parameter is NULL. It
|
||||
should return an error code if the endpoint is remote or invalid. It should
|
||||
work fine on local endpoints, registered or not. SetProperties() does not
|
||||
compare the contents of the new BMessage to the old, so it will always send out
|
||||
the change request.</P></LI>
|
||||
|
||||
<LI><P>If you Unregister() an endpoint that is connected, the connection should
|
||||
not be broken.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Consulting the roster</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that creates several endpoints, and registers
|
||||
some of them (not all), and uses the BMidiRoster::FindEndpoint() etc functions
|
||||
to examine the roster.</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Verify that FindEndpoint() returns NULL if you pass it:</P>
|
||||
|
||||
<UL>
|
||||
<LI>invalid ID (localOnly = false)</LI>
|
||||
<LI>invalid ID (localOnly = true)</LI>
|
||||
<LI>remote non-registered endpoint (localOnly = false)</LI>
|
||||
<LI>remote non-registered endpoint (localOnly = true)</LI>
|
||||
<LI>remote registered endpoint (localOnly = true)</LI>
|
||||
</UL><BR>
|
||||
|
||||
<P>Verify that FindEndpoint() returns a valid BMidiEndpoint object if you pass
|
||||
it:</P>
|
||||
|
||||
<UL>
|
||||
<LI>local non-registered endpoint (localOnly = false)</LI>
|
||||
<LI>local non-registered endpoint (localOnly = true)</LI>
|
||||
<LI>local registered endpoint (localOnly = false)</LI>
|
||||
<LI>local registered endpoint (localOnly = true)</LI>
|
||||
<LI>remote registered endpoint (localOnly = false)</LI>
|
||||
</UL><BR>
|
||||
|
||||
</LI>
|
||||
|
||||
<LI><P>Verify that FindConsumer() works just like FindEndpoint(), but that it
|
||||
also returns NULL if the endpoint with the specified ID is not a consumer.
|
||||
Likewise for FindProducer().</P></LI>
|
||||
|
||||
<LI><P>Verify that NextEndpoint() returns NULL if you pass it NULL. It also
|
||||
returns NULL if no more endpoints exist. Otherwise, it returns a BMidiEndpoint
|
||||
object, bumps the endpoint's reference count, and sets the "id" parameter to
|
||||
the ID of the endpoint. NextEndpoint() should never return local endpoints
|
||||
(registered or not), nor unregistered remote endpoints. Verify that negative
|
||||
"id" values also work.</P></LI>
|
||||
|
||||
<LI><P>Verify that you can safely call the Find and Next functions without
|
||||
having somehow initialized the BMidiRoster first (by making a new endpoint, for
|
||||
example). The functions themselves should call MidiRoster() and do the
|
||||
handshake with the server.</P></LI>
|
||||
|
||||
<LI><P>The Find and Next functions should bump the reference count of the
|
||||
BMidiEndpoint object that they return. However, they should not (inadvertently)
|
||||
modify the refcounts of any other endpoint objects.</P></LI>
|
||||
|
||||
<LI><P>Get a BMidiEndpoint proxy for a remote published endpoint. Release().
|
||||
Now it should not be removed from the endpoint list or even be deleted, even
|
||||
though its reference count dropped to zero.</P></LI>
|
||||
|
||||
<LI><P>Start app1. Start app2. App2 gets a BMidiEndpoint proxy for a remote
|
||||
endpoint from app1. Ctrl-C app1. Start app1 again. Now app2 receives an mDEL
|
||||
message for app1's old endpoint. Verify that the endpoint is removed from the
|
||||
endpoint list, but not deleted because its reference count isn't zero. If app2
|
||||
now Release()s the endpoint, the BMidiEndpoint object should be deleted. Try
|
||||
again, but now Release() the endpoint before you Ctrl-C; now it should be
|
||||
deleted and removed from the list when you start app1 again.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Making/breaking connections</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that creates a producer and consumer endpoint,
|
||||
optionally registers them, consults the roster for remote endpoints, and makes
|
||||
various kinds of connections.</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Test the following for BMidiProducer::Connect():</P>
|
||||
|
||||
<UL>
|
||||
<LI>Connect(NULL)</LI>
|
||||
<LI>Connect(invalid consumer)</LI>
|
||||
<LI>Connect() using an invalid producer</LI>
|
||||
<LI>Send Mcon request with invalid IDs</LI>
|
||||
<LI>Kill the midi_server just before you Connect()</LI>
|
||||
<LI>Let the midi_server snooze, so the connect request times out</LI>
|
||||
<LI>Have the midi_server return an error result code</LI>
|
||||
<LI>On successful connect, verify that the consumer is added to the producer's
|
||||
list of endpoints</LI>
|
||||
<LI>Verify that you can make connections between 2 local endpoints, a local
|
||||
producer and a remote consumer, a remote producer and a local consumer, and two
|
||||
2 remote endpoints. Test the local endpoints both registered and
|
||||
unregistered.</LI>
|
||||
<LI>2x Connect() on same consumer should give an error</LI>
|
||||
<LI>The other applications should receive an mCON notification, and adjust
|
||||
their own local rosters accordingly</LI>
|
||||
<LI>If you are calling Connect() on a local producer, its Connected() hook
|
||||
should be called. If you are calling Connect() on a remote producer, then its
|
||||
own application should call the Connected() hook.</LI>
|
||||
</UL><BR></LI>
|
||||
|
||||
<LI><P>Test the following for BMidiProducer::Disconnect():</P>
|
||||
|
||||
<UL>
|
||||
<LI>Disconnect(NULL)</LI>
|
||||
<LI>Disconnect(invalid consumer)</LI>
|
||||
<LI>Disconnect() using an invalid producer</LI>
|
||||
<LI>Send Mdis request with invalid IDs</LI>
|
||||
<LI>Kill the midi_server just before you Disconnect()</LI>
|
||||
<LI>Let the midi_server snooze, so the disconnect request times out</LI>
|
||||
<LI>Have the midi_server return an error result code</LI>
|
||||
<LI>On successful disconnect, verify that the consumer is removed from the
|
||||
producer's list of endpoints</LI>
|
||||
<LI>Verify that you can break connections between 2 local endpoints, a local
|
||||
producer and a remote consumer, a remote producer and a local consumer, and two
|
||||
2 remote endpoints. Test the local endpoints both registered and
|
||||
unregistered.</LI>
|
||||
<LI>Disconnecting 2 endpoints that were not connected should give an error</LI>
|
||||
<LI>The other applications should receive an mDIS notification, and adjust
|
||||
their own local rosters accordingly</LI>
|
||||
<LI>If you are calling Disconnect() on a local producer, its Disconnected()
|
||||
hook should be called. If you are calling Disconnect() on a remote producer,
|
||||
then its own application should call the Disconnected() hook.</LI>
|
||||
</UL><BR></LI>
|
||||
|
||||
<LI><P>Make a connection on a local producer. Release() the producer. The other
|
||||
app should only receive an mDEL notification. Likewise if you have a connection
|
||||
with a local consumer and you Release() that. However, now all apps should
|
||||
throw away this consumer from the connection lists, invoking the Disconnected()
|
||||
hook of local producers. The same thing happens if you Ctrl-C the app and
|
||||
restart it. (Now the old endpoints are purged.)</P></LI>
|
||||
|
||||
<LI><P>BMidiProducer::IsConnected() should return false if you pass NULL or an
|
||||
invalid consumer.</P></LI>
|
||||
|
||||
<LI><P>BMidiProducer::Connections() should return a new BList every time you
|
||||
call it. The objects in this list are the BMidiConsumers that are connected to
|
||||
this producer; verify that their reference counts are bumped for every call to
|
||||
Connections().</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Watching</H2>
|
||||
|
||||
<P><I>Required:</I> Client app that creates local consumer and producer
|
||||
endpoints, and calls Register(), Unregister(), SetName(), SetLatency(), and
|
||||
SetProperties(). It should also make and break connections.</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>When you call StartWatching(), you should receive B_MIDI_EVENT
|
||||
notifications for all remote registered endpoints and the connections between
|
||||
them. You will get no notifications for local endpoints, or for any connections
|
||||
that involve unregistered endpoints. The BMidiRosterLooper should make a copy
|
||||
of the BMessenger, so when the client destroys the original messenger, you will
|
||||
still receive notifications. Verify that calling StartWatching() with the same
|
||||
BMessenger twice in a row will also send the initial set of notifications
|
||||
twice. StartWatching(NULL) should be ignored and does not remove the current
|
||||
messenger.</P></LI>
|
||||
|
||||
<LI><P>Run the client app from two different Terminals. Verify that you receive
|
||||
properly formatted B_MIDI_EVENT notifications when the other app changes the
|
||||
attributes of its <I>registered</I> endpoints with the various Set() functions.
|
||||
You should also receive notifications if the app Register()s or Unregister()s
|
||||
its endpoints. That app that makes these changes does not receive the
|
||||
notifications.</P></LI>
|
||||
|
||||
<LI><P>Run the client app from two different Terminals. Verify that you receive
|
||||
properly formatted B_MIDI_EVENT notifications when the apps make and break
|
||||
connections. Every app receives these connection notifications, whether the
|
||||
endpoints are published or not. The app that makes and breaks the connections
|
||||
does not receive any notifications.</P></LI>
|
||||
|
||||
<LI><P>StopWatching() should delete BMidiRosterLooper's BMessenger copy, if
|
||||
any. Verify that you no longer receive B_MIDI_EVENT notifications for remote
|
||||
endpoints after you have called StopWatching().</P></LI>
|
||||
|
||||
<LI><P>If the client is watching, and the BMidiRosterLooper receives an mDEL
|
||||
notification for a registered remote endpoint, it should also send an
|
||||
"unregistered" B_MIDI_EVENT to let the client know that this endpoint is no
|
||||
longer available. If the endpoint was connected to anything, you'll also
|
||||
receive "disconnected" B_MIDI_EVENTs.</P></LI>
|
||||
|
||||
<LI><P>If you get a "registered" event, and you do FindEndpoint() for that id,
|
||||
you'll get its BMidiEndpoint object. If you get an "unregistered" event, then
|
||||
FindEndpoint() returns NULL. So the events are send <I>after</I> the roster is
|
||||
modified.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Event tests</H2>
|
||||
|
||||
<P><I>Required:</I> Several client apps that create and register consumer
|
||||
endpoints that override the various MIDI event hook functions, as well as
|
||||
producer endpoints that spray MIDI events. Also useful is a tool that lets you
|
||||
make connections between all these endpoints (PatchBay), and a tool that lets
|
||||
you monitor the MIDI events (MidiMonitor).</P>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>BMidiLocalProducer's spray functions should only try to send something
|
||||
if there is one or more connected consumer. If the spray functions cannot
|
||||
deliver their events, they simply ignore that consumer until the next spray.
|
||||
(No connections are broken or anything.)</P></LI>
|
||||
|
||||
<LI><P>All spray functions except SprayData() should set the atomic flag to
|
||||
true, even SpraySystemExclusive().</P></LI>
|
||||
|
||||
<LI><P>When you send a sysex message using SpraySystemExclusive(), it should
|
||||
add 0xF0 in front of your data and 0xF7 at the back. When you call SprayData()
|
||||
instead, no bytes are added to the MIDI event data.</P></LI>
|
||||
|
||||
<LI><P>Verify that all events arrive correctly and that the latency is minimal,
|
||||
even when the load is heavy (i.e. many events are being sprayed to many
|
||||
different consumers).</P></LI>
|
||||
|
||||
<LI><P>Verify that the BMidiLocalConsumer destructor properly destroys the
|
||||
corresponding port and event thread before it returns.</P></LI>
|
||||
|
||||
<LI><P>BMidiLocalConsumer should ignore messages that are too small, addressed
|
||||
to another consumer, or otherwise invalid.</P></LI>
|
||||
|
||||
<LI><P>BMidiLocalConsumer's Data() hook should ignore all non-atomic events.
|
||||
The rest of the events, provided they contain the correct number of bytes for
|
||||
that kind of event, are passed on to the other hooks.</P></LI>
|
||||
|
||||
<LI><P>Hook a producer up to a consumer and call all SprayXXX() functions with
|
||||
a variety of arguments to make sure the correct hooks are being called with the
|
||||
correct values. Call SprayData() and SpraySystemExclusive() with NULL data
|
||||
and/or length 0.</P></LI>
|
||||
|
||||
<LI><P>Call GetProducerID() from one of BMidiLocalConsumer's hooks to verify
|
||||
that this indeed returns the ID of the producer that sprayed the
|
||||
event.</P></LI>
|
||||
|
||||
<LI><P>To test timeouts, first call SetTimeout(system_time() + 2000000), spray
|
||||
an event to the consumer, and wait 2 seconds. The consumer's Timeout() hook
|
||||
should now be called. Try again, but now spray multiple events to the consumer.
|
||||
The Timeout() hook should still be called after 2 seconds, measured from the
|
||||
moment the timeout was set. Replace the call to SetTimeout() with
|
||||
SetTimeout(0). After spraying the first event, you should immediately get the
|
||||
Timeout() signal, because the target time was set in the past. Verify that
|
||||
calling SetTimeout() only takes effect after at least one new event has been
|
||||
received.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
<HR SIZE="1">
|
||||
|
||||
<H2>Other tests</H2>
|
||||
|
||||
<UL>
|
||||
|
||||
<LI><P>Kill the server. Now run a client app. It should recognize that the
|
||||
server isn't running, and return error codes on all operations. Also kill the
|
||||
server while the test app is running. From then on, the client app will return
|
||||
error codes on all operations. Also bring it back up again while the test app
|
||||
is still running. Now the client app's request messages will be delivered to
|
||||
the server again, but the server will ignore them, because our app did not
|
||||
register with this new instance of the server.</P></LI>
|
||||
|
||||
<LI><P>Start the midi_server and several client apps. Use PatchBay to make and
|
||||
break a whole bunch of connections. Quit PatchBay. Start it again. Now the same
|
||||
connections should show up. Run similar tests with MidiKeyboard. Also install
|
||||
VirtualMidi (and run the old midi_server for the time being) to get a whole
|
||||
bunch of fake MIDI devices.</P></LI>
|
||||
|
||||
<LI><P><I>Regression bug:</I> After you quit one client app, another app fails
|
||||
to send request to the midi_server.</P>
|
||||
|
||||
<P><I>Required:</I> Client app that creates a new endpoint and registers it. In
|
||||
the app's destructor, it unregisters and releases the endpoint.</P>
|
||||
|
||||
<P><I>How to reproduce:</I> Run the app from two different Terminals. Ctrl-C
|
||||
app1. Start app1 again. From the Deskbar quit both apps at the same time (that
|
||||
is possible because app1 and app2 both have the same signature). When it tries
|
||||
to send the Unregister() request to the midi_server, app2 gives the error
|
||||
"Cannot send msg to server". The error code is "Bad Port ID", which means that
|
||||
the reply port is dead. The Mdel message from Release() is sent without any
|
||||
problems, however, because that expects no reply back. This is not the only way
|
||||
to reproduce the problem, but it seems to be the most reliable one.</P>
|
||||
|
||||
<P>The reason this happens is because you kill app1. When app2 sends a
|
||||
synchronous request to the midi_server, the server re-used that same message to
|
||||
notify the other apps. (Because it already contained all the necessary fields.)
|
||||
But app1 is dead, the notification fails, and this (probably) wipes out the
|
||||
reply address in the message. I changed the midi_server to create new BMessages
|
||||
for the notifications, and was no longer able to reproduce the
|
||||
problem.</P></LI>
|
||||
|
||||
</UL>
|
||||
|
||||
</BODY>
|
||||
</HTML>
|
||||
Reference in New Issue
Block a user