Chapter 14. Playing Wave Sounds

In the previous hour, you learned that digital sounds are called waves in Windows, and that they can be easily recorded and edited using a sound utility such as Sound Recorder, which ships with Windows. What you didn’t learn was how to play wave sounds in the context of a game. This hour picks up where the previous hour left off by showing you exactly how to play wave sounds. You might be thinking that if wave sounds are anything like bitmaps, this hour will be fairly technical and include a lot of messy code for loading and interacting with wave data. You’ll be glad to know that waves are actually quite easy to play, and adding wave sound effects to a game is unbelievably simple to do. This hour shows you how!

In this hour, you’ll learn:

  • The two different ways to access wave sounds for playback

  • How to play wave sounds using the Win32 API

  • How to play a looped wave sound, and then stop it

  • How to incorporate wave sounds into an existing game

Accessing Wave Sounds

You learned in the previous hour that wave sounds are stored in files with a .WAV file extension. So, if you record a wave using Sound Recorder or some other sound utility, the end result will be a file with a .WAV file extension. Hopefully by now you’ve already recorded a few sounds or maybe found a few on the Internet. Either way, you understand that a wave sound is stored in a file on disk, kind of like how bitmap images are stored. Similar to bitmaps, it’s possible to load a wave directly from a file in a game. However, this isn’t the most advantageous way to use waves in games.

If you recall from earlier in the book, I showed you how to include bitmaps as resources in your games. In fact, every program example you’ve seen thus far that uses bitmaps, such as the Henway game in Hour 12, has included them as resources that are stored directly in the executable program file. Waves are also resources, and can therefore be included in the resource script of a program and compiled into the executable program file. Playing the wave sound is slightly different depending on whether the wave is stored in a file or placed directly within the program as a resource. It’s ultimately up to you how you want to access waves, but organizationally I think it’s much better to use them as resources so that you don’t have to worry about distributing separate wave files with your games.

Playing Wave Sounds

The Win32 API provides high-level support for playing wave sounds, which is extremely good news for you and me. One drawback with the high-level wave support in the Win32 API is that it doesn’t allow for wave mixing, which is the process of mixing wave sounds so that multiple sounds can be played at once. Unfortunately, this means that only one wave sound can be played at any given moment. You’ll have to decide how you want this to impact your games because there are certainly going to be situations in which two or more sounds need to be played at once. You have the following options when it comes to resolving the problem of being able to play only one sound among several:

  • Interrupt the currently playing sound to play the next sound.

  • Allow every sound to play to completion, and reject sounds that attempt to interrupt the currently playing sound.

Note

Playing Wave Sounds

Wave mixing is made possible by the DirectX game programming API, which I’ve mentioned a few times throughout the book. DirectX is incredibly powerful, especially when it comes to its digital sound capabilities, but it has a very steep learning curve. If you should decide to tackle DirectX after reading this book, by all means go for it. However, for the purposes of creating your own games without spending a great deal of time and energy learning the inner workings of DirectX, the high-level Win32 API approach to playing wave sounds is sufficient.

Until you actually experiment with these two wave playing approaches, you might think that the first approach sounds appealing because it gives every sound a chance to play. However, in some situations you’ll find that sounds interrupting each other can be as annoying as a bunch of people interrupting each other in a heated debate. If you’ve ever seen the political television show Crossfire, you know what I’m talking about. On the other hand, when you let sounds play to completion, you will inevitably be disallowing other sounds their chance at playing—some of which might be important to the game play. So, it ultimately depends on the kind of game as to whether it makes sense to interrupt sounds or allow them to play out.

Regardless of how you address the limitation of only being able to play one sound, the Win32 API function that makes wave playing possible is called PlaySound(), and it looks like this:

BOOL PlaySound(LPCSTR szSound, HMODULE hModule, DWORD dwSound);

The three arguments to the PlaySound() function determine a variety of different things such as whether the sound is being played from a wave file or from memory as a wave resource. Additionally, the function allows you to control whether a sound may be interrupted while playing, as well as whether it should be looped repeatedly. The next few sections examine the PlaySound() function in more detail, and show you how to control the playback of wave sounds.

Playing a Wave from a File

The simplest way to use the PlaySound() function is to play a wave file directly from the local hard drive. When playing a wave from a file, the first argument to the PlaySound() function, szSound, identifies the name of the wave file. The second argument, hModule, applies only to playing wave resources, so you can pass NULL as the second argument when playing wave files. The third argument, dwSound, is used to determine the specific manner in which the wave is played.

