From 3b101c8633ac45b882a33bb914687f998ae2f50a Mon Sep 17 00:00:00 2001 From: mahlzeit Date: Mon, 10 Mar 2003 19:23:14 +0000 Subject: [PATCH] New design docs git-svn-id: file:///srv/svn/repos/haiku/trunk/current@2880 a95241bf-73f2-0310-859d-f6bbb57e9c96 --- docs/develop/midi/ToDo | 6 - docs/develop/midi/design.html | 538 +++++++++++++++++++++++ docs/develop/midi/libmidi2.png | Bin 0 -> 32618 bytes docs/develop/midi/midi_server.png | Bin 0 -> 17838 bytes docs/develop/midi/oldprotocol.html | 660 +++++++++++++++++++++++++++++ docs/develop/midi/testing.html | 566 +++++++++++++++++++++++++ 6 files changed, 1764 insertions(+), 6 deletions(-) delete mode 100644 docs/develop/midi/ToDo create mode 100644 docs/develop/midi/design.html create mode 100644 docs/develop/midi/libmidi2.png create mode 100644 docs/develop/midi/midi_server.png create mode 100644 docs/develop/midi/oldprotocol.html create mode 100644 docs/develop/midi/testing.html diff --git a/docs/develop/midi/ToDo b/docs/develop/midi/ToDo deleted file mode 100644 index 155eb0943c..0000000000 --- a/docs/develop/midi/ToDo +++ /dev/null @@ -1,6 +0,0 @@ - -All of the Midi Kit documentation currently resides -on the MIDI team website: - - - diff --git a/docs/develop/midi/design.html b/docs/develop/midi/design.html new file mode 100644 index 0000000000..92f31f9ccd --- /dev/null +++ b/docs/develop/midi/design.html @@ -0,0 +1,538 @@ + + + +

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:

+ +
+ +

And here is the picture for libmidi2.so:

+ +
+ +

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

+ + + +

Initialization

+ + + +

Error handling

+ + + +

Creating and deleting endpoints

+ + + +

Changing endpoint attributes

+ + + +

Connections

+ + + +

Watching

+ + + +

Thread safety

+ + + +

Misc remarks

+ + + +

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

+ + + + + diff --git a/docs/develop/midi/libmidi2.png b/docs/develop/midi/libmidi2.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7da1ed88e89d11cad5437ba58ab86698c4c6d2 GIT binary patch literal 32618 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�?#GN*-(#W;ypN z%VxAJbdgkd0MxTkQGa0tNt6N0)DGiit#=c&?B}(}uP25m_1?d*pTxT4F%7>Z>GJT8 z37i_9&sT#dsua`*2(&oTY@`~VF$f4~X)5r(^zf)jO-+r6xW0(j-9~Sg8OCGL{~47^ z0e#{_aE{fuZ|v+CDV%Tp_v#h>oOJzdI*5v&{2U)IN0pQc#s%PVTJIE64wu>}mK1|p z4KiP4&TcIbyN4Nd$N)hK*AXo-VpQgK@bH9N9)wMepYN>9tq&FX(%YM4nk^x&Pdddw z1<~K%4~C^x257A2Zi3v~FeY_U8XConfCCzY{m<2^ z(T#|AFaID*NwywID7Fc=Tx4u@ON@eQE`Qw8BT9n(Pjy=4RNsR=zf^`AA=ibQg4sXC z1mE^|K$JZHi>5iQjOf&@@yKOQWbQJ-#R~M_h%1S=Y)m;)A8KWTQtFqptE-8z@k2@J z;?EJVVF3jx>660{)$wWXyF#76$rH!w(klANEP z|BOG)4PVo{_d)mhuvYc5zn*u4(n|e6+~sld(Jv2nR@UX-EV5#h7LHxh&PaDBA)$U# zTKZ|HSnDreB1nRfG8SMq1L&_rqt@dD)TQ<(oR?J}toB)KRxAi(>CI}?q*Q(1*t)g1 zsItAY<5S8Zb=1u@ug$T}c16pif587plj|dER&EjhZJ&a(@)C6A#uxX>wNcfZH{VjB zA32OOdZe}XqCB=6_S2}khGL?IOL{0@2)zl8vhiFdv0vEsyL)6v$JLH3dMpLzOkDcLKJ<)`WwbKktZSLmmbpp!U_jrO z>}}eK8?7K_`d1i^i_7Y4^^pc41@B?sfE5)((I5ObHY+0(wjAs{B7AW*&FpMAA=H8n z6_* z%3Dcax4$lQwakzOo#S4lNOgq9v9r%}hmGR)(owlf!>Lx H@JR49%qOXRM^*vqdXgTERvI(YrgO% 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 l5l9fc!^`ad$4fmC;U9%J-jf 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 + + +

