Mar 99 Getting Started
Volume Number: 15 (1999)
Issue Number: 3
Column Tag: Getting Started
Asynchronous Sound: Action and Sound
By Dan Parks Sydow
How a Mac program plays sounds that coincide with animated effects
In last month's Getting Started we covered sound-playing basics. The information in that article provided a good framework for including sound in your Mac applications. However, one key use of sound was omitted from that article - the playing of sounds asynchronously. Asynchronous sound playing is the playing of a sound in such a way that other action can take place for the duration of the sound. Rather than have your program - and the user - wait while a sound plays, you'll want other events to take place. Most typically asynchronous sound playing coincides with animation. As some animated effect goes on in a window, an accompanying sound plays. This month we'll tackle asynchronous sound playing so that you can turn your programming efforts into a really polished multimedia Mac application.
When a program is to play sound data that's loaded into memory, the program must create a corresponding sound channel that holds a queue of sound-playing commands for that data. A program that calls SndPlay() and passes a nil pointer as the first parameter is telling the Sound Manager to take care of the allocation of the sound channel. That's exactly what we did in last month's Getting Started column for the SoundPlayer program:
SndPlay( nil, (SndListHandle)theHandle, false );
If your program plays a sound synchronously, as SoundPlayer did, then letting the Sound Manager take care of the task of creating the sound channel makes sense. No action can take place in a program during synchronous sound playing, so there's no need to access the sound channel. If your program is to instead play a sound asynchronously, then your program will want to keep tabs on the sound channel. In particular, your program will want to insert a special command into the sound channel after it starts playing a sound. This command will be used by the sound channel to notify your program when sound playing ends. As long as the sound is still playing your program knows it can carry on with some other action - typically another step, or frame, in an animated sequence that is choreographed with the sound.
To allocate a sound channel, use the Toolbox function SndNewChannel(). A call to this function creates a new sound channel record and returns to your program a pointer to that record:
SndNewChannel( &theChannel, 0, 0, nil );
The first parameter to SndNewChannel() is a pointer to a SndChannelPtr. When the routine completes, the Toolbox will have filled in this first parameter with a pointer to a new sound channel record. The second parameter is a constant that indicates the type of sound data that is to be played on the new channel. Passing a value of 0 means the channel can be used for any type of sound. The third parameter can be used to supply channel initialization information. Again, simply pass a value of 0 if you're uncertain of the exact type of sound that is to be played from the channel. For synchronous sound playing, the last parameter can be nil. For asynchronous sound playing, the last parameter is a pointer to a callback routine. As you'll see ahead, a callback routine is an application-defined function that the system executes when a sound has finished playing on this channel. It's important to take note of that last point: even though you'll write the code for the callback routine and include it in your source code listing with your other application-defined routines, it will be the Sound Manager that invokes function.
When you participate in the allocation of your program's own sound channel (rather than allowing the Sound Manager to do all the work), you gain the power to send sound commands of your choice to the sound channel. A sound channel includes a queue of sound commands. Each sound command can affect the way in which a sound is played. Each sound command has a type and one or two command options. Consider the amplitude sound command, which affects the amplitude (volume) of a sound. For this command the type is ampCmd, the first option holds the amplitude (a short value in the range of 0 to 255), and the second option is ignored. To define a sound command that can be used to set the amplitude of a sound to its maximum, use this code:
theCommand.cmd = ampCmd;
theCommand.param1 = 255;
theCommand.param2 = 0;
The above code sets up a sound command, but it doesn't affect any particular sound channel. To place the command in an existing sound channel, call the Toolbox function SndDoCommand():
SndDoCommand( theChannel, &theCommand, false );
The first parameter to SndDoCommand() is a sound channel pointer. In the above call the sound channel pointer theChannel is the one created earlier in this article with the call to SndNewChannel(). The second parameter is a pointer to a command. Before calling SndDoCommand() you'll want to have created and filled in this command, as shown for the ampCmd in the previous snippet. The last parameter tells the Sound Manager what to do if the specified sound channel's command queue is full. A value of true means an error is to be returned, while a value of false means the Sound Manager is to wait for a free position in the queue.
A call to SndDoCommand() places a command in a sound channel's command queue. Once a sound is played on that sound channel, the command goes into effect. For instance, if the above ampCmd was placed in theChannel, then any subsequent sounds played on that channel would be played at full volume.
Asynchronous Sound and the Callback Routine
So far we've discussed the sound channel and the sound commands that can be placed in the channel - but we haven't seen how those topics tie in with asynchronous sound playing. We're just about there. Before getting into the details of implementing asynchronous sound, let's step back to get an overview of the sound channel, sound commands, asynchronous sound, and something referred to as a callback routine.
To play a sound resource asynchronously, you'll first allocate a new sound channel using SndNewChannel(). In the call to SndNewChannel() you'll specify the name of a callback routine. This callback routine is a function that is to execute when this newly created sound channel finishes playing a sound. Next, you'll load the sound to memory and call SndPlay() to play it. After the sound begins playing, you'll call SndDoCommand() to add a callback command to the command queue of the sound channel that's playing the sound. The timing of the addition of this command is important. Because the sound has started playing, the newly added callback command will be placed at the end of the sound channel's command queue. Doing this means that the sound channel executes this command when the sound has completed.
A callback routine is a simple function that exists to tell your program that a sound has completed playing. When your program gets this information it knows that any action taking place that was to be timed to the playing of the sound should now stop.
Before looking at the code that makes all this asynchronous business possible, let's complete the overview with a list of steps necessary to make sound and animation happen together:
- Create a sound channel, pairing it with a callback routine.
- Begin sound playing on the new sound channel.
- Add a callback command to the sound channel that is playing the sound.
- Enter a loop, with each pass through the loop performing a step in an animation.
The above steps seem straightforward enough, but the last step may puzzle you a bit. Performing animation using a loop is simple enough - you can just move a picture a pixel or two at each pass through the loop. But as the loop executes, how does the loop know when the corresponding sound has ended, and that animation should cease? That's the job of the callback routine and the system. When the sound ends, the system executes the callback routine - even if your application is in the middle of an animation loop. Your callback routine will be written such that it sets a global flag indicating that sound playing has ended. It will be this flag that the animation loop looks at and relies on in order to know whether to continue or to stop animation.
This month's program is called AsynchPlayer. If you read last month's column, then much of the code will look familiar to you - though as you'll see ahead we'll be adding a few twists. Running AsynchPlayer results in the appearance of a menu bar and a window. Of significance in the menu bar is the Sound menu. This menu has a single item - Play and Move. Choosing this item causes the program to play a sound and move a picture. The sound is the distinctive noise of a helicopter flying, and the picture is that of a helicopter. When the Play and Move item is selected the helicopter appears and moves from right to left as the sound plays. When the sound stops, so does the animation. Figure 1 shows the helicopter as it crosses the window. After running the animation a few times, choose Quit from the File menu to end the program.
Figure 1. The AsynchPlayer window.
Creating the AsynchPlayer Resources
Start by opening your CodeWarrior development folder and creating a folder named AsynchPlayer. Launch ResEdit and create a new resource file named AsynchPlayer.rsrc inside the AsynchPlayer folder. Figure 2 shows the seven types of resources used by AsynchPlayer. If you've been keeping up with Getting Started articles, each type should be familiar to you.
Figure 2. The AsynchPlayer resources.
AsynchPlayer includes a single snd resource - as shown in Figure 2. This sound resource is a digitized sound obtained from an external source - it can't be created from within ResEdit. This month's project is available for download from MacTech's ftp site at ftp://ftp.mactech.com/src/mactech/volume15_1999/15.03.sit, and it includes a single system sound file named Helicopter. Use ResEdit to open that file, copy its one snd resource, and then paste that resource into the AsynchPlayer.rsrc file. From last month's column you know that ResEdit won't display anything useful about a sound resource (since it's difficult to graphically display a sound), so there's no need to double-click on the snd resource to peek at what's inside.
AsynchPlayer needs one picture resource in order to carry out the animation. If you're artistically inclined, or if you have a large clip art collection, you'll be able to come up with a helicopter picture. Otherwise use the Helicopter.PICT file included in project that can be downloaded from MacTech's ftp site at ftp://ftp.mactech.com/src/mactech/volume15_1999/15.03.sit. In any case, get the picture to the clipboard and paste it into the resource file. As shown in Figure 3, the resulting PICT resource should have an ID of 128.
Figure 3. The picture resource used in animation.
The one WIND resource is used to create the window that displays the moving helicopter. This window won't be movable, so you don't have to be too selective about what type of window you create. The program won't care where the window gets positioned on the screen, but it will be expecting the window to be about 500 pixels in width and 150 pixels in height - so do use those values. Make sure the WIND has an ID of 128.
The one ALRT and one DITL resource used by AsynchPlayer are the same ones used in the last several Getting Started examples. These resources are used in the display of an error-handling alert displayed by the program's DoError() routine.
Figure 4 shows the four MENU resources the AsynchPlayer program uses. After creating the MENU resources, create a single MBAR resource that includes the ID of each of the four menus.
Figure 4. The AsynchPlayer menu resources.
That completes the AsynchPlayer.rsrc file. Now save the file and quit ResEdit - we're ready to write some code.
Creating the AsynchPlayer Project
Create a new project by launching CodeWarrior and then choosing New Project from the File menu. Base the file on the MacOS:C_C++:MacOS Toolbox:MacOS Toolbox Multi-Target stationary. Uncheck the Create Folder check box before clicking the OK button. Give the project a name of AsynchPlayer.mcp, and make sure the AsynchPlayer folder is set as the project's destination.
Next, add the AsynchPlayer.rsrc file to the project window and remove the SillyBalls.rsrc file. Since the AsynchPlayer project doesn't use of any of the standard ANSI libraries, go ahead and remove the ANSI Libraries folder if you feel so inclined.
Now choose New from the File menu to create a new, empty source code window. Save it with the name AsynchPlayer.c. Choose Add Window from the Project menu to add this empty file to the project. Remove the SillyBalls.c placeholder file from the project window. Now get ready to type some source code. Or, save some work and download the entire AsynchPlayer project from MacTech's ftp site at ftp://ftp.mactech.com/src/mactech/volume15_1999/15.03.sit.
Walking Through the Source Code
Now, in its entirety - the AsynchPlayer code. AsynchPlayer begins with the usual parade of constant definitions. Many of the constants define resource IDs, including ksnd_ResID for the sound resource and kPICTResID for the picture.
/********************* constants *********************/
#define kWINDResID 128
#define kMBARResID 128
#define kALRTResID 128
#define ksnd_ResID 12000
#define kPICTResID 128
#define kSleep 7
#define kMoveToFront (WindowPtr)-1L
#define mApple 128
#define iAbout 1
#define mFile 129
#define iQuit 1
#define mSound 131
#define iMovePlay 1
AsynchPlayer needs several global variables, including the always-present gDone flag that tells the program that the user has elected to quit. The Boolean variable gCallbackExecuted tells the program whether a call to SndPlay() has just completed (true), or is still executing (false). The variable gSoundChannel is a sound channel pointer that we'll use to associate a callback routine with the execution of a call to SndPlay(). Variable gSoundHandle serves as a reference to the sound resource data that gets loaded to memory. The PicHandle variable gHelicopterPicture serves as a reference to the picture resource data that gets loaded to memory. Variable gPictRect is the bounding rectangle of the helicopter picture. The last four variables (gPictStartX, gPictStartY, gPictWidth, and gPictHeight) define the starting coordinates for the helicopter picture. Each time the user chooses Play and Move from the Sound menu, the picture begins its animation at these coordinates.
/****************** global variables *****************/
Boolean gCallbackExecuted = false;
Handle gSoundHandle = nil;
short gPictStartX = 500;
short gPictStartY = 35;
short gPictWidth = 256;
short gPictHeight = 82;
Next come the program's function prototypes.
/********************* functions *********************/
void ToolBoxInit( void );
void MenuBarInit( void );
void InitWindow( void );
pascal void SndChannelCallback( SndChannelPtr, SndCommand);
void PlaySoundResource( void );
void AnimateWhileSoundPlays( void );
void EventLoop( void );
void DoEvent( EventRecord *eventPtr );
void HandleMouseDown( EventRecord *eventPtr );
void HandleMenuChoice( long menuChoice );
void HandleAppleChoice( short item );
void HandleFileChoice( short item );
void HandleSoundChoice( short item );
void DoError( Str255 errorString );
The main() function begins by initializing the Toolbox. Just as we did last month, a check is then made to ensure that the user has version 3.0 or later of the Sound Manager. That version of the Sound Manager holds a lot of sound-related goodies your program may want to take advantage of, so if the user doesn't have 3.0 or later, the application-defined routine DoError() posts a message and exits the program.
/************************ main ***********************/
void main( void )
theSndMgrVers = SndSoundManagerVersion();
if ( theSndMgrVers.majorRev < 3 )
DoError( "\pSound Manager is outdated" );
Before carrying on with the rest of the initializations, main() makes a call to the Toolbox function Gestalt() to see if the user's machine is equipped with a PowerPC processor. While it's possible to achieve asynchronous sound on a 68K-based Mac, the details of achieving that feat are out of the scope of this article. Apple no longer produces 68K-based Macs, owners of older Macs are upgrading to Power Macs, and a great deal of new Mac software relies on very fast processing power. With that in mind, expect to see support for 68K-based Macs waning in the future. If you need to support 68K-based machines for your own asynchronous sound-playing application, read up on the topic of the A5 World, and dig into the Sound volume of Inside Macintosh.
Gestalt( gestaltSysArchitecture, &theResponse );
if ( theResponse != gestaltPowerPC )
DoError( "\pThis program only runs on Power Macs" );
The ToolBoxInit() and MenuBarInit() functions are the same as prior versions.
/******************** ToolBoxInit ********************/
void ToolBoxInit( void )
InitGraf( &qd.thePort );
InitDialogs( nil );
/******************** MenuBarInit ********************/
void MenuBarInit( void )
menuBar = GetNewMBar( kMBARResID );
SetMenuBar( menuBar );
menu = GetMenuHandle( mApple );
AppendResMenu( menu, 'DRVR' );
InitWindow() creates a new window from the one WIND resource, then calls ShowWindow() to show the window and SetPort() to ensure that drawing takes place to this window. After that the PICT resource is loaded into memory and the returned picture handle is stored in the global variable gHelicopterPicture for later use during animation.
void InitWindow( void )
window = GetNewWindow( kWINDResID, nil, (WindowPtr)-1L );
ShowWindow( window );
SetPort( window );
gHelicopterPicture = GetPicture( kPICTResID );
if ( gHelicopterPicture == nil )
DoError( "\pAttempt to load picture resource failed" );
Next we write the callback routine that's to be used by a call to SndPlay(). The format of the callback routine is: the pascal keyword, a return type of void, the function name, and a SndChannelPtr parameter and a SndCommand parameter. The body of the callback routine can include any code you want, but it typically just toggles the value a global variable from false to true. That's what we do here with the Boolean variable gCallbackExecuted.
pascal void SndChannelCallback( SndChannelPtr theChannel,
SndCommand theCommand )
gCallbackExecuted = true;
If gCallbackExecuted is set to false at some point before sound playing starts, then the program will know sound is playing and will animate - and continue to animate - until it sees that gCallbackExecuted takes on the value true. That happens when the sound ends and the system automatically invokes our SndChannelCallback() callback routine.
Now, the meat of the program - PlaySoundResource(). In last month's version of this sound resource playing routine, the following occurred: a sound resource was loaded, the resulting handle was locked, SndPlay() played the sound, the handle was unlocked, and the handle was released from memory. All of these actions again take place this month, but because the sound is now being played asynchronously we need to add a few more steps.
PlaySoundResource() begins with a call to the Toolbox routine NewSndCallBackProc(). This function creates a universal procedure pointer (UPP) to a callback function. In short, we've just created a pointer to our own application-defined function named SndChannelCallback(). This UPP will be used just ahead.
/****************** PlaySoundResource ****************/
void PlaySoundResource( void )
callBackUPP = NewSndCallBackProc( SndChannelCallback );
Next, a call to SndNewChannel() is made to create a new sound channel. When a sound channel is to be used to play asynchronous sound, a universal procedure pointer to a callback routine should be the last parameter. We've just created that UPP, so we're all set:
theErr = SndNewChannel( &gSoundChannel, 0, 0,
if ( theErr != noErr )
DoError( "\pNew sound channel failed" );
Now the data from the sound resource is loaded to memory. We'll save the resulting handle in the global variable gSoundHandle:
gSoundHandle = GetResource( 'snd ', ksnd_ResID );
if ( gSoundHandle == nil )
DoError( "\pAttempt to load sound resource failed" );
When a sound is playing asynchronously, just about any action can take place. Our very short AsynchPlayer example program doesn't do anything too tricky, so we can be reasonably sure nothing bad might take place during sound play. But a larger program could include code that closes an open resource file. If that file is the one that holds the sound resource that's being played, things could get ugly. So when playing a sound asynchronously it's wise to take the precaution of detaching the sound resource from its resource file. Even though the sound resource data has been loaded to memory, there could still be a dependency on information in the resource itself. Detaching the resource removes any dependency on the resource file.
DetachResource( gSoundHandle );
Next, lock the sound handle - just as was done in last month's example. That prevents the sound data from getting moved about in memory as the sound plays.
HLock( gSoundHandle );
Now set about playing the sound by calling SndPlay(). Here we're using SndPlay() for asynchronous sound playing so we need to provide our own sound channel pointer as the first parameter, a handle to the sound to play as the second parameter, and a value of true (for "yes, we're playing asynchronous sound") for the last parameter.
theErr = SndPlay( gSoundChannel,
(SndListHandle)gSoundHandle, true );
if ( theErr != noErr )
DoError( "\pPlaying of sound failed" );
Now, with sound playing started, we need to slip a sound command into the command queue of the sound channel that's just begun playing the sound. Earlier in this article you saw how to do that: define the fields of a SndCommand variable and call SndDoCommand() to place the command in a sound channel's queue. Here we define the sound command to be of the type callBackCmd. For this type of command both options (the param1 and param2 fields) are unused. The call to SndDoCommand() places theCommand in the sound channel pointed to by gSoundChannel. The third parameter value of false tells the Sound Manager to wait for an opening in the sound channel's queue if it is currently full (with room for 128 commands, an unlikely event).
theCommand.cmd = callBackCmd;
theCommand.param1 = 0;
theCommand.param2 = 0;
theErr = SndDoCommand(gSoundChannel, &theCommand, false);
if ( theErr != noErr )
DoError( "\pSound command failed" );
Now that sound has started, it's also time to start the animation. We handle that in a separate application-defined function named AnimateWhileSoundPlays(). A call to that function ends the PlaySoundResource() code.
When sound starts, so does animation. For our simple example all we need to do to achieve an animated effect is to offset the picture boundary rectangle by one pixel in the horizontal direction. A call to OffsetRect() does just that. At each pass through the infinite while loop, gPictRect is repositioned one pixel to the left. This assumes, of course, that the sound is still playing. We'll know this to be the case if gCallbackExecuted remains false (the value the variable was initialized to when it was declared). As long as this flag is false (and note that its value is checked at each pass through the loop), animation continues.
void AnimateWhileSoundPlays( void )
while ( true )
if ( gCallbackExecuted == false )
OffsetRect( &gPictRect, -1, 0 );
DrawPicture( gHelicopterPicture, &gPictRect );
Once sound playing has completed, the system invokes the callback routine. That function sets gCallbackExecuted to true, which gets picked up on by the loop in AnimateWhileSoundPlays(). With the callback routine executed, it's time to wrap things up and end the animation loop. A call to HUnlock() unlocks the memory that holds the sound data, and a call to ReleaseResource() frees up this memory. We then dispose of the sound channel - a necessary step because we were the ones who allocated it. The global variable gSoundChannel is set to nil since it no longer points to valid data. The global flag gCallbackExecuted is set to false in preparation for the next call to SndPlay() (an act which occurs if the user again chooses Play and Move from the Sound menu). Finally, return breaks the otherwise infinite while loop and ends the AnimateWhileSoundPlays() function.
HUnlock( gSoundHandle );
ReleaseResource( gSoundHandle );
gSoundHandle = nil;
theErr = SndDisposeChannel( gSoundChannel, true );
if ( theErr != noErr )
DoError( "\pDisposing of sound channel failed" );
gSoundChannel = nil;
gCallbackExecuted = false;
That just about does it for the new stuff. The remaining AsynchPlayer code is the basic stuff found in just about every Mac program. The only new code is a couple of minor additions to the code that handles a selection from the Sound menu.
/********************** EventLoop ********************/
void EventLoop( void )
gDone = false;
while ( gDone == false )
if ( WaitNextEvent( everyEvent, &event, kSleep, nil ) )
DoEvent( &event );
/*********************** DoEvent *********************/
void DoEvent( EventRecord *eventPtr )
switch ( eventPtr->what )
HandleMouseDown( eventPtr );
theChar = eventPtr->message & charCodeMask;
if ( (eventPtr->modifiers & cmdKey) != 0 )
HandleMenuChoice( MenuKey( theChar ) );
BeginUpdate( (WindowPtr)(eventPtr->message) );
EndUpdate( (WindowPtr)(eventPtr->message) );
/******************* HandleMouseDown *****************/
void HandleMouseDown( EventRecord *eventPtr )
thePart = FindWindow( eventPtr->where, &window );
switch ( thePart )
menuChoice = MenuSelect( eventPtr->where );
HandleMenuChoice( menuChoice );
case inSysWindow :
SystemClick( eventPtr, window );
Here in HandleMenuChoice() we've added a switch case section for the handling of a selection from the Sound menu.
/******************* HandleMenuChoice ****************/
void HandleMenuChoice( long menuChoice )
if ( menuChoice != 0 )
menu = HiWord( menuChoice );
item = LoWord( menuChoice );
switch ( menu )
HandleAppleChoice( item );
HandleFileChoice( item );
HandleSoundChoice( item );
HiliteMenu( 0 );
/****************** HandleAppleChoice ****************/
void HandleAppleChoice( short item )
switch ( item )
SysBeep( 10 );
appleMenu = GetMenuHandle( mApple );
GetMenuItemText( appleMenu, item, accName );
accNumber = OpenDeskAcc( accName );
/******************* HandleFileChoice ****************/
void HandleFileChoice( short item )
switch ( item )
gDone = true;
When the user chooses Play and Move from the Sound menu, HandleSoundChoice() gets invoked. Before calling PlaySoundResource() to start the sound and animation, the global rectangle that holds the starting boundaries of the picture is reset. This places the picture at the far right of the window so it can begin its journey to the left side of the window.
/****************** HandleSoundChoice ****************/
void HandleSoundChoice( short item )
switch ( item )
SetRect( &gPictRect, gPictStartX, gPictStartY,
gPictStartX + gPictWidth,
gPictStartY + gPictHeight );
/*********************** DoError *********************/
void DoError( Str255 errorString )
ParamText( errorString, "\p", "\p", "\p" );
StopAlert( kALRTResID, nil );
Run AsynchPlayer by selecting Run from the Project menu. After the code compiles, a menu bar and an empty window appear. Choose Play and Move from the Sound menu to confirm that the sound that plays coincides with the animation that takes place. Depending on the speed of your Mac, the helicopter may or may not make it off the left edge of the window. If the helicopter does exit the window while the sound is still playing, you may want to make the window larger (and move the starting point of the picture farther to the right) so that you can verify that when the sound stops the animation ends as well. When you're satisfied that all is working as intended, choose Quit from the File menu to end the program.
Till Next Month...
The purpose of playing a sound asynchronously is to allow sound and animation to occur simultaneously. The purpose of the callback routine is to free your program from having to somehow predetermine the duration of a sound. When a sound ends playing, the callback routine in effect tells the program that this is the case. You can prove that the callback routine works by opening the standalone AsynchPlayer application itself from a resource editor such as ResEdit. Give the helicopter snd resource a different ID (any ID will do - you're just changing it so that the program won't use this resource). Now add a different snd resource to the program and give it an ID of 12000 - the ID of the snd resource the program is expecting to find. Save AsynchPlayer and close it. Now run AsynchPlayer and choose Play and Move from the Sound menu. Doing that moves the helicopter, but now the animation will be of a different length then before. Regardless of the length of the new sound, the animation ends when the sound ends.
AsynchPlayer demonstrates asynchronous sound playing using a sound resource. If you want to instead use a sound file, read last month's Getting Started article to learn about sound files and the SndStartFilePlay() routine, and then read the Sound volume of Inside Macintosh to learn about completion routines. Like the callback routine used with SndPlay(), the completion routine used with SndStartFilePlay() executes when a sound has completed playing. In fact, it turns out that the callback routine you just learned about is a type of completion routine.
Finally, you may want to try asynchronous sound playing with a different animation technique. Here, because the focus was on sound playing, we used the simple trick of repositioning a picture to achieve animation. Try changing the AnimateWhileSoundPlays() function so that it uses offscreen bitmaps to achieve smooth, flicker-free animation that doesn't obscure a window's background. You can read up on offscreen bitmaps in the Getting Started column in both the October and the December issues of last year.
There are plenty of other sound-related tricks and techniques to learn about in the Sound volume of Inside Macintosh. Go ahead and read up on sound, and experiment with the AsynchPlayer code until we cover a new topic next month...