How to Use MIDI Stream Callbacks

ID: Q142107


The information in this article applies to:


SUMMARY

Applications running under Windows 95 that use MIDI streams to send events to a MIDI device can use MIDI stream callbacks to be notified of the playback progress at specified times during playback of the stream. This article discusses the following topics regarding MIDI stream callbacks:

A general familiarity with MIDI stream APIs and especially the MIDIEVENT data structure is assumed. For an introduction to MIDI streams, see the "Stream Buffers" and "MIDIEVENT" topics in the Win32 SDK Multimedia documentation.


MORE INFORMATION

Uses of MIDI Stream Callbacks

MIDI stream callbacks can be used to inform the application of playback progress while a MIDI stream is being played. This allows the application to display a graphical indication of the progress of the music playback, or perform some other periodic task in synchronization with the music. MIDI stream callbacks can also be used to synchronize other non-periodic events as well. For example, a MIDI stream-based rendition of the 1812 Overture could include callback events to trigger the playback of a wave file of a cannon fusillade at appropriate times.

How to Generate a MIDI Stream Callback

An application specifies that a MIDI event within a MIDI stream be used to generate a callback by setting the MEVT_F_CALLBACK flag in the dwEvent parameter of the MIDIEVENT structure before the stream is sent to the MIDI device with midiStreamOut(). There are several kinds of short events and long events, and any of these may be used to generate a callback. The following is an example of a short MIDI event with callback:


    MIDIEVENT me;
    me.dwDeltaTime = 48;  // play 48 ticks after preceding event
    me.dwStreamID = 0;    // must be 0
    me.dwEvent = MEVT_F_SHORT | MEVT_F_CALLBACK | // flags
   (((DWORD)MEVT_SHORTMSG) << 24) | // event code
        (DWORD) 0x90 |                   // MIDI note-on status byte
        (((DWORD) 0x3C) << 8) |          // middle C
        (((DWORD) 0x7F) << 16);          // maximum key velocity 


Note that the flags and the event code must all be coded into the high byte of dwEvent, but only the flags are defined in Mmsystem.h as DWORD values with bits set in the high byte. The event codes such as MEVT_SHORTMSG are defined as BYTE values, so it is necessary to left-shift them to the high byte position. Similarly, two of the three bytes describing the event data are shifted to their appropriate positions in the lower 24 bits of dwEvent.

If a callback is desired at a time for which there are no MIDI events in the stream, a special event can be added that does not play back MIDI data but causes a callback to occur by using the MEVT_NOP "no-op" code. In the following example, the MIDI data is not played back through the MIDI output device, but a pointer is sent to the application in a callback:


    MIDIEVENT me;
    me.dwDeltaTime = 48;
    me.dwStreamID = 0;
    me.dwEvent = MEVT_F_SHORT | MEVT_F_CALLBACK |
        (((DWORD)MEVT_NOP) << 24) |
        (DWORD) 0x90 |
        (((DWORD) 0x3C) << 8) |
        (((DWORD) 0x7F) << 16); 


Simultaneous callback events are not meaningful and there are practical limitations to spacing callbacks close together in time. These limitations are described in detail later in this article.

Determining Resolution of Periodic MIDI Stream Callbacks

Periodic callbacks can be generated by tagging appropriate events with a callback, and in cases where there are no events at the required times, inserting MEVT_NOP events with callbacks, as discussed previously. Note that there is no API for generating periodic MIDI stream callbacks similar to the timeSetEvent API for generating periodic multimedia timer callbacks. Instead, each callback event must be individually specified by a MIDIEVENT data structure in the MIDI stream buffer.

You should choose a time period (dwDeltaTime) that provides sufficient resolution for your application's needs but does not impose too great a performance penalty. While the optimum period will depend on many factors, it is probably not realistic to try for a period shorter than something on the order of 25 milliseconds. Also, by allowing a tolerance on this period, you can tag existing events with a callback that are not exactly on time, but sufficiently close to being on time that it will not be noticed by the user. As a result, many fewer MEVT_NOP events with callbacks must be inserted, which will result in more efficient playback performance. The amount of tolerance to allow depends on a number of factors, but for a period of 25 milliseconds, a tolerance of 10 milliseconds would be a reasonable value to try.