The BeOS R5 Midi Kit protocol

+ +

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.

+ +

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.

+ +

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 do block until the reply +is received, though, so client code does not have to worry about any of +this.

+ +

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.

+ +
+ +

Who does what?

+ +

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.

+ +

The midi_server keeps a list of all 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.

+ +

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 not notify your own app! (Sometimes, however, the midi_server +also notifies everyone else even if your local endpoint is not +published. The reason for this escapes me, because the other BMidiRosters have +no access to those endpoints anyway.)

+ +

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().

+ +
+ +

BMidiRoster::MidiRoster()

+ +

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:

+ +

+OUT BMessage: what = Mapp (0x4d617070, or 1298231408)
+    entry       be:msngr, type='MSNG', c=1, size=24,         
+
+ +

The server now responds with mOBJ messages for all remote +published producers and consumers. (Obviously, this list only contains +remote objects because by now you can't have created any local endpoints +yet.)

+ +

For a consumer this message looks like:

+ +

+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"
+
+ +

(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 is a LLGN.)

+ +

And for a producer:

+ +

+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"
+
+ +

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.

+ +

Next up are notifications for all connections, even those between +endpoints that are not registered:

+ +

+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, '')
+
+ +

These messages are followed by an Msyn message:

+ +

+IN  BMessage: what = Msyn (0x4d73796e, or 1299413358)
+
+ +

And finally the (asynchronous) reply:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

Only after this reply is received, MidiRoster() returns.

+ +

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?

+ +
+ +

BMidiLocalProducer constructor

+ +

BMidiRoster, on behalf of the constructor, sends the following to the +midi_server:

+ +

+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"
+
+ +

The be:name field is optional.

+ +

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.

+ +

+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_, ...
+
+ +

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!)

+ +

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.

+ +
+ +

BMidiLocalConsumer constructor

+ +

This is similar to the BMidiLocalProducer constructor, although the contents +of the message differ slightly. Again, be:name is optional.

+ +

+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"  
+
+ +

And the reply:

+ +

+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_, ...
+
+ +

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.

+ +
+ +

BMidiEndpoint::Register()
+BMidiRoster::Register()

+ +

Sends the same message for producers and consumers:

+ +

+OUT BMessage: what = Mreg (0x4d726567, or 1299342695)
+    entry          be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')     
+
+ +

The reply:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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.

+ +

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.

+ +

If another app Register()'s a producer, your BMidiRoster receives:

+ +

+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"
+
+ +

If the other app registers a consumer, your BMidiRoster +receives:

+ +

+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"
+
+ +

These are the same messages you get when your BMidiRoster instance is +constructed. In both messages, the be:name field is optional again.

+ +

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.

+ +
+ +

BMidiEndpoint::Unregister()
+BMidiRoster::Unregister()

+ +

Sends the same message for producers and consumers:

+ +

+OUT BMessage: what = Munr (0x4d756e72, or 1299541618)
+    entry          be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')       
+
+ +

The reply:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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.

+ +

When another app Unregister()'s one of its own endpoints, your BMidiRoster +receives:

+ +

+IN  BMessage: what = mDEL (0x6d44454c, or 1833190732)
+    entry          be:id, type='LONG', c=1, size= 4, data[0]: 0x17 (23, '')             
+
+ +

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.

