Previous Book Contents Book Index Next

Inside Macintosh: Sound /
Chapter 2 - Sound Manager / Using the Sound Manager


Legacy Documentclose button

Important: Inside Macintosh: Sound is deprecated as of Mac OS X v10.5. For new audio development in Mac OS X, use Core Audio. See the Audio page in the ADC Reference Library.

Playing Sounds Asynchronously

The Sound Manager currently allows you to play sounds asynchronously only if you allocate sound channels yourself, using techniques described in "Managing Sound Channels" on page 2-19. But if you use such a technique, your application will need to dispose of a sound channel whenever the application finishes playing a sound. In addition, your application might need to release a sound resource that you played on a sound channel.

To avoid the problem of not knowing when to dispose of a sound channel playing a sound asynchronously, your application could simply allocate a single sound channel when it starts up (or receives a resume event) and dispose of the channel when the user quits (or the application receives a suspend event). However, this solution will not work if you need to release a resource when a sound finishes playing. Also, you might not want to keep a sound channel allocated when you are not using it. For instance, you might want to use the memory taken up by a sound channel for other tasks when no sound is playing.

Your application could call the SndChannelStatus function once each time through its main event loop to determine if a channel is still making sound. When the scBusy field of the sound channel status record becomes FALSE, your application could then dispose of the channel. This technique is easy, but calling SndChannelStatus frequently uses up processing time unnecessarily.

The Sound Manager provides other mechanisms that allow your application to find out when a sound finishes playing, so that your application can arrange to dispose of sound channels no longer being used and of other data (such as a sound resource) that you no longer need after disposing of a channel. If you are using the SndPlay function or low-level commands to play sound in a channel, then you can use callback procedures. If you are using the SndStartFilePlay function to play sound in a channel, then you can use completion routines. The following sections illustrate how to use callback procedures and completion routines.

Note
Callback procedures are a form of completion routine. However, for clarity, this section uses the terminology "completion routine" only for the routines associated with the SndStartFilePlay function.

Using Callback Procedures

This section shows how you can use callback procedures to play one sound asynchronously at a given time. "Managing Multiple Sound Channels" on page 2-53 expands the techniques in this section to show how you can play several asynchronous sounds simultaneously.

The SndNewChannel function allows you to associate a callback procedure with a sound channel. For example, the following code opens a new sound channel for which memory has already been allocated and associates it with the callback procedure MyCallBack:

myErr := SndNewChannel(gSndChan, sampledSynth, initMono, @MyCallback);
After filling a channel created by SndNewChannel with various commands to create sound, you can then issue a callBackCmd command to the channel. When the Sound Manager encounters a callBackCmd command, it executes your callback procedure. Thus, by placing the callBackCmd command last in a channel, you can ensure that the Sound Manager executes your callback procedure only after it has processed all of the channel's other sound commands.

Note
Be sure to issue callBackCmd commands with the SndDoCommand function and not the SndDoImmediate function. If you issue a callBackCmd command with SndDoImmediate, your callback procedure might be called before other sound commands you have issued finish executing.
A callback procedure has the following syntax:

PROCEDURE MyCallBack (chan: SndChannelPtr; cmd: SndCommand);
Because the callback procedure executes at interrupt time, it cannot access its application global variables unless the application's A5 world is set correctly. (For more information on the A5 world, see the chapter "Memory Management Utilities" in Inside Macintosh: Memory.) When called, the callback procedure is passed two parameters: a pointer to the sound channel that received the callBackCmd command and the sound command that caused the callback procedure to be called. Applications can use param1 or param2 of the sound command as flags to pass information or instructions to the callback procedure. If your callback procedure is to use your application's global data storage, it must first reset A5 to your application's A5 and then restore it on exit. For example, Listing 2-18 illustrates how to set up a callBackCmd command that contains the required A5 information in the param2 field. The MyInstallCallback function defined there must be called at a time when your application's A5 world is known to be valid.

Listing 2-18 Issuing a callback command

FUNCTION MyInstallCallback (mySndChan: SndChannelPtr): OSErr;
CONST
   kWaitIfFull = TRUE;           {wait for room in queue}
VAR
   mySndCmd:   SndCommand;       {a sound command}