The dwSound argument can contain one or more of several different flags that control various facets of the wave playback. For example, the SND_FILENAME flag indicates that you’re playing the wave from a file. There are two other flags, SND_SYNC and SND_ASYNC, that determine whether a wave is played synchronously or asynchronously. A synchronous wave is a wave that doesn’t allow a program to resume operation until it finishes playing, whereas an asynchronous wave allows a program to continue its business while the wave plays in the background. As you might be guessing, synchronous waves are pretty much out of the question in games because you don’t want the whole game to pause while a wave is playing. So, you’ll need to use the SND_ASYNC flag when playing waves using the PlaySound() function.

Following is an example of playing a wave file using the PlaySound() function:

PlaySound("Boo.wav", NULL, SND_ASYNC | SND_FILENAME);

As you can see, this is a pretty simple line of code when you think about everything it is accomplishing. A wave file is being loaded from disk and played through the speakers on your computer while your program is allowed to continue operating.

Another PlaySound() flag worth mentioning is the SND_NOSTOP flag, which causes a sound to be more respectful of a sound that is already playing. More specifically, if you specify the SND_NOSTOP flag when playing a wave sound, the sound will not interrupt another wave if it is already playing. The downside to this flag is that the sound will end up never getting played. If you want to make sure that a sound is played no matter what, be sure not to specify the SND_NOSTOP flag. When you don’t specify the SND_NOSTOP flag, the sound you’re playing will interrupt the currently playing sound no matter what.

Playing a Wave from a Resource

Earlier in the hour, I mentioned that playing a wave sound as a resource has advantages over playing a wave file because you can combine the wave resource into the main program file. The PlaySound() function includes a flag that allows you to specify that you’re playing a wave resource, as opposed to a wave file. The SND_RESOURCE flag indicates that the wave is a resource and that the hInstance argument to PlaySound() is a resource identifier. Because a wave resource must be loaded from an executable program file, you must provide the module handle for the program in the second argument to PlaySound(). The games and program examples you’ve seen throughout the book store away this handle in the _hInstance global variable.

Following is an example of playing a wave sound as a resource using the PlaySound() function:

PlaySound((LPCSTR)IDW_BOO, _hInstance, SND_ASYNC | SND_RESOURCE);

Note

Playing a Wave from a Resource

Wave resource IDs are typically named so that they begin with IDW_, which indicates that the ID is associated with a wave.

In this example, the resource ID of the wave sound is IDW_BOO, whereas the module handle of the program is the global variable _hInstance. Also, the SND_RESOURCE flag is used to indicate that this is indeed a wave resource, as opposed to a wave file. One important thing to note about this code is that it assumes you’ve already declared the IDW_BOO resource ID and placed a reference to the wave resource in the resource script for the program. Just as when you add new bitmaps to a game, you’ll need to create a resource ID and an entry in the resource script for each wave sound you use in a game.

Looping a Wave Sound

In some situations, you might want to play a sound repeatedly. For example, if you opt to use sampled music in a game, it probably will make sense to use a wave sound that can be looped over and over and sound like one continuous piece of music. The PlaySound() function supports a flag for playing a wave sound looped, which means that the sound is played repeatedly until you interrupt it with another sound or until you explicitly stop it using the PlaySound() function (more on stopping waves in a moment). The flag to which I’m referring is SND_LOOP, and specifying it results in a wave being repeated over and over.

Following is an example of playing a looped wave resource:

PlaySound((LPCSTR)IDW_BACKBEAT, _hInstance, SND_ASYNC | SND_RESOURCE |
  SND_LOOP);

The SND_LOOP flag requires you to also use the SND_ASYNC flag because it wouldn’t make sense to loop a sound synchronously. Keep in mind that a looped sound will continue to loop indefinitely unless you stop it by playing another wave or by stopping the looped wave with another call to PlaySound().

Stopping a Wave Sound

If you ever use a looped wave, you will undoubtedly want to know how to stop it from playing at some point. Additionally, you might have a fairly lengthy wave sound that you’d like to be able to stop in case your game is deactivated or otherwise needs to slip into silent mode. You stop a wave from playing by using the SND_PURGE flag in the PlaySound() function. The SND_PURGE flag requires the first two arguments to PlaySound() to be used just as when you first played the sound. For example, if you provided a module handle as part of playing a wave resource, you’ll still need to provide the handle when purging the wave.