+ +

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.

+ +
+ +

BMidiEndpoint::Release()

+ +

Only sends a message if the refcount of local objects (published or not) +becomes 0:

+ +

+OUT BMessage: what = Mdel (0x4d64656c, or 1298425196)
+    entry          be:id, type='LONG', c=1, size= 4, data[0]: 0x17f (383, '')
+
+ +

The corresponding reply:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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 will 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.)

+ +

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 will be +deleted when their refcount reaches zero.

+ +

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.

+ +
+ +

BMidiEndpoint::SetName()

+ +

For local endpoints, both unpublished and published, libmidi2 sends:

+ +

+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"
+
+ +

And receives:

+ +

+IN BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

You cannot rename remote endpoints. If you try, libmidi2 will simply ignore +your request. It does not send a message to the midi_server.

+ +

If another application renames one of its own endpoints, all other +BMidiRosters receive:

+ +

+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"
+
+ +

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.

+ +
+ +

BMidiEndpoint::GetProperties()

+ +

For any kind of endpoint (local non-published, local published, +remote) libmidi2 sends the following message to the server:

+ +

+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,
+
+ +

(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.)

+ +

+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_, ...
+
+ +

This means that endpoint properties are stored in the server only, not +inside the BMidiEndpoints, and not by the local BMidiRosters.

+ +
+ +

BMidiEndpoint::SetProperties()

+ +

For local endpoints, published or not, libmidi2 sends the following message +to the server:

+ +

+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,
+
+ +

And expects this back:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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).

+ +

If another application changes the properties of one of its own endpoints, +all other BMidiRosters receive:

+ +

+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,
+
+ +

You receive this message even if the other app did not publish its +endpoint.

+ +
+ +

BMidiLocalConsumer::SetLatency()

+ +

For local endpoints, published or not, libmidi2 sends the following message +to the server:

+ +

+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, '')
+
+ +

And receives:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

If another application changes the latency of one of its own consumers, all +other BMidiRosters receive:

+ +

+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, '')
+
+ +

You receive this message even if the other app did not publish its +endpoint.

+ +
+ +

BMidiProducer::Connect()

+ +

The message:

+ +

+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, '')
+
+ +

The answer:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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.

+ +

When another app makes the connection, your BMidiRoster receives:

+ +

+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, '')
+
+ +

Note: your BMidiRoster receives this notification even if the producer or +the consumer (or both) are not registered endpoints.

+ +
+ +

BMidiProducer::Disconnect()

+ +

The message:

+ +

+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, '')
+
+ +

The answer:

+ +

+IN  BMessage: what =  (0x0, or 0)
+    entry      be:result, type='LONG', c=1, size= 4, data[0]: 0x0 (0, '')
+    entry     _previous_, ...
+
+ +

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.

+ +

When another app breaks the connection, your BMidiRoster receives:

+ +

+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, '')
+
+ +

Note: your BMidiRoster receives this notification even if the producer or +the consumer (or both) are not registered endpoints.

+ +
+ +

Watchin'

+ +

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.

+ +

The relationship between midi_server messages and B_MIDI_EVENT notifications +is as follows:

+ +
+ + + + + + + + + +
messagenotification
mOBJB_MIDI_REGISTERED
mDEL B_MIDI_UNREGISTERED
mCONB_MIDI_CONNECTED
mDISB_MIDI_DISCONNECTED
mRENB_MIDI_CHANGED_NAME
mLATB_MIDI_CHANGED_LATENCY
mPRPB_MIDI_CHANGED_PROPERTIES
+
+ +

For each message on the left, the watcher will receive the corresponding +notification on the right.

+ +
+ +

Other observations

+ +

Operations that do not send messages to the midi_server:

+ +
    + +
  • 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.)

  • + +
  • 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" +does keep track of them, since FindXXX() do return local +endpoints.

  • + +
  • BMidiEndpoint::Name(), ID(), IsProducer(), IsConsumer(), IsRemote(), +IsLocal() IsPersistent(). BMidiConsumer::Latency(). +BMidiLocalConsumer::GetProducerID(), SetTimeout(). These all appear to consult +BMidiRoster's local roster.

  • + +
  • 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.

  • + +
  • BMidiProducer::IsConnected(), Connections(). This means that +BMidiRoster's local roster, or maybe the BMidiProducers themselves (including +the proxies) keep track of the various connections.

  • + +
  • 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.

  • + +
  • 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.

  • + +
+ +

Other stuff:

+ +
    + +
  • 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.

  • + +
  • 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.

  • + +
+ + + diff --git a/docs/develop/midi/testing.html b/docs/develop/midi/testing.html new file mode 100644 index 0000000000..e6d434708f --- /dev/null +++ b/docs/develop/midi/testing.html @@ -0,0 +1,566 @@ + + + +

Testing the Midi Kit

+ +

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.

+ +

So instead of a CppUnit test suite, here is a list of manual tests that I +performed when developing the midi2 kit:

+ +
+ +

Registering the application

+ +

Required: Client app that calls BMidiRoster::MidiRoster()

+ +
    + +
  • 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).

  • + +
  • Send invalid Mapp message (without messenger). The midi_server ignores +the request, and the client app blocks forever.

  • + +
  • 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.)

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • When the app quits before it has created a BMidiRoster instance, the +BMidiRosterKiller should do nothing.

  • + +
+ +
+ +

Creating endpoints

+ +

Required: Client app that creates a new BMidiLocalProducer and/or +BMidiLocalConsumer

+ +
    + +
  • Send invalid Mnew message (missing fields). The server will return an +error code.

  • + +
  • Don't send reply from midi_server. The client receives a B_NO_REPLY +error.

  • + +
  • 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).

  • + +
  • 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.

  • + +
  • 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).

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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).

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • You should be allowed to pass NULL into the BMidiLocalProducer and +BMidiLocalConsumer constructor.

  • + +
  • 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.

  • + +
+ +
+ +

Deleting endpoints

+ +

Required: client app that creates one or more endpoints and +Release()'s them

+ +
    + +
  • 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.

  • + +
  • Send an Mdel request with an invalid ID to the server. Examples of +invalid IDs: -1, 0, 1000 (or any other large number).

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
+ +
+ +

Changing attributes

+ +

Required: Client app that creates an endpoint and calls Register(), +Unregister(), SetName(), and SetLatency()

+ +
    + +
  • Send an Mchg request with an invalid ID to the server.

  • + +
  • 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.

  • + +
  • Register() or Unregister() a remote endpoint, or an invalid local +endpoint. That should immediately return an error code.

  • + +
  • Verify that BMidiRoster::Register() does the same thing as +BMidiEndpoint::Register(). Also for BMidiRoster::Unregister() and +BMidiEndpoint::Unregister().

  • + +
  • If you pass NULL into BMidiRoster::Register() or Unregister(), the +functions immediately return with an error code.

  • + +
  • 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.

  • + +
  • 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.)

  • + +
  • 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().

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • GetProperties() should return NULL if the message parameter is +NULL.

  • + +
  • The properties of new endpoints are empty. Create a new endpoint and +call GetProperties(). The BMessage that you receive should contain no +fields.

  • + +
  • 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.

  • + +
  • If you Unregister() an endpoint that is connected, the connection should +not be broken.

  • + +
+ +
+ +

Consulting the roster

+ +

Required: Client app that creates several endpoints, and registers +some of them (not all), and uses the BMidiRoster::FindEndpoint() etc functions +to examine the roster.