BEGIN
   WITH mySndCmd DO
   BEGIN
      cmd := callBackCmd;        {install the callback command}
      param1 := kSoundComplete;  {last command for this channel}
      param2 := SetCurrentA5;    {pass the callback the A5}
   END;
   MyInstallCallback := SndDoCommand(mySndChan, mySndCmd, kWaitIfFull);
END;
In this function, kSoundComplete is an application-defined constant that indicates that the requested sound has finished playing. You could define it like this:

CONST
   kSoundComplete          = 1;     {sound is done playing}
Because param2 of a sound command is a long integer, Listing 2-18 uses it to pass the application's A5 to the callback procedure. That allows the callback procedure to gain access to the application's A5 world.

Note
You can also pass information to a callback routine in the userInfo field of the sound channel.
The sample callback procedure defined in Listing 2-19 can thus set A5 to access the application's global variables.

Listing 2-19 Defining a callback procedure

PROCEDURE MyCallback (theChan: SndChannelPtr; theCmd: SndCommand);
VAR
   myA5:          LongInt;
BEGIN
   IF theCmd.param1 = kSoundComplete THEN
   BEGIN
      myA5 := SetA5(theCmd.param2);    {set my A5}
      gCallbackPerformed := TRUE;      {set a global flag}
      myA5 := SetA5(myA5);             {restore the original A5}
   END;
END;
WARNING
Callback procedures are called at interrupt time and therefore must not attempt to allocate, move, or dispose of memory, dereference an unlocked handle, or call other routines that do so. Also, assembly-language programmers should note that a callback procedure is a Pascal procedure and must preserve all registers other than A0-A1 and D0-D2.
Callback procedures cannot dispose of channels themselves, because that involves disposing of memory. To circumvent this restriction, the callback procedure in Listing 2-19 simply sets the value of a global flag variable that your application defines. Then, once each time through its main event loop, your application must call a routine that checks to see if the flag is set. If the flag is set, the routine should dispose of the channel, release any other memory allocated specifically for use in the channel, and reset the flag variable. Listing 2-20 defines such a routine. Your application should call it once each time through its main event loop.

Listing 2-20 Checking whether a callback procedure has executed

PROCEDURE MyCheckSndChan;
CONST
   kQuietNow = TRUE;                {need to quiet channel?}
VAR
   myErr:   OSErr;
BEGIN
   IF gCallbackPerformed THEN       {check global flag}
   BEGIN                            {channel is done}
      gCallbackPerformed := FALSE;  {reset global flag}
      IF gSndChan^.userInfo <> 0 THEN
      BEGIN                         {release sound data}
         HUnlock(Handle(gSndChan^.userInfo));
         HPurge(Handle(gSndChan^.userInfo));
      END;
      myErr := MyDisposeSndChannel(gSndChan, kQuietNow);
      gSndChan := NIL;              {set pointer to NIL}
   END;
END;
The MyCheckSndChan procedure defined in Listing 2-20 checks the userInfo field of the sound channel to see if it contains the address of a handle. Thus, if you would like the MyCheckSndChan procedure to release memory associated with a sound handle, you need only put the address of the handle in the userInfo field of the sound channel. (If you do not want the MyCheckSndChan procedure to release memory associated with a handle, then you should set the userInfo field to 0 when you allocate the channel. The MyCreateSndChannel function defined in Listing 2-1 on page 2-20 automatically sets this field to 0.) After releasing the memory associated with the sound handle, the MyCheckSndChan procedure calls the MyDisposeSndChannel function (defined in Listing 2-3 on page 2-25) to release the memory occupied by both the sound channel and the sound channel record.

To ensure that the MyCheckSndChan procedure defined in Listing 2-20 does not attempt to dispose a channel before you have created one, you should initialize the gCallbackPerformed variable to FALSE. Also, you should initialize the gSndChan variable to NIL, so that other parts of your application can check to see if a sound is playing simply by checking this variable. For example, if your application must play a sound but another sound is currently playing, you might ensure that the application gives priority to the newer sound by stopping the old one. Listing 2-21 defines a procedure that stops the sound that is playing.

Listing 2-21 Stopping a sound that is playing asynchronously