Although there isn't a set hard-and-fast limit on the minimum effective callback period, the 25 milliseconds figure reflects realistic design limitations. You don't want to burn processor time trying to be more accurate than the user can possibly distinguish. And equally so, you don't want to burn processor time at the cost of application performance. Because MIDI stream callbacks result in system overhead which could potentially impact MIDI playback performance, callbacks should not be scheduled more frequently than absolutely necessary.

Processing MIDI Stream Callbacks

For each MIDI stream callback event, a MOM_POSITIONCB message is sent to the application's callback procedure. Along with this message, the callback procedure receives a pointer to a MIDIHDR structure, the dwOffset member of which specifies the offset into the MIDI stream buffer in bytes for the callback event most recently processed. Using this offset, the application can access the event data associated with the callback event to learn which event has occurred and perform any necessary processing.

The callback function used to process MIDI stream callbacks is similar to other MIDI callback functions. The following is an example of a MIDI stream callback function:


void CALLBACK MidiStreamProc(
    HMIDIOUT hmo,     // handle of MIDI output device
    UINT uMsg,        // MIDI message
    DWORD dwInstance, // callback instance data
    DWORD dwParam1,   // address of MIDI header
    DWORD dwParam2)   // unused parameter
{
    LPMIDIHDR lpMIDIHeader;
    MIDIEVENT *lpMIDIEvent;

    if (!dwParam1)
        return;    // nothing to do

    switch (uMsg)
    {
        case MOM_POSITIONCB:
            // assign address of MIDIHDR
            lpMIDIHeader = (LPMIDIHDR)dwParam1;

            // get address of event data
            lpMIDIEvent = (MIDIEVENT *)&(lpMIDIHeader->
                lpData[lpMIDIHeader->dwOffset]);

            // do something here with event data

            break;

        // handle these messages if desired
        case MOM_OPEN:
        case MOM_DONE:
        case MOM_CLOSE:
            break;
    }
} 


Behavior of MIDI Stream Callbacks in Real-time

Look at the MIDI stream callback as a request by the application to be called after a specific event in the stream is reached during playback. Once the callback is delivered, the event referred to by the callback is always the absolutely most recent event processed that has the callback flag set. If the callbacks are far apart and the system is running well, this will usually be the same event that triggered the callback, but it doesn't have to be. Stated another way, if another event with the callback flag set has been processed by the time the callback is delivered, then the event referred to by the callback will be the most recently processed callback event, not the event that originally triggered the callback.

The dwOffset parameter of the MIDIHDR received by the callback function tells the application how far playback has travelled in the stream at the time the callback is finally given to the application. For callback events that are scheduled to occur at the same time, the same offset referring to the most recently processed callback event will be received with each of the callbacks. Therefore, the use of simultaneous callback events serves no useful purpose and results in unnecessary overhead, so it should be avoided. Similarly, if callback events are not scheduled simultaneously but are scheduled too closely together, the callbacks will also refer to the most recently processed callback event, so this situation should also be avoided.

When a callback is received by the application, it should use the dwOffset data to decide what to do. If it is expecting a callback at a particular location but gets a callback for somewhere further along in the stream instead, it means that the application is not keeping up or that the callbacks are scheduled too closely together for the capabilities of the machine. The best thing to do in this case is to just throw the callback away because there will be another one in the queue that will be delivered just as soon as processing of the current callback is completed. This may add a little complexity to the logic required to process the callbacks but not to any great extent, and it has the benefit of degrading gracefully on machines that can't keep up.

Additional Notes

Note that MIDI stream support is currently available only on Windows 95. For a complete implementation of a MIDI playback sequencer written with the MIDI stream APIs, see the MIDIPLYR sample in the Windows 95 multimedia samples in the Win32 SDK or the MSTREAM sample in the Games SDK. Although these samples do not use MIDI stream callbacks, they both demonstrate use of MIDI streams in general.

Additional query words: 4.00 MEVT_EVENTPARM MEVT_EVENTTYPE


Keywords          : kbcode kbmm MMMidi 
Version           : 4.00
Platform          : WINDOWS 
Issue type        : 

Last Reviewed: March 6, 1999