Have Fun with the Custom Screensavers Library
Tired of the same old screensavers and don’t want to create your own? In a previous article, I introduced you to my own custom screensavers library. In addition to revealing the library’s internals, the article revealed source code to an example screensaver whose object code integrates with that library. Unfortunately, the article’s substantial length made it impossible to reveal additional (and more interesting) screensavers.
This article makes up for that shortcoming by introducing you to an entertaining 4Balls screensaver. As you read the article, you will discover how this screensaver’s four balls are drawn and animated, how its colorful background is created, and how it can play multiple wave sounds at the same time.
4Balls Screensaver
My former article’s sldemo screensaver wasn’t very entertaining, but it served its purpose in showing you how to construct a screensaver that integrates with my custom screensavers library. In contrast, this article’s 4Balls screensaver is more entertaining: it animates blue, green, magenta, and orange balls on a background that transitions from blue (top) to gold (bottom). Each ball moves with a certain velocity; when it reaches a screensaver window border, a sound is heard, the ball bounces off the border, and the ball moves in a new direction and at a new velocity. Figure 1 shows this screensaver in the Windows 98 SE Display Properties dialog box’s preview window.
Figure 1 Display Properties reveals 4Balls as the current screensaver.
The 4Balls screensaver was created from source code spread out across files 4Balls.c, 4Balls.rc, wave.c, and wave.h. Furthermore, its resources are located in 4Balls.ico, ballblu.bmp (blue ball bitmap), ballgrn.bmp (green ball bitmap), ballmag.bmp (magenta ball bitmap), ballora.bmp (orange ball bitmap), mask.bmp (ball mask bitmap), and richo.wav (richochet wave sound). Apart from 4Balls.ico, these resources load in response to the WM_CREATE message that is sent to 4Balls.c’s ScreenSaverProc() function at screensaver startup:
case WM_CREATE: // Create an animation timer that results in a WM_TIMER message // approximately every TIMER_TIMEOUT milliseconds. if ((uTimerID = SetTimer (hwnd, 1, TIMER_TIMEOUT, NULL)) == NULL) { MessageBox (hwnd, "No available timer!", "4balls", MB_OK); return -1; } // Attempt to open the ball sound effect. If successful, clear the // global sound mute flag to FALSE -- do not mute sound. if ((g_iSndID = OpenWave ("ballsound")) >= 0) g_bMuted = FALSE; // Load the ball image mask, which is a solid black ball image on a // white background. g_hbmMask = LoadBitmap (GetModuleHandle (NULL), "mask"); // Ensure that program doesn’t always start with the same random // number sequence. randomize (); // Initialize all ball information structures. for (i = 0; i < MAXBALLS; i++) { // Zero out each ball’s information structure. ZeroMemory (&g_ballInfo [i], sizeof(g_ballInfo [i])); // Select the ball’s bitmap resource name. switch (i) { case 0 : szWaveName = "ballblu"; break; case 1 : szWaveName = "ballgrn"; break; case 2 : szWaveName = "ballmag"; break; default: szWaveName = "ballora"; } // Load the ball’s bitmap resource. This resource defines the // ball’s image on a black background. g_ballInfo [i].g_hbmBall = LoadBitmap (GetModuleHandle (NULL), szWaveName); // Obtain ball image’s width and height. GetObject (g_ballInfo [i].g_hbmBall, sizeof(bm), &bm); // Save width and height for use in DrawBalls() and // UpdateBalls(). g_ballInfo [i].width = bm.bmWidth; g_ballInfo [i].height = bm.bmHeight; // Initialize ball’s acceleration and velocity. g_ballInfo [i].acceleration = i*MAXBALLS+rand ()%MAXBALLS+1; g_ballInfo [i].dx = g_ballInfo [i].acceleration; g_ballInfo [i].dy = g_ballInfo [i].acceleration; } // Read configuration data from Windows registry. ReadConfig (); return 0;
The WM_CREATE message handler indirectly loads the richochet wave sound resource via a call to the OpenWave() function (in wave.c) and directly loads all needed bitmap resources via calls to the LoadBitmap() Windows API function. Furthermore, this message handler corrects an oversight in the sldemo screensaver’s message handler: SetTimer()’s return value is examined for NULL; if NULL is detected, the screensaver exits with a -1 return code, which terminates the screensaver program. It is a good idea to check this return value, even if there is a low probability that SetTimer() will return NULL.
Besides creating the timer and loading resources, the WM_CREATE message handler initializes an array of ball information structures—one structure per ball. Each structure records the width and height of a ball’s image, the current x and y positions of the ball image’s upper-left corner, the ball’s current horizontal and vertical velocities, a handle to the ball image’s bitmap, and an acceleration value that governs how much the ball speeds up or slows down when it bounces away from a screensaver window border. Acceleration is calculated so that a ball’s velocities don’t overlap with another ball’s velocities. If overlap occurs, ball images can hide other ball images, and you might only see one ball.
ScreenSaverProc() also processes the WM_DESTROY and WM_TIMER messages: WM_DESTROY handles screensaver cleanup; WM_TIMER handles ball drawing and animation.
Drawing and Animating the Balls
Each time Windows sends a WM_TIMER message to ScreenSaverProc(), its message handler responds by performing three tasks: updating the balls’ positions (to handle animation), drawing all balls (and the colorful background) in the screensaver window (either the preview window or the window that covers the entire screen), and advancing the acceleration counter:
case WM_TIMER: { RECT rcClient; HDC hdc = GetDC (hwnd); // Get screensaver window dimensions. GetClientRect (hwnd, &rcClient); // Update ball positions. UpdateBalls (&rcClient); // Draw balls in the screensaver window. DrawBalls (hdc, &rcClient); ReleaseDC (hwnd, hdc); // Update the acceleration counter. Prevent negative numbers by // resetting the counter when it reaches a positive limit. You can // choose any positive number you want by modifying COUNTER_LIMIT. g_iCounter++; if (g_iCounter > COUNTER_LIMIT) g_iCounter = 0; return 0; }
Before I discuss ball animation and how the acceleration counter is used in that context, I want to focus on how the balls are drawn. The following drawing technique relies on a mask bitmap, along with bitwise AND and bitwise OR operations. (Although you might have encountered this technique in the past, a refresher never hurts.) That mask bitmap and one of the ball bitmaps are shown in Figure 2.
Figure 2 The ball mask bitmap is on the left, and the magenta ball bitmap is on the right.
Suppose I invoked the BitBlt() Windows API function with the SRCCOPY operator to copy the magenta ball bitmap (on the right of Figure 2) directly to the screensaver window. Rather than observing colored balls moving across the window, the user would see rectangles, containing colored balls surrounded by black pixels, transiting the window. (This is not desirable from the user’s perspective.)
The user wants to see a round ball—not a rectangle—moving across the window. To satisfy the user, suppose I invoked BitBlt() with the SRCPAINT operator to bitwise OR the magenta ball bitmap’s pixels with the screensaver window’s pixels. This time, the user would see the magenta ball without the surrounding black pixels. However, the ball would be partially transparent: background pixels shine through.
Transparency occurs because SRCPAINT, which sets an output bit to one when either or both input bits are one, merges the bits of the magenta ball with those nonzero bits behind the magenta ball. This does not happen with the black pixels surrounding the ball because black is represented with zero bits, and bitwise ORing these zero bits with the bits of equivalent window pixels results in unchanged window pixels.
The transparency effect can be eliminated if those window pixels under the magenta ball are cleared to black prior to performing the bitwise OR. That way, ORing black pixels with the magenta ball’s pixels will result in only the magenta ball’s pixels appearing—no background pixels will partially appear. This clearing task can be accomplished by invoking BitBlt() with the SRCAND operator and the mask bitmap.
The SRCAND operator sets an output bit to one when both input bits are one. Applying that operator (via BitBlt()) to the mask bitmap and the screensaver window results in the mask’s black pixels clearing equivalent screensaver window pixels to black and the mask’s white pixels leaving screensaver window pixels untouched.
After SRCAND creates an appropriate "black hole" in the screensaver window, SRCPAINT fills that "hole" with the magenta (or other colored) ball. The following excerpt from 4Balls.c’s DrawBalls() function calls BitBlt() with each operator to properly draw the balls. These operators work with a background buffer, rather than the screensaver window, to avoid screen flicker:
// Draw the balls. for (i = 0; i < MAXBALLS; i++) { // Preserve window background surrounding the ball image. SRCAND ands // the mask’s white background, whose bits are ones, with whatever // bits constitute the window background surrounding the ball image. // This preserves that background. // Remove background under the ball image. SRCAND ands the mask’s // black ball image, whose bits are zeroes, with whatever bits // constitute the background under the window’s ball image. This // results in a black round hole in the window. // The SRCAND operation is necessary to prevent a transparency effect. // If this operation was absent, the ball images would appear in the // window, but the window’s background would show through the ball // images. Furthermore, if one of the ball images passed over another // ball image, the underlying ball image would show through. SelectObject (hdcMem, g_hbmMask); BitBlt (hdcBuffer, g_ballInfo [i].x, g_ballInfo [i].y, g_ballInfo [i].width, g_ballInfo [i].height, hdcMem, 0, 0, SRCAND); // Preserve window background surrounding the ball image. SRCPAINT ors // the ball image’s black background, whose bits are zeroes, with // whatever bits constitute the window background surrounding the ball // image. This preserves that background. // Merge ball image with window’s black round hole. SRCPAINT ors the // ball image’s bits with the zero bits constituting the black round // hole, resulting in a solid ball image with no transparency effect. SelectObject (hdcMem, g_ballInfo [i].g_hbmBall); BitBlt (hdcBuffer, g_ballInfo [i].x, g_ballInfo [i].y, g_ballInfo [i].width, g_ballInfo [i].height, hdcMem, 0, 0, SRCPAINT); SelectObject (hdcMem, g_hbmMask); } // Update screensaver window with background buffer. BitBlt (hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);
Before the screensaver window is updated, each ball’s position must be calculated. This task is performed in 4Balls.c’s UpdateBalls() function. For each ball, that function first checks to see whether a global counter (incremented in the WM_TIMER message handler) has reached a certain value. If so, the ball’s acceleration is advanced, or reset so the ball doesn’t move too fast (and be hard to watch).
Moving on, UpdateBalls() adds the ball’s current velocities to its current position. If the new position reaches or exceeds a border, the position is reset, the ball’s direction is reversed, and the ball’s velocity is set to its current acceleration. Hence, the ball either speeds up or slows down. These activities can be seen in the following UpdateBalls() excerpt:
int i; for (i = 0; i < MAXBALLS; i++) { // Modify the acceleration every COUNTER_ITERS iterations. When the // ball’s acceleration exceeds ACCEL_LIMIT, reset the acceleration to // 1 so that balls don’t constantly accelerate. if (g_iCounter % COUNTER_ITERS == 0) { g_ballInfo [i].acceleration++; if (g_ballInfo [i].acceleration > ACCEL_LIMIT) g_ballInfo [i].acceleration = 1; } // Move the ball to a new position based on its velocity. g_ballInfo [i].x += g_ballInfo [i].dx; g_ballInfo [i].y += g_ballInfo [i].dy; // When the ball reaches the left or right edges of the screensaver // window, reverse the ball’s direction, accelerate/decelerate the // ball, and play a sound (if sound isn’t muted) to indicate a bounce. if (g_ballInfo [i].x < 0) { g_ballInfo [i].x = 0; g_ballInfo [i].dx = g_ballInfo [i].acceleration; if (!g_bMuted) PlayWave (g_iSndID); } else if (g_ballInfo [i].x + g_ballInfo [i].width > prc->right) { g_ballInfo [i].x = prc->right - g_ballInfo [i].width; g_ballInfo [i].dx = -g_ballInfo [i].acceleration; if (!g_bMuted) PlayWave (g_iSndID); } // When the ball reaches the top or bottom edges of the screensaver // window, reverse the ball’s direction, accelerate/decelerate the // ball, and play a sound (if sound isn’t muted) to indicate a bounce. if (g_ballInfo [i].y < 0) { g_ballInfo [i].y = 0; g_ballInfo [i].dy = g_ballInfo [i].acceleration; if (!g_bMuted) PlayWave (g_iSndID); } else if (g_ballInfo [i].y + g_ballInfo [i].height > prc->bottom) { g_ballInfo [i].y = prc->bottom - g_ballInfo [i].height; g_ballInfo [i].dy = -g_ballInfo [i].acceleration; if (!g_bMuted) PlayWave (g_iSndID); } }
I’ll discuss the PlayWave() function later. For now, let’s learn how the DrawBalls() function renders the screensaver window’s colorful background.
Creating a Colorful Background
Figure 1 revealed a background that transitions from blue (top) to gold (bottom). This smooth blending of one color into another color is known as a gradient, and the painting technique that creates a gradient is known as gradient painting. Because gradient painting renders more colorful (and interesting, in my opinion) backgrounds than single-color backgrounds, it’s worth learning how to create gradients.
There are many kinds of gradients, including top-to-bottom, left-to-right, and circular. To keep things simple, let’s focus only on a top-to-bottom gradient. An algorithm for creating this gradient begins by defining the red, green, and blue color components for the top and bottom colors; and by defining the number of rows to be painted by this gradient—each row is painted in one of the gradient’s colors.
For each row, the algorithm calculates an intermediate color, starting with the top color and ending near the bottom color. Each of the intermediate colors’ red, green, and blue color components is calculated as the starting color’s red, green, or blue color component (respectively), plus a fraction of the difference between the end and start color components. The DrawBalls() excerpt below demonstrates:
// Paint background buffer using a blue/gold vertical gradient. int r1 = 5, g1 = 35, b1 = 145; int r2 = 167, g2 = 167, b2 = 76; int i; for (i = 0; i < prc->bottom-prc->top+1; i++) { HBRUSH hbr; int r, g, b; RECT rectTemp; r = r1+(i*(r2-r1)/(prc->bottom-prc->top+1)); g = g1+(i*(g2-g1)/(prc->bottom-prc->top+1)); b = b1+(i*(b2-b1)/(prc->bottom-prc->top+1)); rectTemp.left = prc->left; rectTemp.right = prc->right+1; rectTemp.top = prc->top+i; rectTemp.bottom = prc->top+i+1; hbr = CreateSolidBrush (RGB (r, g, b)); FillRect (hdcBuffer, &rectTemp, hbr); DeleteObject (hbr); }
An example will clarify how intermediate colors range from the start color to almost the end color. The example deals with the red color components only, assumes that prc->top is 0, and assumes that prc->bottom is 100. Also, r1 contains 5, prc->bottom-prc->top+1 evaluates to 101, and r2-r1 evaluates to 162. Given these values, examine the table below:
i r --- --- 0 5 1 6 2 8 3 9 4 11 5 13 6 14 7 16 8 17 9 19 10 21 ... ... 90 149 91 150 92 152 93 154 94 155 95 157 96 158 97 160 98 162 99 163 100 165
The table shows that the final row (100) is not painted with a color whose red component equals 167 (the bottom color’s red component). If i were set to 101, the red component would equal 167, but what would row 101 mean? Using the above algorithm, you’ll never reach the bottom color’s components, but this doesn’t matter. The aesthetics of the gradient paint are more important; users won’t notice the difference.
Although users won’t notice whether or not the bottom row is painted with the bottom color, they will notice if two or more balls hit the borders simultaneously and only one richochet wave sound is heard.
Simultaneous Wave Play
Playing a sound at the moment a ball bounces off a border makes 4Balls more interesting. When I first introduced wave-based sound into this screensaver, I naively depended on the PlaySound() Windows API function. It didn’t take me long to find the problem with that function: if two or more balls hit borders at the same time, only one sound is heard. It is better to play multiple wave sounds simultaneously.
To achieve simultaneous wave play, I ended up working with the MMIO and Wave APIs, which are the basis of my own higher-level API for playing multiple wave sounds at the same time. This API consists of functions OpenWave(), PlayWave(), and CloseWave(). Furthermore, several constants have been defined to describe various errors that might occur when you work with my API.
The OpenWave() function takes a string argument that identifies a wave resource in the resource file. If this function successfully opens the wave resource by loading its sound and other details into memory, a positive integer ID returns. That ID is passed to PlayWave() to play the wave and CloseWave() to close the wave. If unsuccessful, a negative error code returns.
Although 4Balls.c calls OpenWave() only once, to load a single wave resource, you could modify this source code to invoke OpenWave() multiple times, passing a different string argument in each function call. That way, you could assign a different wave sound to each ball. If you decide to do this, it is probably best to store the IDs in the ball information structures.
The PlayWave() function takes the ID previously returned by OpenWave() as its sole argument and immediately returns with the wave sound starting to play and a zero return value, or with a negative error code. Call this function multiple times with the same ID to hear simultaneous occurrences of the same sound, or call this function multiple times with different IDs to hear different sounds at the same time.
The CloseWave() function complements OpenWave() by releasing memory allocated to hold the wave sound. As with PlayWave(), CloseWave() takes an ID as its sole argument. Unlike PlayWave(), CloseWave() does not return an error code or success indicator. You must pass a valid ID to CloseWave() because this function doesn’t validate that ID.
Now that you know how to use my API, let’s look behind the scenes at how its functions interact with the MMIO and Wave APIs to accomplish simultaneous wave play. Our examination begins with an array of structures that each record information on a wave’s format, the address of a buffer containing the wave’s bytes, the number of bytes in the buffer, and a flag indicating whether or not the structure is in use:
#define MAXWAVES 4 struct { LPWAVEFORMATEX lpWaveFormatEx; LPSTR lpData; DWORD dwBufferLength; BOOL inUse; } g_waves [MAXWAVES];
The OpenWave() function begins by searching g_waves for an available structure; that is, a structure whose inUse field is set to FALSE. An error code returns if there is no available structure. Otherwise, OpenWave() loads the wave resource identified by its szWaveResName argument, and obtains the size of and a pointer to the loaded wave’s buffer:
hrsrc = FindResource (GetModuleHandle (NULL), szWaveResName, "WAVE"); if (hrsrc == NULL) return ERR_CANNOT_FIND_RES; hglob = LoadResource (GetModuleHandle (NULL), hrsrc); if (hglob == NULL) return ERR_CANNOT_LOAD_RES; dwSize = SizeofResource (GetModuleHandle (NULL), hrsrc); if (dwSize == NULL) return ERR_CANNOT_SIZE_RES; lpWave = LockResource (hglob); if (lpWave == NULL) return ERR_CANNOT_LOCK_RES;
Continuing, OpenWave() initializes an MMIOINFO structure with the wave buffer’s size (dwSize) and address (lpWave). It next invokes mmioOpen() with this structure to open the wave sound from its memory buffer. If the wave sound was to be loaded from a file, this structure would not be needed. Instead, the wave file’s name would be passed to mmioOpen():
ZeroMemory (&mmioinfo, sizeof(MMIOINFO)); mmioinfo.fccIOProc = FOURCC_MEM; mmioinfo.pchBuffer = lpWave; mmioinfo.cchBuffer = dwSize; if ((hmmio = mmioOpen (NULL, &mmioinfo, MMIO_READ)) == NULL) return ERR_CANNOT_OPEN_RES;
Because a wave file is an example of the resource interchange file format, its contents are organized into chunks. MMIO provides the mmioDescend() and mmioAscend() functions for locating chunks and skipping to the end of a chunk, respectively. OpenWave() uses mmioDescend() to locate the fmt chunk within the WAVE chunk:
mmckinfoParentChk.fccType = mmioFOURCC(’W’, ’A’, ’V’, ’E’); if (mmioDescend (hmmio, (LPMMCKINFO) &mmckinfoParentChk, 0, MMIO_FINDRIFF)) { mmioClose (hmmio, 0); return ERR_WAVE_EXPECTED; } mmckinfoChildChk.ckid = mmioFOURCC(’f’, ’m’, ’t’, ’ ’); if (mmioDescend (hmmio, &mmckinfoChildChk, &mmckinfoParentChk, MMIO_FINDCHUNK)) { mmioClose (hmmio, 0); return ERR_FMT_EXPECTED; }
The fmt chunk describes a wave sound’s format in terms of a tag that indicates a waveform format type (pulse code modulation is typical), the number of channels in the waveform data (1 for monaural; 2 for stereo), the sample rate (number of samples per second), and so on. OpenWave() stores this format information in a dynamically allocated structure by invoking MMIO’s mmioRead() function:
if ((lpWaveFormatEx = (LPWAVEFORMATEX) calloc (mmckinfoChildChk.cksize, 1)) == NULL) { mmioClose (hmmio, 0); return ERR_INSUF_MEMORY; } if (mmioRead (hmmio, (HPSTR) lpWaveFormatEx, mmckinfoChildChk.cksize) != (LRESULT) mmckinfoChildChk.cksize) { free (lpWaveFormatEx); mmioClose (hmmio, 0); return ERR_BAD_FMT; }
OpenWave() next obtains the wave sound by calling mmioAscend() to skip to the end of the fmt chunk, by calling mmioDescend() to find the data chunk (the wave sound), and by calling mmioRead() to read that chunk into a dynamically allocated buffer. The buffer’s address and other details store in a g_waves structure, whose index returns as the ID:
mmioAscend (hmmio, &mmckinfoChildChk, 0); mmckinfoChildChk.ckid = mmioFOURCC(’d’, ’a’, ’t’, ’a’); if (mmioDescend (hmmio, &mmckinfoChildChk, &mmckinfoParentChk, MMIO_FINDCHUNK)) { free (lpWaveFormatEx); mmioClose (hmmio, 0); return ERR_BAD_DATA; } if ((lpData = (char *) malloc (mmckinfoChildChk.cksize)) == NULL) { free (lpWaveFormatEx); mmioClose (hmmio, 0); return ERR_INSUF_MEMORY; } if (mmioRead(hmmio, (HPSTR) lpData, mmckinfoChildChk.cksize) != (long) mmckinfoChildChk.cksize) { free (lpData); free (lpWaveFormatEx); mmioClose (hmmio, 0); return ERR_CANNOT_READ; } mmioClose (hmmio, 0); g_waves [i].lpWaveFormatEx = lpWaveFormatEx; g_waves [i].lpData = lpData; g_waves [i].dwBufferLength = mmckinfoChildChk.cksize; g_waves [i].inUse = TRUE; return i;
Now that OpenWave() and the MMIO API have been discussed, consider PlayWave() and the Wave API. PlayWave() begins by dynamically allocating a WAVEHDR structure for identifying the wave sound’s buffer and more. This structure and the previously allocated wave format structure are passed to the Wave API’s waveOutOpen() function to open a waveform output device for playback:
HWAVEOUT hwo; LPWAVEHDR lpWaveHdr; if ((lpWaveHdr = (LPWAVEHDR) malloc (sizeof(WAVEHDR))) == NULL) return ERR_INSUF_MEMORY; ZeroMemory (lpWaveHdr, sizeof(WAVEHDR)); lpWaveHdr->dwBufferLength = g_waves [iWaveID].dwBufferLength; lpWaveHdr->lpData = g_waves [iWaveID].lpData; if (waveOutOpen (&hwo, WAVE_MAPPER, g_waves [iWaveID].lpWaveFormatEx, (DWORD) WaveOutProc, NULL, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { free (lpWaveHdr); return ERR_CANNOT_OPEN_WAVE; }
The hwo argument receives the output device’s handle: It will be passed to subsequent Wave API functions. The WAVE_MAPPER argument tells waveOutOpen() to select a waveform output device capable of playing the specified format, and WaveOutProc() is a callback function that I will discuss later.
Before a wave sound can be written to the waveform output device, its data must be prepared for playback. The Wave API’s waveOutPrepareHeader() function accomplishes this task. If the function is successful, waveOutWrite() is then called to write the sound data to the device, and you hear that sound on your speakers:
if (waveOutPrepareHeader (hwo, lpWaveHdr, sizeof(WAVEHDR)) != MMSYSERR_NOERROR) { free (lpWaveHdr); waveOutClose (hwo); return ERR_CANNOT_PREPARE; } if (waveOutWrite (hwo, lpWaveHdr, sizeof(WAVEHDR)) != MMSYSERR_NOERROR) { free (lpWaveHdr); waveOutClose (hwo); return ERR_CANNOT_WRITE; }
After waveOutWrite() returns, the wave sound starts playing and PlayWave() returns to its caller. At some point, the wave sound will stop playing, the sound data will have to be unprepared, and the waveform output device closed. Because PlaySound() has already returned, some other means is needed to take care of these cleanup tasks. One means is the WaveOutProc() callback function:
static void CALLBACK WaveOutProc (HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2) { LPWAVEHDR lpWaveHdr; switch (uMsg) { case WOM_DONE: lpWaveHdr = (LPWAVEHDR) dwParam1; waveOutUnprepareHeader (hwo, lpWaveHdr, sizeof(WAVEHDR)); waveOutClose (hwo); free (lpWaveHdr); } }
Because WaveOutProc()’s address was passed to WaveOutOpen(), this callback function is called with messages that indicate the progress of playback: WOM_OPEN is sent when the waveform output device is opened, WOM_CLOSE is sent when the waveform output device is closed, and WOM_DONE is called when the wave sound stops playing. Only WOM_DONE is of interest.