PROCEDURE MyStopPlaying;
BEGIN
   IF gSndChan <> NIL THEN          {is sound really playing?}
      gCallbackPerformed := TRUE;   {set global flag}
   MyCheckSndChan;                  {call routine to do disposing}
END;
Once you have defined a callback procedure, a routine that installs the callback procedure, a routine that checks the status of the callback procedure, and a routine that can stop sound play, you need only allocate a sound channel, call the SndPlay function, and install your callback procedure to start an asynchronous sound play. Listing 2-22 defines a procedure that starts an asynchronous play.

Listing 2-22 Starting an asynchronous sound play

PROCEDURE MyStartPlaying (mySndID: Integer);
CONST
   kAsync = TRUE;             {play is asynchronous}
VAR
   mySndHandle:   Handle;     {handle to an 'snd ' resource}
   myErr:         OSErr;
BEGIN
   IF gSndChan <> NIL THEN                {check if channel is active}
      MyStopPlaying;
   gSndChan := MyCreateSndChannel(0, 0, @MyCallbackProc, stdQLength);
   mySndHandle := GetResource('snd ', mySndID);
   IF (mySndHandle <> NIL) AND (gSndChan <> NIL) THEN
   BEGIN                      {start sound playing}
      DetachResource(mySndHandle);        {detach resource from file}
                                          {remember to release sound handle}
      gSndChan^.userInfo := LongInt(mySndHandle);
      HLock(mySndHandle);                 {lock the resource data}
      myErr := SndPlay(gSndChan, mySndHandle, kAsync);
      IF myErr = noErr THEN
         myErr := MyInstallCallback(gSndChan);
      IF myErr <> noErr THEN
         DoError(myErr);
   END;
END;
The MyStartPlaying procedure uses the MyCreateSndChannel function defined in Listing 2-1 to create a sound channel, requesting that the function allocate a standard-sized sound channel command queue. By using such a queue, you can be sure that your application can play any sound resource that contains up to 127 sound commands. If you are sure that your application will play only sampled-sound resources created by the Sound Input Manager, you should request a queue of only two sound commands, thereby leaving enough room for just the bufferCmd command contained within the sound resource and the callBackCmd command that your application issues.

Before playing the sound, the MyStartPlaying procedure defined in Listing 2-22 detaches the sound resource from its resource file after loading it. This is important if the resource file could close while the sound is still playing, or if your application might create another sound channel to play the same sound resource while the sound is still playing.

Synchronizing Sound With Other Actions

If your application uses callback procedures to play sound asynchronously, you might wish to synchronize sound play with other activity, such as an onscreen animation.

Callback procedures allow your application to do that by using different constant values in the param1 field of the callback command. For example, you could define a constant kFirstSoundFinished to signal to your application that the first of a series of sounds has finished playing. Then, your callback procedure could set an appropriate global flag depending on whether the param1 field equals kFirstSoundFinished, kSoundComplete, or some other constant that your application defines. Finally, a procedure that you call once each time through your application's event loop could check to see which of the various global flag variables are set and respond appropriately. Meanwhile, sound continues to play.

Managing an Asynchronous Play From Disk

The Sound Manager allows you to play a sound file asynchronously with the SndStartFilePlay function by defining a completion routine that sets a global flag to alert the application to dispose of the sound channel when the sound is done playing. Completion routines are thus similar to callback procedures, but they are easier to use in that you do not need to install them. The Sound Manager automatically executes them when a play from disk ends, whether it has ended because the application called the SndStopFilePlay function, because the application disposed of the sound channel in which the sound was playing, or because the sound has finished playing.

You define a completion routine like this:

PROCEDURE MySoundCompletionRoutine (chan: SndChannelPtr);
Note that unlike callback procedures, completion routines have only one parameter, a pointer to a sound channel. Thus, for the completion routine to set the application's A5 world properly, you should pass the value of the application's A5 in the userInfo field of the sound channel, like this:

gSndChan^.userInfo := SetCurrentA5;
Then your completion routine can look in the userInfo field of the sound channel to set A5 correctly before it can access any application global variables. Listing 2-23 defines a completion routine that sets A5 correctly.

Listing 2-23 Defining a completion routine