+ +
    + +
  • Verify that FindEndpoint() returns NULL if you pass it:

    + +
      +
    • invalid ID (localOnly = false)
    • +
    • invalid ID (localOnly = true)
    • +
    • remote non-registered endpoint (localOnly = false)
    • +
    • remote non-registered endpoint (localOnly = true)
    • +
    • remote registered endpoint (localOnly = true)
    • +

    + +

    Verify that FindEndpoint() returns a valid BMidiEndpoint object if you pass +it:

    + +
      +
    • local non-registered endpoint (localOnly = false)
    • +
    • local non-registered endpoint (localOnly = true)
    • +
    • local registered endpoint (localOnly = false)
    • +
    • local registered endpoint (localOnly = true)
    • +
    • remote registered endpoint (localOnly = false)
    • +

    + +
  • + +
  • 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().

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • 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.

  • + +
+ +
+ +

Making/breaking connections

+ +

Required: Client app that creates a producer and consumer endpoint, +optionally registers them, consults the roster for remote endpoints, and makes +various kinds of connections.

+ +
    + +
  • Test the following for BMidiProducer::Connect():

    + +
      +
    • Connect(NULL)
    • +
    • Connect(invalid consumer)
    • +
    • Connect() using an invalid producer
    • +
    • Send Mcon request with invalid IDs
    • +
    • Kill the midi_server just before you Connect()
    • +
    • Let the midi_server snooze, so the connect request times out
    • +
    • Have the midi_server return an error result code
    • +
    • On successful connect, verify that the consumer is added to the producer's +list of endpoints
    • +
    • 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.
    • +
    • 2x Connect() on same consumer should give an error
    • +
    • The other applications should receive an mCON notification, and adjust +their own local rosters accordingly
    • +
    • 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.
    • +

  • + +
  • Test the following for BMidiProducer::Disconnect():

    + +
      +
    • Disconnect(NULL)
    • +
    • Disconnect(invalid consumer)
    • +
    • Disconnect() using an invalid producer
    • +
    • Send Mdis request with invalid IDs
    • +
    • Kill the midi_server just before you Disconnect()
    • +
    • Let the midi_server snooze, so the disconnect request times out
    • +
    • Have the midi_server return an error result code
    • +
    • On successful disconnect, verify that the consumer is removed from the +producer's list of endpoints
    • +
    • 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.
    • +
    • Disconnecting 2 endpoints that were not connected should give an error
    • +
    • The other applications should receive an mDIS notification, and adjust +their own local rosters accordingly
    • +
    • 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.
    • +

  • + +
  • 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.)

  • + +
  • BMidiProducer::IsConnected() should return false if you pass NULL or an +invalid consumer.

  • + +
  • 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().

  • + +
+ +
+ +

Watching

+ +

Required: Client app that creates local consumer and producer +endpoints, and calls Register(), Unregister(), SetName(), SetLatency(), and +SetProperties(). It should also make and break connections.

+ +
    + +
  • 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.

  • + +
  • 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 registered 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.

  • + +
  • 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.

  • + +
  • 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().

  • + +
  • 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.

  • + +
  • 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 after the roster is +modified.

  • + +
+ +
+ +

Event tests

+ +

Required: 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).

+ +
    + +
  • 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.)

  • + +
  • All spray functions except SprayData() should set the atomic flag to +true, even SpraySystemExclusive().

  • + +
  • 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.

  • + +
  • 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).

  • + +
  • Verify that the BMidiLocalConsumer destructor properly destroys the +corresponding port and event thread before it returns.

  • + +
  • BMidiLocalConsumer should ignore messages that are too small, addressed +to another consumer, or otherwise invalid.

  • + +
  • 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.

  • + +
  • 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.

  • + +
  • Call GetProducerID() from one of BMidiLocalConsumer's hooks to verify +that this indeed returns the ID of the producer that sprayed the +event.

  • + +
  • 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.

  • + +
+ +
+ +

Other tests

+ +
    + +
  • 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.

  • + +
  • 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.

  • + +
  • Regression bug: After you quit one client app, another app fails +to send request to the midi_server.

    + +

    Required: Client app that creates a new endpoint and registers it. In +the app's destructor, it unregisters and releases the endpoint.

    + +

    How to reproduce: 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.

    + +

    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.

  • + +
+ + +