Following is an example of stopping the looped sound played in the previous section:

PlaySound((LPCSTR)IDW_BACKBEAT, _hInstance, SND_PURGE | SND_RESOURCE);

This example reveals how to stop a single wave from playing. It’s also possible to stop any waves from playing so that you don’t have to be so specific about what you’re doing. This is accomplished by passing NULL as the first argument to PlaySound(), like this:

PlaySound(NULL, NULL, SND_PURGE);

This line of code results in the currently playing sound being stopped, regardless of what it is and how it was played.

Building the Brainiac 2 Program Example

You now have enough wave playing skills to see how waves are played in the context of a real game. In fact, instead of embarking on an entirely new game project, it makes sense to revisit a game you’ve already created and examine how to spiff it up with wave sounds. I’m referring to the Brainiac game from Hour 8, which is a simple tile matching memory game if you recall.

The Brainiac game isn’t necessarily suffering from a lack of sound support, but it could definitely be made more interesting with some carefully injected waves. Think about how the game plays and how playing a wave sound here and there might improve its playability. After playing the game a few times and thinking about what would make it more fun, I came up with the following list of game events that could benefit from having sound effects associated with them:

  • Selecting a tile

  • Matching a pair of tiles

  • Mismatching a pair of tiles

  • Winning the game by matching all the tiles

If you play the game again and pay attention to each of these game events, you can start to see how a brief sound could add some interest to the game and make it a little more fun. The next few sections explore how to create a new version of the Brainiac game called Brainiac 2 that plays wave sounds in response to each of these game events.

Writing the Program Code

If you recall, the bulk of the code for the game logic in the Brainiac game resides in the MouseButtonDown() function. This function is called whenever the player clicks the mouse on the game screen, and is therefore where all of the tile matching takes place. Not surprisingly, this function is where you’ll find all the game events for the Brainiac game, such as tile selections, matches, and mismatches. Listing 14.1 shows the new MouseButtonDown() function, which includes several calls to the PlaySound() function to play sounds responding to game events.

Example 14.1. The MouseButtonDown() Function Plays Wave Sounds in Response to Several Important Game Events

 1: void MouseButtonDown(int x, int y, BOOL bLeft)
 2: {
 3:   // Determine which tile was clicked
 4:   int iTileX = x / _pTiles[0]->GetWidth();
 5:   int iTileY = y / _pTiles[0]->GetHeight();
 6:
 7:   // Make sure the tile hasn't already been matched
 8:   if (!_bTileStates[iTileX][iTileY])
 9:   {
10:     // See if this is the first tile selected
11:     if (_ptTile1.x == -1)
12:     {
13:       // Play a sound for the tile selection
14:       PlaySound((LPCSTR)IDW_SELECT, _hInstance, SND_ASYNC | SND_RESOURCE);
15:
16:       // Set the first tile selection
17:       _ptTile1.x = iTileX;
18:       _ptTile1.y = iTileY;
19:     }
20:     else if ((iTileX != _ptTile1.x) || (iTileY != _ptTile1.y))
21:     {
22:       if (_ptTile2.x == -1)
23:       {
24:         // Play a sound for the tile selection
25:         PlaySound((LPCSTR)IDW_SELECT, _hInstance, SND_ASYNC |
26:           SND_RESOURCE);
27:
28:         // Increase the number of tries
29:         _iTries++;
30:
31:         // Set the second tile selection
32:         _ptTile2.x = iTileX;
33:         _ptTile2.y = iTileY;
34:
35:         // See if it's a match
36:         if (_iTiles[_ptTile1.x][_ptTile1.y] ==
37:           _iTiles[_ptTile2.x][_ptTile2.y])
38:         {
39:           // Play a sound for the tile match
40:           PlaySound((LPCSTR)IDW_MATCH, _hInstance, SND_ASYNC |
41:             SND_RESOURCE);
42:
43:           // Set the tile state to indicate the match
44:           _bTileStates[_ptTile1.x][_ptTile1.y] = TRUE;
45:           _bTileStates[_ptTile2.x][_ptTile2.y] = TRUE;
46:
47:           // Clear the tile selections
48:           _ptTile1.x = _ptTile1.y = _ptTile2.x = _ptTile2.y = -1;
49:
50:           // Update the match count and check for winner
51:           if (++_iMatches == 8)
52:           {
53:             // Play a victory sound
54:             PlaySound((LPCSTR)IDW_WIN, _hInstance, SND_ASYNC |
55:               SND_RESOURCE);
56:             TCHAR szText[64];
57:             wsprintf(szText, "You won in %d tries.", _iTries);
58:             MessageBox(_pGame->GetWindow(), szText, TEXT("Brainiac"),
59:               MB_OK);
60:           }
61:         }
62:         else
63:           // Play a sound for the tile mismatch
64:           PlaySound((LPCSTR)IDW_MISMATCH, _hInstance, SND_ASYNC |
65:             SND_RESOURCE);
66:       }
67:       else
68:       {
69:         // Clear the tile selections
70:         _ptTile1.x = _ptTile1.y = _ptTile2.x = _ptTile2.y = -1;
71:       }
72:     }
73:
74:     // Force a repaint to update the tile
75:     InvalidateRect(_pGame->GetWindow(), NULL, FALSE);
76:   }
77: }

