![]() |
|
Playing multiple wave files continuously and simultaneously
By Fabricio Kury, May 14, 2003.
IntroductionWindows has various APIs for outputting wave files. The simplest way is to use PlaySound(), which I bet you have already used. It seemed to work fine - until you realized it is unable to play more than one wave at the same time, which makes it useless for purposes like game sound effects. On this article I'll explain how to turn around this limitation, but for that we'll have to forget about the so-beautiful PlaySound(), and take a look at the waveOut interface. It is a bit scary at first sight (especially for those used to PlaySound), but I'll try to take it easy by using only what we really need from it. You may notice that this article is very similar to Playing audio on the Pocket PC, and that's true (I even based myself on it while writing this one). I recommend also reading it, if you want more information about the waveOut interface. What You Need
Getting it to PlayWe'll start by getting our wave file (applause.wav) to be played, just like PlaySound() does, but using waveOut. We first need to load the file, and for that we will need some structures to hold it's data: Take a look at what we need. WAVEFORMATEX and WAVEHDR are structures Windows uses to store information about wave files, and HWAVEOUT is used for playing that wave file with the waveOut interface. First, we write a simple function to load the wave file into memory, in the WAVEHDR, to be more specific: With that we have the wave file ready to be used from memory. But, in order to play it we need to fill in the WAVEFORMATEX structure with information about the wave. The file itself already has that information, so all we have to do is copy them to inside WAVEFORMATEX: Those offsets we defined are the positions of the information we need inside a regular wave file. It's always good to have them #defined so we won't find 'magic numbers' around our own code. With the WAVEFORMATEX filled, we can now get waveOut to play it for us and... wait, there's still more to do. Since now we used the whdr.lpData to store the whole wave file, and whdr.dwBufferLength to keep it's size. That's actually not correct. whdr.lpData should contain a pointer to the wave data itself, and whdr.dwBufferLength the lengh of that wave data alone. To 'convert' the information is simple. We advance the pointer to after the header, and deduct it's size from the total file size: By now we have all information we need ready for use. To being playback, we have to find a waveOut device for us to use. We do that by scanning all available wave devices, and trying to 'open' our wave on it. It's simple, we only need to use waveOutGetNumDevs() and check if the waveOutOpen() returns any error. This loop will stop as soon as it succeeds in opening a device to play our wave, and assigning no callbacks to it (we'll revise this later). If you take a while and debug this code, you'll notice that it always finds a device in the first attempt, which is natural since the device '0' is the default. But it is always good to use safe code, and it is not adding much overhead to the method. With an open device, we can finally tell waveOut to play the wave file for us. Before playing, the API requires the header to be prepared, what is done by the API itself in the following function call: If it does not return any error, which I have never seen happening, we will have our header fully ready to be played. We then give it to waveOurWrite(), which will do the job of sending all the data on the buffer to the sound card, and so, to the speakers, headphones or whatever sound output hardware is installed. Yay! Our file applause.wav should be playing right now (if it isn't, and the functions did not return errors, then something is seriously wrong with your computer). But don't think the work is done, we still have to undo what we have done and 'release' the device and free the memory allocated by the wave file. This is the easiest part, all we have to do is call the counterparts of waveOutPrepareHeader() and iwaveOutOpen() and delete: By now we have successfully played a wave file and left things as they were before. But don't commemorate yet, we haven't done anything more than what PlaySound() would do, except the fact that this method does not wait for the sound to finish to return the command to the application (which is already an useful thing!). Now we can go to a more interesting part... The Multiple Instances 'Trick'Playing a wave file without having to wait for it to finish, like we have just done, is sometimes all we need (like with warning sounds). But, if we want to make a game, specially those fast-paced shoot'em up ones with a crowded screen of lasers and explosions, you'll need to play multiple wave files all at the same time. More than that, you might also need to play the same wave file, multiple times, simultaneously. The first one (play multiple wave files simultaneously) is actually already done. All we have to do is have separate WAVEFORMATEX, WAVEHDR and HWAVEOUT variables for each one and use the same functions we've just written. Of course, we're not going to copy & paste the code, we will make a class to handle it. I'm not going to get into that part here, simply because it is way too obvious and simple (and would make the article pages bigger), but you can look at the sample application if you have any problems with it. But, even so, I will not be using classes in this article, to keep the clarity and 'continuity' from what we've done in the beginning.
But what about if you wanted to play the same wave file, many times, simultaneously? Reloading it every time doesn't seem a good idea, since it would waste tons of memory. Not to mention how slow it would be to open and read a file in runtime every single time we wanted to play a sound. We also can't call waveOutWrite() repeatedly, because the waveOut interface won't let you use that function again with the same data until it is done playing what the first call has requested. Looking for new devices to open is also not possible, since you'd realize 99% of the computers have only one 'valid' wave output device. No way we're going to write a dynamic sound mixing engine, would be waaay too much work for lazy programmers like us. The hell, what can we do? Simple. Let's trick waveOut into thinking we have reloaded the file, when all we did was make a new WAVEHDR pointing to the same memory. Don't worry, it's safe to do that since waveOut only reads memory (the wave data memory, that is). All we have to do are some modifications in our code, with a bit of dynamic memory. First, if we want to make multiple 'instances' of the same memory, we need to have a master copy of it. In other words, we'll need a separate pointer to the wave file loaded in LoadWave(), and a separate variable to hold it's size. Then, we substitute whdr.lpData and whdr.dwBufferLength to those names in the LoadWave() function: This way we will have the wave file loaded into lpfile, not into whdr.lpData like we did before. Of course, we still need to define whdr.lpData, but we will do that later using the 'master copy' of the wave memory (lpfile). Because WAVEHDR is the structure that holds information while waveOutWrite() plays the wave data, for each time we want to play a wave we'll need a new WAVEHDR for it. This means there should not be a WAVEHDR in the 'global' scope, it'll be created right when we call the PlayWave() function. We then modify the PlayWave() function to work like that: Take a look at how PlayWave() works now. It first declares a pointer to a WAVEHDR and allocates memory for it. Then it cleans all garbage data that was left there (this wasn't needed earlier because variables in the global scope are already cleaned). To define the lpData, it copies the lpfile pointer 'advanced' to where wave data begins. The same with the dwBufferLenght. The rest of the function is unchanged, except that we remove the reference operator ('&') and use '->' instead of '.', because we're now dealing with a pointer, not a real object. But wait, there is a problem. We use whdr at the CloseWave() function, to unprepare the header. If it is not in the global scope anymore, how are we gonna access it from outside PlayWave()? The answer is that CloseWave() is not supposed to call waveOutUnprepareHeader(). Stop and think about what we're doing: we're playing a file, waiting for it to stop using Sleep() and then closing right away. That's really not a wise thing to do. Instead of using Sleep(), we should have something to tell us exactly when the wave finishes playing, so then we can call waveOutUnprepareHeader(). Luckly, the waveOut API has support for that. Remember when we call waveOutOpen()? We told it to use no callbacks (CALLBACK_NULL). That's not what we are going to do anymore, we will use a callback function. Callback functions are much like WindowProc()s. It receives a message and decides what should be done in response. In this case, there are three possible messages that it can receive: WOM_OPEN, WOM_DONE WOM_CLOSE. Here's a brief explanation of what each one is:
So, let's write our WaveOut callback: Obviously, we are going to use the WOW_DONE to call waveOutUnprepareHeader(), because when that message is sent the buffer (whdr.lpData) is no longer being played so we can do whatever we want with it. Dislike CloseWave(), the callback function has access to the WAVEHDR because it is supplied in the parameters. To be more specific, when a WOW_DONE message is sent the dwParam1 points to the WAVEHDR. So we can use it like this: This will have the header unprepared (just like the API wants us to do when we've finished with it). But, what good is to do that if the memory we allocated for it is not freed? If we don't delete the whdr, its memory will be forgotten allocated, and in a short time you'll have hundreds of unused megabytes occupying precious memory. So, let's free the whdr memory after unpreparing it: Then, we can remove waveOutUnprepareHeader() from CloseWave(): That's it! We played a wave file in a way we can play it again, from the same memory, as many times you want. Have fun calling PlayWave() a hundred times and hearing a huge crowd clapping at you :) Having achieved a successful simultaneous multiple-instanced wave file playback, there is only one more thing to complete a system with 'everything you'd ever need' for playing wave files... Getting it to LoopMany times we have sounds that need to be played more than once. Some examples are sounds of machineguns, lasers, rain dropping, etc. Those not only need to play, but need to keep playing until we tell it to stop. Of course we can simply keep calling PlayWave(), but that'd not be the best choice because we would be redoing things that don't need to be redone, which means waste of processing power. The best would be a way to keep reusing the data (that is, the WAVEHDR) for continuous playback. Of course, there is a way to do that. Earlier in this tutorial I said that calling waveOutWrite() with the same data will not work until the API has done playing what we have told it to play before. That's true, but it doesn't keep us from calling waveOutWrite() again, with the same data, when it tells us it has just done playing, that is, when we receive a WOM_DONE message: But we were using that message to unprepare the header... So then, what do we do, unprepare the header or call waveOut? To decide that we'll obviously need a flag. This flag will tell us if the wave file should be replayed, and this way keep playing continuously, or if its memory should be deleted because all we wanted was to play it once. Fortunately, the WAVEHDR itself has a member for special use, the dwUser, which can contain anything we want because the API doesn't do anything with it. It is only for our use. So, to define that flag, we can put a parameter in the PlayWave() function, like this: Then, we check the value of whdr->dwUser at the WOM_DONE message processing, and if it is 'true' we don't unprepare the header, we replay it: That way the wave will keep playing forever if you gave 'true' as parameter when calling PlayWave(). But playing a sound forever isn't really what we want. We need to be able to stop it, but only when we tell it to. And more, we need to be able to stop a specific 'instance' of the sound being continuously played, and leave the others playing normally (a good example of that would be some machine guns firing at the same time. All of them are playing the same sound continuously, and we need to be able to stop the sound of one separately if one of the machine guns stop firing). For that we will need two things. First, a pointer to the WAVEHDR we want to stop, and second, a function to use that pointer and manipulate the dwUser flag (ok, the second is really pointless, but I like to use functions so the code keeps clear and readable). The pointer to the WAVEHDR needs to be returned from the PlayWave() itself. Until now it was a bool-type function which returned 'true' on error, but we will make it return NULL on error and a valid WAVEHDR pointer in success, like this: This pointer is what we're going to give to a new function, StopWave(), so it can set the dwUser to zero and this way getting the header to be unprepared when it finishes playing. Notice that StopWave() will not stop playing the wave file in the middle of the playback, it will simply break the continuous playing loop. Also notice that it's always good to use safe code and check if whdr is not NULL. ConclusionWe've accomplished a continuous multiple-instanced simultaneously wave file playing. Now you can get back making your side scroller game and put everything on the screen explode at the same time with the proper sound effects. But, the method explained here is not all-wonderful. It has a major flaw. During the whole article, we have assumed we're only going to play small wave files (less than 100kb). This is ok since most sound effects are small, then there's no problem in loading it entirely on memory at once. But, if you need to play bigger sounds, like long speeches, you'll need to load small portions of the file and play them in sequence, using double or circular buffers techniques to keep the perfect continuity of the sound (otherwise you'll have an audible gap between the playback of each portion). But, for now, what we have done should be enough. Warning!This code will very probably not work on the Pocket PC emulator. If you try to play two files simultaneously it will simply play one and, when it finishes, play the other. Sometimes it even outputs 'gitched' sounds. It is known that the Pocket PC emulator has several problems with sound output, so don't rely on it to know if your sound output method works. The methods described in this article fully agree to what is found at MSDN (Microsoft Developer Network), have been tested and shown to work properly on real Pocket PCs. Related resources:
DiscussDiscuss this article. Here you can write your comments and read comments of other developers. |
|||||||||||||||||||||