Michael Wolraich (mike@zone28.com), September 20, 2001.
Mr. Wolraich is an independent contractor in New York City. He specializes in high-end client applications for Windows platforms.
So you want to make music on the Pocket PC. After all, one of the major selling points of most Pocket PC devices is their multimedia capabilities. Unfortunately, the powers that be have not made it easy. For one thing, even though WinCE supports the powerful DirectSound library, Microsoft elected to leave it out of the Pocket PC platform, so you're going to have to resort to the old MMIO API. To make matters worse, Pocket PC does not include the entire MMIO API, and the hardware support for the functions that are included can be, shall we say, finicky.
The good news is that your Pocket PC can still do just about anything that your PC can; it's just a little trickier to program. This article will help you to build a simple audio player while avoiding some of the pitfalls along the way. (Note: if all you want to do is play WAVE file, there's no need to read any further. Use the sndPlaySound() function, and avoid the headaches.)
To start off, you're going to need some audio data, such as a wave file. Unfortunately, all the public domain code libraries for parsing wave files are based on the part of the MMIO API that Pocket PC does not support. I have included a very simple class for parsing wave files that does not rely on the missing functionality, but be warned that while this will work for most wave files, it may not work for all of them.
In order to open a connection to an audio device, you need to know three relevant attributes of your audio data:
With these attributes, you can initialize a WAVEFORMATEX structure as follows:
Now you can use the waveOutOpen() function with the WAVEFORMATEX structure to open an HWAVEOUT handle to the audio device. The documentation for waveOutOpen() will tell you that you can use the WAVE_MAPPER flag to let the system to find the appropriate device automatically. Do not do it! When I use WAVE_MAPPER on my iPAQ, I get bugs in the waveOutGetVolume(), waveOutSetVolume(), and waveOutGetID() functions. Instead, loop through the available audio devices until you find an appropriate one:
Most likely, you will have only one available device: id = 0.
Notice that the waveOutOpen() calls listed above specify the CALLBACK_NULL flag. Most likely, you're going to want to know when you your device has finished playing an audio file. The waveOut API provides four mechanisms for getting that information: a callback function, windows messages, an event handle, or a thread handle. To use a callback, define a function as follows:
Then call waveOutOpen() with a pointer to the function:
To use windows messages, call waveOutOpen() with a window handle:
Then handle the WM_WOM_OPEN, WM_WOM_DONE, and WM_WOM_CLOSE messages in the window's WndProc(). An example of using an event handle is provided in the included PlayWav.zip sample.
Now that you have a handle to the audio device, you need to prepare a WAVEHDR structure so that you can write your audio data to the device. Allocate space for whdr.lpData, and copy the data into it. Set whdr.dwBufferLength to the size of the buffer. You can set the whdr.dwLoops member to play the file repeatedly and whdr.dwUser to pass buffer-specific data to your callback function. To be safe, set all the other members to zero-I have had errors even when one of the reserved members hasn't been set.
Next, tell device to prepare the buffer with waveOutPrepareHeader():
You are finally ready to play the buffer. This is the easy part. Just call waveOutWrite().
Unfortunately, that's not the end of it. You have to clean up your mess, and you cannot start cleaning until the audio device has finished playing. That's why you need some kind callback mechanism. Handle the clean-up at the point where you receive the WOM_DONE heads up. You'll probably need to put some careful thought into how you architect the scope of your HWAVEOUT and WAVEHDR variables. The dwUser member of WAVEHDR and the dwInstance parameter of waveOutOpen() may be helpful to avoid relying on ugly global variables.
To clean up, first un-prepare the WAVEHDR structure with the waveOutUnprepare() call:
Then free your buffer and close the device connection:
Now, you're free and clear to exit the program or to play another audio file.
Those are the essentials of playing audio on the Pocket PC. Of course, you haven't done anything more complicated then what sndPlaySound() does. Most likely, you'll want to use the waveOut functions to handle data that does not come from a wave file, such as a custom tone or data from a compressed format like MP3. Furthermore, most audio data is going to be too big to handle in a single buffer. You can't just break it into pieces and play each piece one after the other because there will be gaps in the audio output. Instead, you will have to employ double-buffer or circular buffer techniques. A discussion of such techniques is beyond the scope of this article, but there is plenty of information available on the web and in the book store.
On last word of caution: audio devices can be fussy if they're not shut down properly. If your program crashes or you exit directly from a debugger without going through the clean-up process, you may get a MMSYSERR_ALLOCATED error or other problems when you open an audio device connection the next time. Performing a soft reset on your Pocket PC should resolve the problem.
Good luck!