Home > Articles > Programming > User Interface (UI)

Have Fun with the Custom Screensavers Library

📄 Contents

  1. 4Balls Screensaver
  2. Conclusion
Screensavers should be fun to watch. Unlike most rather boring screensavers, the 4Balls screensaver in Jeff Friesen's new article is anything but dull. You learn how to draw bitmaps using masks and bitwise operations, how to animate these bitmaps using simple physics, how to render colorful backgrounds with gradient paints, and how to play multiple wave sounds simultaneously.
Like this article? We recommend

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 01

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:

   // 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),

     // 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.

   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 02

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,

   // 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,

   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);
   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);
   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

  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)

hglob = LoadResource (GetModuleHandle (NULL), hrsrc);
if (hglob == NULL)

dwSize = SizeofResource (GetModuleHandle (NULL), hrsrc);
if (dwSize == NULL)

lpWave = LockResource (hglob);
if (lpWave == NULL)

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)

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);

mmckinfoChildChk.ckid = mmioFOURCC(’f’, ’m’, ’t’, ’ ’); 
if (mmioDescend (hmmio, &mmckinfoChildChk, &mmckinfoParentChk,
  mmioClose (hmmio, 0);

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);

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,
  free (lpWaveFormatEx);
  mmioClose (hmmio, 0);
  return ERR_BAD_DATA;

if ((lpData = (char *) malloc (mmckinfoChildChk.cksize)) == NULL)
  free (lpWaveFormatEx);
  mmioClose (hmmio, 0);

if (mmioRead(hmmio, (HPSTR) lpData, mmckinfoChildChk.cksize) !=
       (long) mmckinfoChildChk.cksize)
  free (lpData);
  free (lpWaveFormatEx);
  mmioClose (hmmio, 0);

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:


if ((lpWaveHdr = (LPWAVEHDR) malloc (sizeof(WAVEHDR))) == NULL)

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,
  free (lpWaveHdr);

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)) !=
  free (lpWaveHdr);
  waveOutClose (hwo);

if (waveOutWrite (hwo, lpWaveHdr, sizeof(WAVEHDR)) != MMSYSERR_NOERROR)
  free (lpWaveHdr);
  waveOutClose (hwo);

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.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information

To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.


Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.


If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information

Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.


This site is not directed to children under the age of 13.


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information

If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information

Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents

California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure

Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact

Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice

We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020