538 lines
25 KiB
ReStructuredText
538 lines
25 KiB
ReStructuredText
|
Midi Kit design
|
||
|
===============
|
||
|
|
||
|
The Midi Kit consists of the midi_server and two shared libraries,
|
||
|
libmidi2.so and libmidi.so. The latter is the "old" pre-R5 Midi Kit 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.
|
||
|
|
||
|
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.)
|
||
|
|
||
|
--------------
|
||
|
|
||
|
Ooh, pictures
|
||
|
-------------
|
||
|
|
||
|
The following image shows the center of Midi Kit activity, the
|
||
|
midi_server, and its data structures:
|
||
|
|
||
|
|image0|
|
||
|
|
||
|
And here is the picture for libmidi2.so:
|
||
|
|
||
|
|image1|
|
||
|
|
||
|
Note that these diagrams give only a conceptual overview of who is
|
||
|
responsible for which bits of data. The actual implementation details of
|
||
|
the kit may differ.
|
||
|
|
||
|
--------------
|
||
|
|
||
|
Housekeeping
|
||
|
------------
|
||
|
|
||
|
- The design for our implementation of the midi2 "housekeeping"
|
||
|
protocol roughly follows `what Be did <oldprotocol.html>`__, 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 *all* 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 ;-)
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
Initialization
|
||
|
~~~~~~~~~~~~~~
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
Error handling
|
||
|
~~~~~~~~~~~~~~
|
||
|
|
||
|
- 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).
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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 *do* 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.)
|
||
|
|
||
|
- 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).
|
||
|
|
||
|
- 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 *can* kill the app for testing the error
|
||
|
handling on notifications, however.)
|
||
|
|
||
|
Creating and deleting endpoints
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- If you "delete" an endpoint, your app drops into the debugger.
|
||
|
|
||
|
- If you Release() an endpoint too many times, your app *could* 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
Changing endpoint attributes
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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 *did* 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.
|
||
|
|
||
|
- 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).
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.)
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- SetProperties() returns NULL if the message parameter is NULL. It
|
||
|
returns an error code if the endpoint is remote or invalid.
|
||
|
SetProperties() does *not* compare the contents of the new BMessage
|
||
|
to the old, so it will always send out the change request.
|
||
|
|
||
|
Connections
|
||
|
~~~~~~~~~~~
|
||
|
|
||
|
- BMidiProducer::Connect() sends an 'Mcon' request to the midi_server.
|
||
|
This request contains the IDs of the producer and the consumer 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- Connect() and Disconnect() immediately return an error code if you
|
||
|
pass a NULL argument, or if the producer or consumer is invalid.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
Watching
|
||
|
~~~~~~~~
|
||
|
|
||
|
- 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()).
|
||
|
|
||
|
Thread safety
|
||
|
~~~~~~~~~~~~~
|
||
|
|
||
|
- 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).
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
Misc remarks
|
||
|
~~~~~~~~~~~~
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
The messages
|
||
|
~~~~~~~~~~~~
|
||
|
|
||
|
::
|
||
|
|
||
|
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)
|
||
|
|
||
|
--------------
|
||
|
|
||
|
MIDI events
|
||
|
-----------
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- 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.)
|
||
|
|
||
|
- 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:
|
||
|
|
||
|
+---------+------------------------------+
|
||
|
| 4 bytes | ID of the producer |
|
||
|
+---------+------------------------------+
|
||
|
| 4 bytes | ID of the consumer |
|
||
|
+---------+------------------------------+
|
||
|
| 8 bytes | performance time |
|
||
|
+---------+------------------------------+
|
||
|
| 1 byte | atomic (1 = true, 0 = false) |
|
||
|
+---------+------------------------------+
|
||
|
| 3 bytes | padding (0) |
|
||
|
+---------+------------------------------+
|
||
|
| x bytes | MIDI event data |
|
||
|
+---------+------------------------------+
|
||
|
|
||
|
- In the case of a sysex event, the SystemExclusive() hook is 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.
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
- The MIDI spec allows for a number of shortcuts. A Note On event with
|
||
|
velocity 0 is supposed to be interpreted as a Note Off, for example.
|
||
|
The Midi Kit does not concern itself with these shortcuts. In this
|
||
|
case, it still calls the NoteOn() hook with a velocity parameter of
|
||
|
0.
|
||
|
|
||
|
- The purpose of BMidiLocalConsumer's AllNotesOff() function is not
|
||
|
entirely clear. All Notes Off is a so-called "channel mode message"
|
||
|
and is generated by doing a SprayControlChange(channel,
|
||
|
B_ALL_NOTES_OFF, 0). BMidi has an AllNotesOff() function that sends
|
||
|
an All Notes Off event to all channels, and possible Note Off events
|
||
|
to all keys on all channels as well. I suspect someone at Be was
|
||
|
confused by AllNotesOff() being declared "virtual", and thought it
|
||
|
was a hook function. Only that would explain it being in
|
||
|
BMidiLocalConsumer as opposed to BMidiLocalProducer, where it would
|
||
|
have made sense. The disassembly for Be's libmidi2.so shows that
|
||
|
AllNotesOff() is empty, so to cut a long story short, our
|
||
|
AllNotesOff() simply does nothing and is never invoked either.
|
||
|
|
||
|
- 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.)
|
||
|
|
||
|
- 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.
|
||
|
|
||
|
.. |image0| image:: midi_server.png
|
||
|
.. |image1| image:: libmidi2.png
|
||
|
|