Mar 99 Getting Started
Volume Number: 15
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:
SndChannelPtr theChannel;
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:
SndCommand theCommand;
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:
1. Create a sound channel, pairing it with a callback routine.
2. Begin sound playing on the new sound channel.
3. Add a callback command to the sound channel that is playing the sound.
4. 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.
AsynchPlayer
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/, 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/.
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.