The first two calls to the PlaySound() function occur in response to tile selections (lines 14, 25, and 26). If you recall, two tiles must be selected in each turn of the game, so the IDW_SELECT sound is played in response to each of the two tile selections. The IDW_MATCH sound is played whenever a pair of tiles is successfully matched (lines 40–41). Similarly, the IDW_MISMATCH sound is played whenever two mismatched tiles are selected (lines 64–65). Finally, the IDW_WIN wave is played whenever all the tiles have been matched (lines 54–55).

Assembling the Resources

Because waves are resources similar to bitmaps and icons, you must declare resource IDs for them, as well as include them in the resource script for your games. In the case of the Brainiac 2 game, the wave resource IDs are declared in the Resource.h header file, which is shown in Listing 14.2.

Example 14.2. The Resource.h Header File Defines New Resource IDs for the Wave Sounds

 1: //-----------------------------------------------------------------
 2: // Icons                    Range : 1000 - 1999
 3: //-----------------------------------------------------------------
 4: #define IDI_BRAINIAC        1000
 5: #define IDI_BRAINIAC_SM     1001
 6:
 7: //-----------------------------------------------------------------
 8: // Bitmaps                  Range : 2000 - 2999
 9: //-----------------------------------------------------------------
10: #define IDB_TILEBLANK       2000
11: #define IDB_TILE1           2001
12: #define IDB_TILE2           2002
13: #define IDB_TILE3           2003
14: #define IDB_TILE4           2004
15: #define IDB_TILE5           2005
16: #define IDB_TILE6           2006
17: #define IDB_TILE7           2007
18: #define IDB_TILE8           2008
19:
20: //-----------------------------------------------------------------
21: // Wave Sounds              Range : 3000 - 3999
22: //-----------------------------------------------------------------
23: #define IDW_SELECT          3000
24: #define IDW_MATCH           3001
25: #define IDW_MISMATCH        3002
26: #define IDW_WIN             3003

The four wave resource IDs are declared in the Resource.h header file (lines 23–26), which means that you can now place them in the Brainiac.rc resource script. This script is shown in Listing 14.3.

Example 14.3. The Brainiac.rc Resource Script Includes Four New Wave Resources

 1: //-----------------------------------------------------------------
 2: // Include Files
 3: //-----------------------------------------------------------------
 4: #include "Resource.h"
 5:
 6: //-----------------------------------------------------------------
 7: // Icons
 8: //-----------------------------------------------------------------
 9: IDI_BRAINIAC       ICON         "Brainiac.ico"
10: IDI_BRAINIAC_SM    ICON         "Brainiac_sm.ico"
11:
12: //-----------------------------------------------------------------
13: // Bitmaps
14: //-----------------------------------------------------------------
15: IDB_TILEBLANK      BITMAP       "TileBlank.bmp"
16: IDB_TILE1          BITMAP       "Tile1.bmp"
17: IDB_TILE2          BITMAP       "Tile2.bmp"
18: IDB_TILE3          BITMAP       "Tile3.bmp"
19: IDB_TILE4          BITMAP       "Tile4.bmp"
20: IDB_TILE5          BITMAP       "Tile5.bmp"
21: IDB_TILE6          BITMAP       "Tile6.bmp"
22: IDB_TILE7          BITMAP       "Tile7.bmp"
23: IDB_TILE8          BITMAP       "Tile8.bmp"
24:
25: //-----------------------------------------------------------------
26: // Wave Sounds
27: //-----------------------------------------------------------------
28: IDW_SELECT         WAVE         "Select.wav"
29: IDW_MATCH          WAVE         "Match.wav"
30: IDW_MISMATCH       WAVE         "Mismatch.wav"
31: IDW_WIN            WAVE         "Win.wav"

