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.
Using Double Buffers
The play-from-disk routines make extensive use of theSndPlayDoubleBuffer
function. You can use this function in your application directly if you wish to bypass the normal play-from-disk routines. You might want to do this to maximize the efficiency of your application while maintaining compatibility with the Sound Manager. Or, you might define your own double-buffering routines so that your application can convert 16-bit sound data on disk to 8-bit data that all versions of the Sound Manager can play. By usingSndPlayDoubleBuffer
instead of the normal play-from-disk routines, you can specify your own doubleback procedure (that is, the algorithm used to switch back and forth between buffers) and customize several other buffering parameters.
You call
- IMPORTANT
SndPlayDoubleBuffer
is a very low-level routine and is not intended for general use. In most cases, you should use the high-level Sound Manager routines (such asSndPlay
orSndStartFilePlay
) or standard sound commands (such asbufferCmd
) to play sounds. You should useSndPlayDoubleBuffer
only if you require very fine control over double buffering. Remember also that theSndPlayDoubleBuffer
function is not always available. You'll need to ensure that it's available in the current operating environment before calling it. See "Testing for Multichannel Sound and Play-From-Disk Capabilities" beginning on page 2-35 for details.SndPlayDoubleBuffer
by passing it a pointer to a sound channel (into which the double-buffered data is to be written) and a pointer to a sound double buffer header record. Here's an example:
myErr := SndPlayDoubleBuffer(mySndChan, @myDoubleHeader);A sound double buffer header record has the following structure:
TYPE SndDoubleBufferHeader = PACKED RECORD dbhNumChannels: Integer; {number of sound channels} dbhSampleSize: Integer; {sample size, if noncompressed} dbhCompressionID: Integer; {ID of compression algorithm} dbhPacketSize: Integer; {number of bits per packet} dbhSampleRate: Fixed; {sample rate} dbhBufferPtr: ARRAY[0..1] OF SndDoubleBufferPtr; {pointers to SndDoubleBuffer} dbhDoubleBack: ProcPtr; {pointer to doubleback procedure} END;The values for thedbhCompressionID
,dbhNumChannels
, anddbhPacketSize
fields are the same as those for thecompressionID
,numChannels
, andpacketSize
fields of the compressed sound header, respectively.The
dbhBufferPtr
array contains pointers to two records of typeSndDoubleBuffer
. These are the two buffers between which the Sound Manager switches until all the sound data has been sent into the sound channel. When the call toSndPlayDoubleBuffer
is made, the two buffers should both already contain a nonzero number of frames of data.
Here is the structure of a sound double buffer:
- IMPORTANT
- The Sound Manager defines the data type
SndDoubleBufferHeader2
that is identical to theSndDoubleBufferHeader
data type except that it contains thedbhFormat
field (of typeOSType
) that defines a custom codec to be used to decompress the sound data. ThedbhFormat
field is used only if thedbhCompressionID
field contains the valuefixedCompression
. See "Sound Double Buffer Header Records" beginning on page 2-111 for details.
TYPE SndDoubleBuffer = PACKED RECORD dbNumFrames: LongInt; {number of frames in buffer} dbFlags: LongInt; {buffer status flags} dbUserInfo: ARRAY[0..1] OF LongInt; {for application's use} dbSoundData: PACKED ARRAY[0..0] OF Byte; {array of data} END;The buffer status flags field for each of the two buffers might contain either of these values:
CONST dbBufferReady = $00000001; dbLastBuffer = $00000004;All other bits in thedbFlags
field are reserved by Apple; your application should not modify them.The following two sections illustrate how to fill out these data structures, create your two buffers, and define a doubleback procedure to refill the buffers when they become empty.
Setting Up Double Buffers
Before you can callSndPlayDoubleBuffer
, you need to allocate two buffers (of typeSndDoubleBuffer
), fill them both with data, set the flags for the two buffers todbBufferReady
, and then fill out a record of typeSndDoubleBufferHeader
with the appropriate information. Listing 2-34 illustrates how you can accomplish these tasks.Listing 2-34 Setting up double buffers
CONST kDoubleBufferSize = 4096; {size of each buffer (in bytes)} TYPE LocalVars = {variables used by the doubleback procedure} RECORD bytesTotal: LongInt; {total number of samples} bytesCopied: LongInt; {number of samples copied to buffers} dataPtr: Ptr; {pointer to sample to copy} END; LocalVarsPtr = ^LocalVars; {This function uses SndPlayDoubleBuffer to play the sound specified.} FUNCTION MyDBSndPlay (chan: SndChannelPtr; sndHeader: SoundHeaderPtr): OSErr; VAR myVars: LocalVars; myDblHeader: SndDoubleBufferHeader; myDblBuffer: SndDoubleBufferPtr; myStatus: SCStatus; myIndex: Integer; myErr: OSErr; BEGIN {Set up myVars with initial information.} myVars.bytesTotal := sndHeader^.length; myVars.bytesCopied := 0; {no samples copied yet} myVars.dataPtr := Ptr(@sndHeader^.sampleArea[0]); {pointer to first sample} {Set up SndDoubleBufferHeader.} WITH myDblHeader DO BEGIN dbhNumChannels := 1; {one channel} dbhSampleSize := 8; {8-bit samples} dbhCompressionID := 0; {no compression} dbhPacketSize := 0; {no compression} dbhSampleRate := sndHeader^.sampleRate; dbhDoubleBack := @MyDoubleBackProc; END; FOR myIndex := 0 TO 1 DO {initialize both buffers} BEGIN {Get memory for double buffer.} myDblBuffer := SndDoubleBufferPtr(NewPtr(Sizeof(SndDoubleBuffer) + kDoubleBufferSize)); IF myDblBuffer = NIL THEN BEGIN MyDBSndPlay := MemError; Exit(MyDBSndPlay); END; myDblBuffer^.dbNumFrames := 0; {no frames yet} myDblBuffer^.dbFlags := 0; {buffer is empty} myDblBuffer^.dbUserInfo[0] := LongInt(@myVars); {Fill buffer with samples.} MyDoubleBackProc(sndChan, myDblBuffer); {Store buffer pointer in header.} myDblHeader.dbhBufferPtr[myIndex] := myDblBuffer; END; {Start the sound playing.} myErr := SndPlayDoubleBuffer(sndChan, @myDblHeader); IF myErr <> noErr THEN BEGIN MyDBSndPlay := myErr; Exit(MyDBSndPlay); END; {Wait for the sound's end by checking the channel status.} REPEAT myErr := SndChannelStatus(chan, sizeof(myStatus), @status); UNTIL NOT myStatus.scChannelBusy; {Dispose double buffer memory.} FOR myIndex := 0 TO 1 DO DisposePtr(Ptr(myDblHeader.dbhBufferPtr[myIndex])); MyDBSndPlay := noErr; END;The functionMyDBSndPlay
takes two parameters, a pointer to a sound channel and a pointer to a sound header. For information about obtaining a pointer to a sound header, see "Obtaining a Pointer to a Sound Header" on page 2-57. TheMyDBSndPlay
function reads the sound header to determine the characteristics of the sound to be played (for example, how many samples are to be sent into the sound channel). ThenMyDBSndPlay
fills in the fields of the double buffer header, creates two buffers, and starts the sound playing. The doubleback procedureMyDoubleBackProc
is defined in the next section.Writing a Doubleback Procedure
ThedbhDoubleBack
field of a double buffer header specifies the address of a doubleback procedure, an application-defined procedure that is called when the double buffers are switched and the exhausted buffer needs to be refilled. The doubleback procedure should have this format:
PROCEDURE MyDoubleBackProc (chan: SndChannelPtr; exhaustedBuffer: SndDoubleBufferPtr);The primary responsibility of the doubleback procedure is to refill an exhausted buffer of samples and to mark the newly filled buffer as ready for processing. Listing 2-35 illustrates how to define a doubleback procedure. Note that the sound channel pointer passed to the doubleback procedure is not used in this procedure.This doubleback procedure extracts the address of its local variables from the
dbUserInfo
field of the double buffer record passed to it. These variables are used to keep track of how many total bytes need to be copied and how many bytes have been copied so far. Then the procedure copies at most a bufferfull of bytes into the empty buffer and updates several fields in the double buffer record and in the structure containing the local variables. Finally, if all the bytes to be copied have been copied, the buffer is marked as the last buffer.
Listing 2-35 Defining a doubleback procedure
- Note
- Because the doubleback procedure is called at interrupt time, it cannot make any calls that move memory either directly or indirectly. (Despite its name, the
BlockMove
procedure does not cause blocks of memory to move or be purged, so you can safely call it in your doubleback procedure, as illustrated in Listing 2-35.)
PROCEDURE MyDoubleBackProc (chan: SndChannelPtr; doubleBuffer: SndDoubleBufferPtr); VAR myVarsPtr: LocalVarsPtr; myNumBytes: LongInt; BEGIN {Get pointer to my local variables.} myVarsPtr := LocalVarsPtr(doubleBuffer^.dbUserInfo[0]); {Get number of bytes left to copy.} myNumBytes := myVarsPtr^.bytesTotal - myVarsPtr^.bytesCopied; {If the amount left is greater than double buffer size, limit the number } { of bytes to copy to the size of the buffer.} IF myNumBytes > kDoubleBufferSize THEN myNumBytes := kDoubleBufferSize; {Copy samples to double buffer.} BlockMove(myVarsPtr^.dataPtr, @doubleBuffer^.dbSoundData[0], myNumBytes); {Store number of samples in buffer and mark buffer as ready.} doubleBuffer^.dbNumFrames := myNumBytes; doubleBuffer^.dbFlags := BOR(doubleBuffer^.dbFlags, dbBufferReady); {Update data pointer and number of bytes copied.} myVarsPtr^.dataPtr := Ptr(ORD4(myVarsPtr^.dataPtr) + myNumBytes); myVarsPtr^.bytesCopied := myVarsPtr^.bytesCopied + myNumBytes; {If all samples have been copied, then this is the last buffer.} IF myVarsPtr^.bytesCopied = myVarsPtr^.bytesTotal THEN doubleBuffer^.dbFlags := BOR(doubleBuffer^.dbFlags, dbLastBuffer); END;