MSTREAM
Design Description and Notes
The MSTREAM sample application is an example of how you can use the new
midiStream API that's built into Windows 95 to play MIDI data with low
latency and low processor overhead.
The basic idea behind the implementation is to start the application and
initialize the user interface. After that's done, the following events occur.
When a user selects and opens a file
------------------------------------
Open a MIDI output device (the Microsoft MIDI Mapper in this case)
Open the file using buffered I/O and fill our buffers with data in MIDI stream
format
Prepare and queue the buffers using midiOutPrepareHeader and midiStreamOut
Wait until the user decides to do something else
When PLAY is selected
---------------------
Call midiStreamRestart to un-pause the device and begin playback
As buffers are returned, the callback function fills them with more data and
sends them back to be played by calling midiStreamOut() again. IT IS
IMPORTANT THAT YOU NOTE THAT IT IS NOT NECESSARY FOR YOU TO UNPREPARE AND
PREPARE BUFFERS EVERY TIME THEY ARE RETURNED. This is only a waste of time.
All you have to do is send them back into the subsystem with midiStreamOut().
The API documentation is a bit confusing on this point.
When PAUSE is selected
----------------------
Call midiStreamPause() and wait for a PLAY or PAUSE to call midiStreamRestart().
Note that pausing may make your audio sound kind of funny, since all notes are
turned off and some note on events may be lost when playback is restarted.
When STOP is selected
---------------------
Call midiStreamStop(). Since the callback function is in another thread, we
use a Win32 synchronization object, an event to block the main thread until
the callback thread has received all buffers. When this happens, we know it's
okay to go ahead and call midiStreamReset() and then to go ahead and free all
our buffers. Currently, the device is also closed using midiStreamClose().
Then, if we are resetting the file to playback starting at its beginning point
the next time PLAY is hit, reopen the file by calling StreamBufferSetup(),
which is the workhorse function for opening a file and initializing the
the converter and the buffers.
Note that the reason we must close and reopen the device is due to an apparent
bug in the Multimedia System which causes undesireable playback once a device
has been stopped. If you disable the code for closing the device, then the rest
of the code will automatically know it does not have to reopen the device.
However, the following may occur: After the first instance of playback followed
by a midiStreamStop() and a midiStreamReset(), there will be a pause equal in
length to the first playback period before playback begins once more with a call
to midiStreamRestart().
If Looped is selected
---------------------
Notice that the converter has a little function in it called RewindConverter()
which is called by ConvertToBuffer() if the bLooped variable is TRUE. This
function resets the track state structures and performs most of the steps
originally executed in ConvertInit() with the notable exception of opening the
file and reading in file and track header data. It simply resets the tracks
to their initial state.
More on the buffering scheme
----------------------------
Note that there is an OUT_BUFFER_SIZE and a BUFFER_TIME_LENGTH. The idea is
that the converter counts ticks and calculates when it has put at least
BUFFER_TIME_LENGTH milliseconds worth of events in the buffer. At this point,
it returns to the caller with a "full" buffer. At the very worst, there will
be as many events as can fit into OUT_BUFFER_SIZE bytes. Imagine that you have
to MIDI files loaded into memory somehow and you want to switch between them in
a hurry to correspond to some action the player performed like switching rooms.
All you have to do is start filling the next buffer with new data (assuming
they streams use similar patch sets, time division settings, etc.) and after
(NUM_STREAM_BUFFERS-1)*BUFFER_TIME_LENGTH milliseconds, the music will switch
over automatically. This theory is sort of illustrated by the tempo trackbar
control in this sample. This control sets a flag which forces the converter
code to start a new buffer, with the first event being a new tempo setting.
The tempo setting is calculated as a relative increase with respect to the
last real tempo event from the file. Of course, to implement the scheme
mentioned above requires some modification to the converter code so that it
will work with multiple MIDI files.
Since the buffering scheme uses very small buffers, it is currently rather
sensitive to heavy activity which may prevent it from completing processing
in time. This can be solved by increasing the NUM_STREAM_BUFFERS constant,
but you must make a trade-off between latency and playback stability.
Known problems and possible improvements:
-----------------------------------------
It is more desireable to enumerate all possible MIDI output devices and then
either allow the user to use a specific device, or choose one which has
desireable capabilities. It is not recommended that you ship a product
which is hard-coded to use the MIDI Mapper only. For more on enumerating
MIDI output devices, see the MIDIPLYR sample application which is part of
the Win32 SDK.
Instead of using the BUFFER_TIME_LENGTH, it would be possible to handle the
time signature META event in MIDI files and calculate the length of a
measure of music. Then you could change buffers at the end of each measure,
which would probably yield a smoother sounding transition. It may even be
possible to define system-exclusive events or other such extensions to the
MIDI converter code designed to provide your application with extra data
about when to switch between buffers or do other processing, though it is
not necessarily recommended that you modify the MIDI file format spec. For
more information on that spec, contact the International MIDI Association.
You may wish to modify the way a change in tempo is handled, or remove this
code entirely. Right now, there is a chunk of code in the convert function
AddEventToStreamBuffer() which detects tempo events and stores the new tempo.
There is also code which will react to the tempo slider by calculating a new
tempo, truncating the current buffer, and starting the next buffer with a
tempo event reflecting the new desired tempo. It may be more desireable to
force any tempo changes which are not encoded in the file originally to take
effect only on buffer boundaries, instead of always creating a buffer
boundary. Proceeding under the above context of buffers equal in length to
measures of music, it may make more sense to only change tempo between each
measure. You can also send tempo change messages using the midiOutShortMsg()
function, similar to the way SetAllChannelVolumes() behaves.
The volume control is a channel-wide, percentage-based control which relies on
a cache of volumes for each channel. As the converter encounters a volume
change message, it flags it for a callback. This causes the MidiProc() to
receive notification when that event is reached. MidiProc() then grabs a
copy of the new volume event and sends a MIDI short message to the proper
channel which reflects the current slider position. In other words, the
code saves the "full" or "raw" value and then modifies it so the volume
trackbar represents a percentage of that volume. Though it is not shown
here, this scheme could be broken down to allow for individual volume
control also. This idea could also be expanded to include the LSB volume
controller(39), which is not handled here.
Further, by duplicating the volume code described above and making slight
modifications to the converter (to detect other events), it is possible to
handle pan, balance, or other controller messages using the exact same idea.
Having said the above, it should be noted that attaching the volume change code
to a trackbar is for illustration purposes only. The implementation shown
and described works best for isolated volume events, like when your player
moves away from the sound source and you need to update volume. It should
not really be used for real-time scrolling because the method tends to flood
the MIDI output device with short messages, which interferes severly with
playback.
BUG: If you are using an internal MIDI device which uses the OPL chipset, you
should be aware of a bug which seems to occur in most of these drivers. If
a volume channel message is sent to these drivers, they will not reflect the
change until a note on/off event occurs. This means long sustaining notes
will not reduce in volume.