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 thescBusy
field of the sound channel status record becomesFALSE
, your application could then dispose of the channel. This technique is easy, but callingSndChannelStatus
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 theSndStartFilePlay
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 procedureMyCallBack
:
myErr := SndNewChannel(gSndChan, sampledSynth, initMono, @MyCallback);After filling a channel created bySndNewChannel
with various commands to create sound, you can then issue acallBackCmd
command to the channel. When the Sound Manager encounters acallBackCmd
command, it executes your callback procedure. Thus, by placing thecallBackCmd
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.
A callback procedure has the following syntax:
- Note
- Be sure to issue
callBackCmd
commands with theSndDoCommand
function and not theSndDoImmediate
function. If you issue acallBackCmd
command withSndDoImmediate
, your callback procedure might be called before other sound commands you have issued finish executing.
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 thecallBackCmd
command and the sound command that caused the callback procedure to be called. Applications can useparam1
orparam2
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 acallBackCmd
command that contains the required A5 information in theparam2
field. TheMyInstallCallback
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}Becauseparam2
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.
The sample callback procedure defined in Listing 2-19 can thus set A5 to access the application's global variables.
- Note
- You can also pass information to a callback routine in the
userInfo
field of the sound channel.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;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.
- 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.
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;TheMyCheckSndChan
procedure defined in Listing 2-20 checks theuserInfo
field of the sound channel to see if it contains the address of a handle. Thus, if you would like theMyCheckSndChan
procedure to release memory associated with a sound handle, you need only put the address of the handle in theuserInfo
field of the sound channel. (If you do not want theMyCheckSndChan
procedure to release memory associated with a handle, then you should set theuserInfo
field to 0 when you allocate the channel. TheMyCreateSndChannel
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, theMyCheckSndChan
procedure calls theMyDisposeSndChannel
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 thegCallbackPerformed
variable toFALSE
. Also, you should initialize thegSndChan
variable toNIL
, 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 theSndPlay
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;TheMyStartPlaying
procedure uses theMyCreateSndChannel
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 thebufferCmd
command contained within the sound resource and thecallBackCmd
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 constantkFirstSoundFinished
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 theparam1
field equalskFirstSoundFinished
,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 theSndStartFilePlay
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 theSndStopFilePlay
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 theuserInfo
field of the sound channel, like this:
gSndChan^.userInfo := SetCurrentA5;Then your completion routine can look in theuserInfo
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 theSndPauseFilePlay
andSndStopFilePlay
functions.You use
SndPauseFilePlay
to temporarily suspend a sound from playing. If a sound is playing and you callSndPauseFilePlay
, then the sound is paused. If the sound is paused and you callSndPauseFilePlay
again, then the sound resumes playing. Hence, theSndPauseFilePlay
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 theSndChannelStatus
function. See "Obtaining Information About a Single Sound Channel" on page 2-37 for more details.) Finally, you can useSndStopFilePlay
to stop the file from playing.Playing Selections
The sixth parameter passed to theSndStartFilePlay
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 fromNIL
, thenSndStartFilePlay
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 theselStart
andselEnd
fields the starting and ending point in seconds of the sound to play. Also, you must set theunitType
field to the constantunitTypeSeconds
.If you wish to play an entire sound, you can simply pass
NIL
to theSndStartFilePlay
function. Alternatively, you can set theunitType
field to the constantunitTypeNoSelection
, in which case the values in theselStart
andselEnd
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;TheSCInfo
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 thekMaxNumSndChans
constant might vary from application to application. Having defined the data structure, you must initialize it (so that thesndChan
anditsData
fields areNIL
and themustDispose
field isFALSE
). 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;TheMyCleanUpTrackedChans
procedure defined in Listing 2-26 works just like theMyCheckSndChan
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 themustDispose
flag on all sound channels that are allocated (that is for all channels that are notNIL
) and then callMyCleanUpTrackedChans
. Note, however, that when theMyCleanUpTrackedChans
procedure disposes of a sound channel processing a play from disk, the completion routine will be called and will thus set themustDispose
flag toTRUE
. Thus, themustDispose
flag must be reset toFALSE
after the sound channel has been disposed. Otherwise, theMyCleanUpTrackedChans
procedure would try to dispose of the same sound channel again when the application called it from its main event loop.