PROCEDURE MySoundCompletionRoutine (chan: SndChannelPtr);
VAR
   myA5:    LongInt;
BEGIN
   myA5 := SetA5(chan^.userInfo);      {set my A5}
   gCompletionPerformed := TRUE;       {set a global flag}
   myA5 := SetA5(myA5);                {restore the original A5}
END;
The completion routine defined in Listing 2-23 sets a global flag variable to indicate that the completion routine has been called. To start a sound file playing, you can use a routine analogous to that defined in Listing 2-22, but when allocating a sound channel, you need only allocate a queue of a single sound command. You can than use a procedure analogous to that defined in Listing 2-20 to check the flag once each time through the application's event loop and dispose of the sound channel if the flag is set.

If you do use the SndStartFilePlay function to play sounds asynchronously, then you can pause, restart, and stop play simply by using the SndPauseFilePlay and SndStopFilePlay functions.

You use SndPauseFilePlay to temporarily suspend a sound from playing. If a sound is playing and you call SndPauseFilePlay, then the sound is paused. If the sound is paused and you call SndPauseFilePlay again, then the sound resumes playing. Hence, the SndPauseFilePlay routine acts like a pause button on a tape player, which toggles the tape between playing and pausing. (You can determine the current state of a play from disk by using the SndChannelStatus function. See "Obtaining Information About a Single Sound Channel" on page 2-37 for more details.) Finally, you can use SndStopFilePlay to stop the file from playing.

Playing Selections

The sixth parameter passed to the SndStartFilePlay function is a pointer to an audio selection record, which allows you to specify that only part of the sound be played. If that parameter has a value different from NIL, then SndStartFilePlay plays only a specified selection of the entire sound. You indicate which part of the entire sound to play by giving two offsets from the beginning of the sound, a time at which to start the selection and a time at which to end the selection. Currently, both time offsets must be specified in seconds.

Here is the structure of an audio selection record:

TYPE AudioSelection =
PACKED RECORD
   unitType:   LongInt;    {type of time unit}
   selStart:   Fixed;      {starting point of selection}
   selEnd:     Fixed;      {ending point of selection}
END;
To play a selection, you should specify in the selStart and selEnd fields the starting and ending point in seconds of the sound to play. Also, you must set the unitType field to the constant unitTypeSeconds.

If you wish to play an entire sound, you can simply pass NIL to the SndStartFilePlay function. Alternatively, you can set the unitType field to the constant unitTypeNoSelection, in which case the values in the selStart and selEnd fields are ignored.

Managing Multiple Sound Channels

If you are writing an application that can play multiple channels of sound on Macintosh computers that support that feature, you can use the Sound Manager's asynchronous playing abilities, but you might encounter some special obstacles. The technique for playing sounds asynchronously described in "Playing Sounds Asynchronously" on page 2-46 has a limitation if you are using multiple sound channels. Using that technique without modification, you would need to define each separate sound channel in a different global variable, and you would need to use several global flags in your callback procedure to signal which sound channels have finished processing sound commands.

Although it is easy to modify the code in "Playing Sounds Asynchronously" to use several flags, this solution might not be satisfactory for an application in which the number of sound channels open can vary. For example, suppose that you are writing entertainment software with dozens of sound effects that correspond to actions on the screen and you wish to use the Sound Manager asynchronously so that several sound effects can be played at once. It would be cumbersome to associate a separate global sound channel variable with each sound and create a flag variable for each of these sound channels. Also, you might wish to play the same sound simultaneously in two separate channels. It would be better to write code that manages a global list of sound channels and then provides a simple routine that allows you to add a channel to the list. This section shows how you might implement such a list of sound channels. Listing 2-24 defines a data structure that you could use to track multiple sound channels.

Listing 2-24 Defining a data structure to track many sound channels

CONST
   kMaxNumSndChans = 20;            {max number of sound channels}
TYPE
   SCInfo = 
   RECORD
      sndChan:       SndChannelPtr; {NIL or pointer to channel}
      mustDispose:   Boolean;       {flag to dispose channel}
      itsData:       Handle;        {data to dispose with channel}
   END;
   SCList = ARRAY[1..kMaxNumSndChans] OF SCInfo;
VAR
   gSndChans:        SCList;
