30.1 Adding sound to your program
How do we get our program to make noise? The easiest way is to use the multimedia features of Windows to play so-called wave or *.wav files. A wave file is to sound as a bitmap is to graphics: it's a binary representation. Although Java code can also play files in the *.au format, the Visual C++ libraries don't supply this capability.
And what about music? What about playing *.mp3, or generating sounds with algorithms? Certainly a full-featured game ought to have some music and some more sophisticated sound-blending modes. Also you'd like to be able to use MIDI to avoid having to store a sound file in such a large format. But, in this chapter, we're only going to give you the bare minimum.
If you use the Windows Start | Find dialog you can look for *.wav files on your machine. Assuming that you have a sound card, and assuming that your speakers are turned on, you can 'play' these sounds by double-clicking on them. Before starting to work with sound programming, you should check that your system will indeed play sounds, otherwise you won't be able to tell if your program is working properly. Note that it often takes some fiddling to get sound to work, as there are usually several ways to turn it on and off, including both a control bar dialog and a physical knob on the speakers.
Let's take a look at an example of where the Pop Framework code makes a sound, the case where an asteroid is hit by a bullet in gamespacewar.cpp.
int cCritterAsteroid::damage(int hitstrength)
{
int deathreward = cCritter::damage(hitstrength); /* This is _value
(typically nonzero) you get
for killing off the critter. */
((CPopApp*)::AfxGetApp())->playSound("Ding", SND_RESOURCE |
SND_ASYNC); //Signal the hit.
return deathreward;
}
The call to the CPopApp::playSound
method simply wraps a call to the Windows multimedia API call ::PlaySound. The reason we wrap it like this is so that we can check every call for sound against a 'global' _soundflag
that belongs to CPopApp. Here's the code from pop.cpp.
void CPopApp::playSound(LPCSTR pszSound, DWORD fdwSound)
{
if (_soundflag)
::PlaySound(pszSound, NULL, fdwSound);
}
Windows has an API function called PlaySound(CString soundname, HINSTANCE
programinstance, int flags). This function is not a member of any MFC class, and we put a ::
in front of it to remind ourselves of this fact.
The soundname
is the name of some sound. There are three kinds of sound names you can use, with the type of sound indicated by a flag which is OR
ed into the third argument. We'll come back to this in a minute.
The HINSTANCE
argument in the second place is a throwback to the old Win32 programming and is not needed in MFC applications. In Win32 we need this argument when we want to use a sound that's stored as a program resource. But in MFC applications the second argument can always be NULL.
The flags
in the third argument is made by combining various bitflags with the OR
operation. A variety of SND_
flags are defined in C:\Program Files\Microsoft Visual
Studio\VC98\Include\MMSYTEM.H. Ordinarily we OR
in a flag to tell PlaySound
what kind of soundname
you are giving it: SND_ALIAS, SND_RESOURCE, or SND_FILENAME.
Another flag which we almost always OR
in is the SND_ASYNC. This tells the program to send the work of playing the sound off to the sound card and not to wait for the sound to finish playing before continuing program execution. If this flag is present in a first call of PlaySound, then this means that if a second sound wants to start up, the first sound will stop and let the second sound start. If the SND_ASYNC flag is not present in the first call of PlaySound, then the first sound insists on playing to conclusion before the second sound is allowed to start.
Usually it's better to use the SND_ASYNC flag when you are doing action-generated sounds, as otherwise the sounds can lag behind the actions. And you don't want to stop the action just for a sound. Better to cut a sound short than to have the next sound come too late.
Now let's talk about the three kinds of sound names we can use. The names usually have the form of a string in quotes. The string is not case-sensitive, by the way, so it doesn't really matter how it's capitalized.
System sound name.
You can use the name of some standard Windows event, and Windows will play whatever sound is associated with that event. In this case your call is of the form
::PlaySound("SystemExclamation", NULL, SND_ALIAS | SND_ASYNC);. Some of the system sound names you can use are: 'SystemExclamation,' 'SystemAsterisk,' 'SystemStart,' 'SystemExit,' and 'SystemDefault.' If you look at the sound dialog in your Windows Start | Settings | Control Panel | Sounds you can find other candidates. The names for system events are usually the obvious ones, consisting of the word 'System' run on with the name listed in the dialog.
External sound file.
You can use the name of some special *.wav file which holds a binary description of a sound. In this case your call is of the form
::PlaySound("Bonk.wav", NULL, SND_FILENAME | SND_ASYNC); With a call like this, PlaySound
looks in the same directory as the executable for the requested *.wav file. If it doesn't find the file it looks in the Windows and the Windows\System directory. If it still doesn't find the *.wav, it makes the default 'system ding' sound.
Resource sound file.
The third option is that you can have the sound file description bound into your executable as a resource. This is the one where Win32 requires something special for the second argument. But in MFC, NULL is still okay. Your call is of the form
PlaySound("Ding", NULL, SND_RESOURCE | SND_ASYNC); You add a *.wav to your resource by placing the desired *.wav into the \res subdirectory under your source code and then using the Project | Add resource... | Import... dialog to import the file. [This is the Insert | Resource... | Import... dialog in Version 6.0.] Once you do this, the Resource View will show a 'WAVE' category. It will assign an integer-valued name like IDR_WAVE1 to your wave resource. Right-click on this name, select Properties... on the context menu and use the dialog to change the ID from IDR_WAVE1 to a string like 'Ding,' being sure to type in the quotation marks as well as the string. If PlaySound
doesn't find a given resource it doesn't make any sound at all.
The benefit of the system sound approach is that you are fairly certain that the program will make some sound, as usually Windows has various sounds associated with different kinds of events. Also these sounds are user programmable. The drawback is that you have no control over which sound the user will hear. It depends on how he or she has configured the sounds on his or her system.
The benefit of the external sound file approach and the resource sound file approach is that you can control which sound the user hears.
In the external sound file approach, if the user wants to change the sound, he or she can rename a favorite *.wav file to match the one your program looks for. And at least PlaySound
will make some kind of system sound even if it can't find the requested *.wav file. A drawback is that you need to distribute the necessary *.wav files along with your executable, and it's nicer to just be able to give someone a single *.exe that includes everything.
The benefit of the third approach is that you control which sound the user hears, and the sound is certain to be available in the *.exe. The drawback is that the *.exe will end up being a little larger than before: a small *.wav file is about 10 K, and they can be much larger.
The most professional approach might be to combine the second and third approaches. Include resource files, but allow the user to use File | Open to load external files if he or she likes. This would be an example of adding flexibility to the user interface.
Oh, one final point. Whenever your program includes sound, it must include a control for turning the sound off! Of course the user can turn sound off by using the Windows controls, but your program must be polite enough to be willing to turn its own sound off. That's, again, the reason that we pass all our sound call requests to CPopApp, and let it check against _soundflag
before making noise.
Resource identifiers
Just as PlaySound
with the SND_RESOURCE flag turned on loads sound files from the program resources, there is a LoadBitmap
function that loads bitmap files from the resources, and a LoadCursor
function to load cursor images from the resources. These functions take resource identifiers as arguments.
A resource identifier can be either an integer or a string. By default the Resource Editor assigns integers as identifiers for resources and then makes up mnemonic names like IDR_EDIT_UNDO or IDR_WAVE1 or IDR_BITMAP1 for them. But you can change the identifier to a string.
The MFC versions of CBitmap::LoadBitmap
and CWinApp::LoadCursor
are polymorphic; that is, they'll accept a resource identifier which is either an integer or a string.
The old-style Win32 non-MFC functions like ::PlaySound
will only accept resource identifiers which are strings. If you don't feel like replacing your resource's integer ID by a string you can fake it by using the Windows macro MAKEINTRESOURCE to convert the integer ID into a string ID which ::PlaySound
is willing to use. Thus you could call something like
::PlaySound(MAKEINTRESOURCE(IDR_WAVE1), NULL, SND_RESOURCE |
SND_ASYNC);
If you want to dynamically select which resource to use it's sometimes handy to use integers to stand for them. If you need for the integer values to be consecutive numbers you can directly edit them in the resource.h file or indirectly in the View | Resource Symbols... dialog.
|