It wasn't too long ago that I was afraid of the Big Bad Wolf. Going by the DirectX documentation, DirectMusic seemed a complicated beast which was too much for me to handle. But when you get down and dirty with it, it really isn't bad at all.
This is the first of a two-part tutorial on using DirectMusic in your games. If you've ever read Tricks of the Windows Game Programming Gurus by Andre' LaMothe, then you already know the majority of what I'll be presenting in Part One. This is where we take that cryptic Microsoft documentation and make some sense out of it. We'll get DirectMusic up and running, load a midi file, and play it.
Part Two will dig a little deeper. There, I'll show you how to manipulate the music volume as well as the basics of using DirectMusic Notifications and threads to gain more control over your game music.
On to Part One...
[size="5"]Initialization
The first thing to note about DirectMusic is that it differs from the other DirectX components in the way you set it up. With the other components you can get by with just linking to the appropriate import lib (ddraw.lib, dsound.lib) and then calling the set-up routines. The corresponding DLL (ddraw.dll, dsound.dll) will be loaded automatically when the app starts up. DirectMusic adds a bit more work to the mix.
There is no such thing as dmusic.lib. This means that dmusic.dll must be loaded manually. To do so, you'll have to call a couple of COM routines. Now, COM is beyond the scope of this article, but the routines we need to call are not that difficult to follow. Let's look at the process step-by-step.
[size="3"]Step One: Include Header Files
Make sure you've included the appropriate header files. DirectMusic requires no less than four headers. These are:
#include
#include
#include
#include
You'll also need to make sure that you've either #define'd INITGUID somewhere in your app or that you have linked to dxguid.lib.
[size="3"]Step Two: Initialize COM
Initializing COM requires just one function call to CoInitialize. This is a Win32 API call and must be called before you can manually load any COM components into your program. The only parameter this function accepts is reserved for future use, so for now it should always be NULL. This only needs to be called once in your program, so if you want to manually load multiple COM objects, you can call it somewhere near the beginning of the program and be done with it.
CoInitialize (NULL);
[size="3"]Step Three: Create The Performance
With other DirectX components you have to create things such as an IDirectDraw or IDirectSound object. Not so with DirectMusic. There is an IDirectMusic interface, but it can be created internally so you'll never have to deal with it (although you can if you want to). Instead, you'll use COM to create an IDirectMusicPerformance object.
The Performance is what you will use to handle almost all of your DirectMusic needs, from playing MIDI's to setting up Notifications. You can think of the Performance as the orchestra and yourself as the maestro. You tell the Performance when and how to play. Getting your orchestra ready requires a call to the COM function CoCreateInstance.
I won't cover all the parameters here. If you absolutely must know what they are, I will refer you once again to MSDN. For now, though, just trust me. This is how you set up a Performance object:
IDirectMusicPerformance *performance;
if (FAILED(CoCreateInstance(CLSID_DirectMusicPerfomance,
NULL,
CLSCTX_INPROC,
IID_IDirectMusicPerformance,
(void **)&performance)))
{
// handle error
}
In order for your orchestra to play some tunes, they must have their instruments ready. A call to IdirectMusicPerformance::Init comes next. Now, there is one caveat here. If you are going to use DirectSound and DirectMusic together in the same app, then you need to make sure that DirectSound is initialized first. Why? Read on.
DirectMusic uses a DirectSound object internally and the Performance's Init function takes a pointer to a DirectSound object as a parameter. If you tried to create your DirectSound object after initializing DirectMusic, I imagine you would get a DSERR_ALLOCATED error. I've never tried, so don't take my word for it. So always initialize DirectSound first and pass a pointer to the DirectSound object to IDirectMusicPerformance::Init. Otherwise, if you're only using DirectMusic (as is the case in this tutorial) then you'll just pass NULL to the Init function.
IDirectMusicPerformance::Init (IDirectMusic** ppDirectMusic,
LPDIRECTSOUND pDirectSound,
HWND hwnd)
HRESULT hr;
if (FAILED(hr=performance->Init (NULL,NULL,main_hwnd)))
{
// handle error
}
Your orchestra needs to know where they'll be playing. This is where the IDirectMusicPort interface comes into play. You can use DirectMusic to enumerate all of the ports available on the machine, but this brings up certain issues you should be aware of.
A port can be a hardware device, a software synthesizer, or a software filter. That's a wide variety of ports, and variety amounts to variations in sound. If you enumerate all of the available ports and let the user choose one, there's no guarantee the music will sound the way you intended it. However, by always using the default Microsoft Software Synthesizer as the port, you can't go wrong. This will ensure that your music will sound the same across all Windows machines with DirectMusic installed. It sounds pretty darn good to boot.
In order to let the Performance know which port we would like to use, we need to make a call to IDirectMusicPerformance::AddPort. It takes one parameter, an IDirectMusicPort pointer. To use the default MS synthesizer, as we are going to do, you just pass NULL as the parameter.
if (FAILED(hr=performance->AddPort(NULL)))
{
// handle error
}
[size="3"]Step 6: Create the Loader
Okay, this is the last step in initialization and I can't think of a real-world orchestral analogy to throw in here (they don't have music boys who deliver music do they?).
The IDirectMusicLoader interface provides methods to load various data files that can be used in DirectMusic. Of course, we're only interested in MIDI files. All we need to do is make a call to that handy COM function, CoCreateInstance, and we'll have a Loader object ready to load up some music.
IDirectMusicLoader *loader = NULL;
if (FAILED(CoCreateInstance(CLSID_DirectMusicLoader,
NULL,
CLSCTX_INPROC,
IID_IdirectMusicLoader,
(void **)&loader)
{
// handle error
}
[size="5"]Loading And Playing Music
Now comes the fun part. Although it may seem a bit daunting at first, loading a MIDI file via a DirectMusicLoader is much simpler than dealing with the Win32 multi-media functions. I'm not going to detail the code here. Instead, I'll just give you the gist of what needs to be done. You can look at the source code that accompanies this article and read the comments to learn what the code does.
[size="3"]Loading a MIDI File
The first thing to do is to tell the loader where to look for the files and what type of files to look for. This is accomplished by a call to IDirectMusicLoader::SetSearchDirectory. Next, a DMUS_OBJDESC structure needs to be setup with values appropriate for MIDI files (again, all of this is in the example source). From that point, you just call IDirectMusicLoader::GetObject and pass a pointer to an IDirectMusicSegment object as one of the parameters. Finally, make a couple of calls to the Segment in order to set some parameters and "download" the instruments. Then you're all done.
So, there's really not much to it at all. I wish I could go into more detail with the source here, but space is sparse. So please make sure you read the comments in the example source thoroughly.
[size="3"]Playing Loaded Music
Playing a MIDI you have already loaded is quite simple. You do so by making a call to IDirectMusicPerformance::PlaySegment. Just pass a pointer to the IDirectMusicSgement you wish to play. There are several flags you can pass along, as well as a time to start playing. You usually will only want to use these flags if you're dealing with music you created in DirectMusicProducer, which is a topic for another article. For now, just pass along 0's.
The final parameter to PlaySegment is a pointer to an IDirectMusicSegmentState. This is important only if you need to track information about the Segment. For our purposes, it can just be NULL.
performance->PlaySegment(Segment_To_Play,0,0,NULL);
[size="5"]A Final Step
Okay. You've got everything loaded, your music is playing. Now you can quit the program and carry on, right? Wrong. The DirectMusic system must be shutdown.
Just like all DirectX objects, the DirectMusic components each have a Release method that should be called before exiting your application. You need to release the DirectMusicLoader you used, as well as the performance. However, the performance requires that you call IDirectMusicPerformance::CloseDown before releasing it.
Before you release the segments, you must first set a segment parameter called GUID_Unload on each segment you loaded. Again, you'll have to peek at the source to see all of this in action, but it's pretty self-explanatory.
[size="5"]Conclusion
Not all that difficult, right? You will find that after you have learned the basics, more and more of the DirectMusic API will begin to make sense to you. Be sure to study the example source carefully and you will be well on your way. If you wish to compile it, there are no additional libs you need to link to (other than the default libs MSVC sets up for you - don't know about other compilers). Just create a new Win32 Application and compile.
Please report any errors you find in the article or the code to me at [email="mdat71@thrunet.com"]mdat71@thrunet.com[/email].
That's all folks. See you in Part Two.