The SCInfo data structure defined in Listing 2-24 allows you to keep track of which channels in the collection are being used and which were being used but currently need disposal; it also allows you to associate data with a sound channel so that you can dispose of the data when you dispose of the sound channel. Note that the value of the kMaxNumSndChans constant might vary from application to application. Having defined the data structure, you must initialize it (so that the sndChan and itsData fields are NIL and the mustDispose field is FALSE). You must also write a procedure that finds an available channel. You might declare such a procedure like this:

PROCEDURE DoTrackChan (chanToTrack: SndChannelPtr; associatedData: Handle);
Using such a procedure, you could simply create sound channels by using local variables and then add them to the tracking list so that your application disposes of them when they finish executing. The exact implementation of such a procedure would depend on the needs of your application. For example, if there are no channels available in the global list of sound channels, your application might report an error, stop sound on all active channels, or stop sound on the channel that has been playing the longest. If you want your application to be compatible with computers that do not support multichannel sound, this procedure could check whether multichannel sound is supported, and if not, would stop any sound playing on other channels. This is particularly useful if your application plays sound effects in response to actions on the screen; overlapping sound effects sound best, but if this is unattainable, the newest sound should have the highest priority.

One advantage of maintaining a list of sound channels is that you can use it in conjunction with both callback procedures and completion routines. Listing 2-25 defines a procedure that either your callback procedure or completion routine could call after setting the application's A5 world correctly.

Listing 2-25 Marking a channel for disposal

PROCEDURE MySetTrackChanDispose (mySndChannel: SndChannelPtr);
VAR
   index:      Integer;       {channel index}
   found:      Boolean;       {flag variable}
BEGIN
   index := 1;                {start at first spot}
   found := FALSE;            {initialize flag variable}
   WHILE (index <= kMaxNumSndChans) AND (NOT found) DO
      IF gSndChans[index].sndChan = mySndChannel THEN
         found := TRUE        {proper channel found}
      ELSE
         index := index + 1;  {move to next spot}
   IF found THEN
      gSndChans[index].mustDispose := TRUE;
END;
The final thing you need to do is to define a procedure that your application calls once each time through its main event loop. This procedure must dispose of sound channels that are marked for disposal. Listing 2-26 defines such a routine.

Listing 2-26 Disposing of channels that have been marked for disposal

PROCEDURE MyCleanUpTrackedChans;
CONST
   kQuietNow = TRUE;                            {need to quiet channel?}
VAR
   index:      Integer;
   myErr:      OSErr;
BEGIN
   FOR index := 1 TO kMaxNumSndChans DO         {go through all channels}
   WITH gSndChans[index] DO
      IF mustDispose THEN                       {check global flag}
      BEGIN                                     {channel needs disposal}
         IF gSndChans[index].itsData <> NIL THEN
         BEGIN                                  {release other data}
            HUnlock(gSndChans[index].itsData);
            HPurge(gSndChans[index].itsData);
         END;
                                                {free channel-related memory}
         myErr := MyDisposeSndChannel(sndChan, kQuietNow);
         sndChan := NIL;                        {set pointer to NIL}
         mustDispose := FALSE;                  {reset global flag}
         IF myErr <> noErr THEN 
            DoError(myErr);
      END;
END;
The MyCleanUpTrackedChans procedure defined in Listing 2-26 works just like the MyCheckSndChan procedure defined in Listing 2-20, but instead of checking a single global flag, it checks the flag associated with each allocated sound channel. Now that you have defined such a procedure, you can easily write a routine to stop sound in all active channels (for example, if your application receives a suspend event). Simply set the mustDispose flag on all sound channels that are allocated (that is for all channels that are not NIL) and then call MyCleanUpTrackedChans. Note, however, that when the MyCleanUpTrackedChans procedure disposes of a sound channel processing a play from disk, the completion routine will be called and will thus set the mustDispose flag to TRUE. Thus, the mustDispose flag must be reset to FALSE after the sound channel has been disposed. Otherwise, the MyCleanUpTrackedChans procedure would try to dispose of the same sound channel again when the application called it from its main event loop.


Previous Book Contents Book Index Next

© Apple Computer, Inc.
2 JUL 1996