The resource script for the Brainiac 2 game is very similar to the original Brainiac resource script, except that it now includes wave resources (lines 28–31). Notice that the resource type WAVE is used when listing each of the wave resources in the script. This script successfully maps the wave sounds to resource identifiers that can then be used with the PlaySound() function, as you saw in the previous section.

Note

The Brainiac.rc Resource Script Includes Four New Wave Resources

If you happen to get a “multiple initialization” compiler error while compiling the Brainiac 2 program, you can easily fix it by removing the int variable declaration in the second for loop of the GameStart() function. This error stems from the fact that some compilers don’t fully support the standard C++ approach of declaring loop initializer variables local to the loop. So, the int variable i is mistakenly interpreted as being declared twice.

Testing the Finished Product

You already have a pretty good idea how to play the Brainiac game, so testing out the new wave sounds is very straightforward. Just take the game for a spin and pay attention to how sounds are played in response to different game events such as selecting and matching tiles. Figure 14.1 shows the game in action.

The Brainiac 2 game is made more interesting than its predecessor by playing wave sounds in response to game events.

Figure 14.1. The Brainiac 2 game is made more interesting than its predecessor by playing wave sounds in response to game events.

Believe it or not, a sound is being played in this figure. Because I couldn’t convince the publisher to include an audio tape with the book, you’ll just have to imagine the sound as you look at the figure. Or just fire up the game yourself and experience the sounds on your own!

Summary

This hour took what you learned in the previous hour about digital sound and gave it a practical face. More specifically, you learned how to play wave sounds using a high-level Win32 API function called PlaySound(). Although this function certainly has its limitations, you can’t beat it when it comes to sheer simplicity; a single line of code is all it takes to play a wave sound. You found out how to play wave sounds from a file or as a resource, as well as how to play looped wave sounds and how to stop a sound once it has started playing.

You might be wondering about the fact that this hour focused solely on sampled digital audio, as opposed to MIDI music. The playback of wave sounds is in fact dramatically different than the playback of MIDI music at the programming level, which is why this hour dodged the topic of playing MIDI music. However, the next hour digs right into the playback of MIDI music, and how to add music to games.

Q&A

Q1:

Why doesn’t Windows keep track of waves that weren’t allowed to play because of interruption, and then play them as soon as it gets a chance?

A1:

Windows doesn’t get into the business of tracking waves because it would be very difficult, if not impossible, to ever play them all when you consider that only one can play at a time. Additionally, sounds are often tied to program events that are time critical, which means that it wouldn’t be very helpful to play the sound late. For example, if you were developing a military game and a bullet sound was postponed because an explosion sound was being played, it wouldn’t really help for the bullet sound to be played later when the bullet might not even be on the screen anymore.

Q2:

Is it possible to prioritize waves so that only some waves are interrupted, while others are not?

A2:

Yes, but it works in the reverse. If you recall, when you play a wave, you indicate whether it can interrupt any other waves. So, instead of identifying a high-priority wave as not being able to be interrupted, you instead indicate that a high-priority wave has the ability to interrupt other waves. Practically speaking, this means that you can have two levels of priority for waves. Low-priority waves would use the SND_NOSTOP flag to indicate that they aren’t allowed to interrupt other waves. On the other hand, high-priority waves would simply not use the SND_NOSTOP flag, which means that they will interrupt any currently playing wave.

Workshop

The Workshop is designed to help you anticipate possible questions, review what you’ve learned, and begin learning how to put your knowledge into practice. The answers to the quiz can be found in Appendix A, “Quiz Answers.”

Quiz

1:

Why is it a problem that the Win32 PlaySound() function doesn’t support wave mixing?

2:

What is the difference between synchronous and asynchronous wave playback?

3:

How do you stop a wave from playing once it has started?

Exercises

  1. Try replacing a couple of the wave sounds in the Brainiac 2 game with some sounds of your own.

  2. Think of another game event in the Brainiac 2 game that doesn’t currently have a sound, and then add a wave sound for it. This isn’t necessarily as difficult as you might think. For example, how about a new wave sound to indicate that the player made two matches in a row? Or maybe a wave sound to reward the player for beating the previous score?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.254.61