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.

Using Double Buffers

The play-from-disk routines make extensive use of the SndPlayDoubleBuffer 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 using SndPlayDoubleBuffer 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.

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 as SndPlay or SndStartFilePlay) or standard sound commands (such as bufferCmd) to play sounds. You should use SndPlayDoubleBuffer only if you require very fine control over double buffering. Remember also that the SndPlayDoubleBuffer 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.
You call 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 the dbhCompressionID, dbhNumChannels, and dbhPacketSize fields are the same as those for the compressionID, numChannels, and packetSize fields of the compressed sound header, respectively.

The dbhBufferPtr array contains pointers to two records of type SndDoubleBuffer. 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 to SndPlayDoubleBuffer is made, the two buffers should both already contain a nonzero number of frames of data.

IMPORTANT
The Sound Manager defines the data type SndDoubleBufferHeader2 that is identical to the SndDoubleBufferHeader data type except that it contains the dbhFormat field (of type OSType) that defines a custom codec to be used to decompress the sound data. The dbhFormat field is used only if the dbhCompressionID field contains the value fixedCompression. See "Sound Double Buffer Header Records" beginning on page 2-111 for details.
Here is the structure of a sound double buffer:

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 the dbFlags 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 call SndPlayDoubleBuffer, you need to allocate two buffers (of type SndDoubleBuffer), fill them both with data, set the flags for the two buffers to dbBufferReady, and then fill out a record of type SndDoubleBufferHeader 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 function MyDBSndPlay 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. The MyDBSndPlay 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). Then MyDBSndPlay fills in the fields of the double buffer header, creates two buffers, and starts the sound playing. The doubleback procedure MyDoubleBackProc is defined in the next section.

Writing a Doubleback Procedure

The dbhDoubleBack 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.

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.)
Listing 2-35 Defining a doubleback procedure

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;

Previous Book Contents Book Index Next

© Apple Computer, Inc.
2 JUL 1996