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.

Parsing Sound Resources and Sound Files

This section explains how you can parse sound resources and sound files to find the component of a sound resource or sound file that contains information about the sound. For sound resources, this information is stored in the sound header. In addition to obtaining information about a sound from a sound header, you might need a pointer to a sound header to use any of several low-level sound commands. For sound files, information is stored in the Form and Common Chunks. This section shows how you can find those chunks and extract information from them.

Note
The techniques shown in this section assume that you are familiar with the format of sound resources and sound files. See "Sound Storage Formats" beginning on page 2-73 for complete information on sound storage formats.

Obtaining a Pointer to a Sound Header

This section shows how you can obtain a pointer to a sound header stored in a sound resource. You can use this pointer to obtain information about the sound. You also need a pointer to a sound header to install a sampled sound as a voice in a channel (as described in "Installing Voices Into Channels" on page 2-43) and to play sounds using low-level sound commands (as described below and in the next section). You can use a technique similar to the one described in this section if you wish to obtain a pointer to wave-table data that is stored in a sound resource.

Sound Manager versions 3.0 and later include the GetSoundHeaderOffset function that you can use to locate a sound header embedded in a sound resource. Listing 2-27 shows how to call the GetSoundHeaderOffset function and then pass the returned offset to the bufferCmd sound command, to play a sampled sound using low-level Sound Manager routines.

Listing 2-27 Playing a sound resource

FUNCTION MyPlaySampledSound (chan: SndChannelPtr; sndHandle: Handle): OSErr;
VAR
   myOffset:      LongInt;
   mySndCmd:      SndCommand;          {a sound command}
   myErr:         OSErr;
BEGIN
   myErr := GetSoundHeaderOffset(sndHandle, myOffset);
   IF myErr = noErr THEN
   BEGIN
      HLock(sndHandle);
      mySndCmd.cmd := bufferCmd;                {command is bufferCmd}
      mySndCmd.param1 := 0;                     {unused with bufferCmd}
      mySndCmd.param2 := LongInt(ORD4(sndHandle^) + myOffset);
      myErr := SndDoImmediate(chan, mySndCmd);
   END;
   MyPlaySampledSound := myErr;
END;
If the GetSoundHeaderOffset function is not available but you still need to obtain a pointer to a sound header, you can use the function MyGetSoundHeaderOffset defined in Listing 2-28. The function defined there traverses a sound resource until it reaches the sound data. It returns, in the offset parameter, the offset in bytes from the beginning of a sound resource to the sound header.

IMPORTANT
The GetSoundHeaderOffset function is available in Sound Manager versions 3.0 and later. As a result, you'll need to use the techniques illustrated in Listing 2-28 only if you want your application to find a sound header when earlier versions of the Sound Manager are available.
Listing 2-28 Obtaining the offset in bytes to a sound header

FUNCTION MyGetSoundHeaderOffset (sndHdl: Handle; VAR offset: LongInt): OSErr;
TYPE
   Snd1Header =                           {format 1 'snd ' resource header}
   RECORD
      format:     Integer;                {format of resource}
      numSynths:  Integer;                {number of data types}
                                          {synths, init option follow}
   END;
   Snd1HdrPtr = ^Snd1Header;
   Snd2Header =                           {format 2 'snd ' resource header}
   RECORD
      format:     Integer;                {format of resource}
      refCount:   Integer;                {for application use}
   END;
   Snd2HdrPtr = ^Snd2Header;
   IntPtr = ^Integer;                     {for type coercion}
   SndCmdPtr = ^SndCommand;               {for type coercion}
VAR
   myPtr:         Ptr;                    {to navigate resource}
   myOffset:      LongInt;                {offset into resource}
   numSynths:     Integer;                {info about resource}
   numCmds:       Integer;                {info about resource}
   isDone:        Boolean;                {are we done yet?}
   myErr:         OSErr;
BEGIN
   {Initialize variables.}
   myOffset := 0;                         {return 0 if no sound header found}
   myPtr := Ptr(sndHdl^);                 {point to start of resource data}
   isDone := FALSE;                       {haven't yet found sound header}
   myErr := noErr;

   {Skip everything before sound commands.}
   CASE Snd1HdrPtr(myPtr)^.format OF
      firstSoundFormat:                   {format 1 'snd ' resource}
         BEGIN                            {skip header start, synth ID, etc.}
            numSynths := Snd1HdrPtr(myPtr)^.numSynths;
            myPtr := Ptr(ORD4(myPtr) + SizeOf(Snd1Header));
            myPtr := Ptr(ORD4(myPtr) + 
                        numSynths * (SizeOf(Integer) + SizeOf(LongInt)));
         END;
      secondSoundFormat:                  {format 2 'snd ' resource}
         myPtr := Ptr(ORD4(myPtr) + SizeOf(Snd2Header));
      OTHERWISE                           {unrecognized resource format}
         BEGIN
            myErr := badFormat;
            isDone := TRUE;
         END;
      END;

   {Find number of commands and move to start of first command.}
   numCmds := IntPtr(myPtr)^;
   myPtr := Ptr(ORD4(myPtr) + SizeOf(Integer));

   {Search for bufferCmd or soundCmd to obtain sound header.}
   WHILE (numCmds >= 1) AND (NOT isDone) DO
   BEGIN
      IF (IntPtr(myPtr)^ = bufferCmd + dataOffsetFlag) OR
            (IntPtr(myPtr)^ = soundCmd + dataOffsetFlag) THEN
      BEGIN                               {bufferCmd or soundCmd found}
                                          {copy offset from sound command}
         myOffset := SndCmdPtr(myPtr)^.param2;
         isDone := TRUE;                  {get out of loop}
      END
      ELSE
      BEGIN                               {soundCmd or bufferCmd not found}
                                          {move to next command}
         myPtr := Ptr(ORD4(myPtr) + SizeOf(SndCommand));
         numCmds := numCmds - 1;
      END;
   END; {WHILE}

   offset := myOffset;                    {return offset}
   MyGetSoundHeaderOffset := myErr;       {return result code}
END;
The MyGetSoundHeaderOffset function defined in Listing 2-28 begins by initializing several variables, including a pointer that it sets to point to the beginning of the data contained in the sound resource. Then, after determining whether the sound resource is format 1 or format 2, the function skips data contained in the format 1 'snd ' resource header or in the format 2 'snd ' resource header, as appropriate.

Note
Do not confuse the format 1 or format 2 'snd ' header with the sound header the MyGetSoundHeaderOffset function defined in Listing 2-28 is designed to find. A sound header contains information about the sampled-sound data stored in a sound resource; a sound resource header contains information about the format of the sound resource.
After skipping information in the sound resource header, MyGetSoundHeaderOffset simply looks through all sound commands in the resource for a bufferCmd or soundCmd command, either of which must contain the offset from the beginning of the resource to the sound header in its param2 field. If the given sound resource contains no sound header (and thus no sampled-sound data), the MyGetSoundHeaderOffset function returns an error and sets the offset variable parameter to 0.

After using the MyGetSoundHeaderOffset function to obtain an offset to the sound header, you can easily obtain a pointer to a sound header. Note, however, that because a handle to a sound resource is contained in a relocatable block, you must lock the relocatable block before you obtain a pointer to a sound header, and you must not unlock it until you are through using the pointer. Listing 2-29 demonstrates how you can convert an offset to a sound header into a pointer to a sound header after locking a relocatable block.

Listing 2-29 Converting an offset to a sound header into a pointer to a sound header

FUNCTION MyGetSoundHeader (sndHandle: Handle): SoundHeaderPtr;
VAR
   myOffset:    LongInt;         {offset to sound header}
   myErr:      OSErr;
BEGIN
   HLockHi(sndHandle);           {lock data in high memory}
                                 {compute offset to sound header}
   myErr := MyGetSoundHeaderOffset(sndHandle, myOffset);
   IF myErr <> noErr THEN
      MyGetSoundHeader := NIL    {no sound header in resource}
   ELSE
                                 {compute address of sound header}
      MyGetSoundHeader := SoundHeaderPtr(ORD4(sndHandle^) + myOffset);
END;
The MyGetSoundHeader function defined in Listing 2-29 locks the sound handle you pass it in high memory and then attempts to find an offset to the sound header in the sound handle. If the MyGetSoundHeaderOffset function defined in Listing 2-28 returns an offset of 0, then MyGetSoundHeader returns a NIL pointer to a sound header; otherwise, it returns a pointer that remains valid as long as you do not unlock the sound handle.

The MyGetSoundHeader function returns a pointer to a sampled sound header even if the sound header is actually an extended sound header or a compressed sound header. Thus, before accessing any other fields of the sound header, you should test the encode field of the sound header to determine what type of sound header it is. Then, if the sound header is, for example, an extended sound header, cast the sampled sound header to an extended sound header. Then you can access any of the fields of the extended sound header. For an example of this technique, see Listing 2-16 on page 2-44.

Playing Sounds Using Low-Level Routines

Once you obtain a pointer to a sampled sound header, you can use the bufferCmd sound command to play a sound without using the high-level Sound Manager routines. Many sampled-sound resources include bufferCmd commands, so the high-level Sound Manager routines often issue the bufferCmd command indirectly. Thus, you might in some cases be able to make your application slightly more efficient by issuing the bufferCmd command directly. Also, you might issue a bufferCmd command directly if you want the Sound Manager to ignore other parts of a sound resource.

Finally, you might issue bufferCmd commands directly if you want your application to be able to play a large sound resource without loading the entire resource at once. By issuing several successive bufferCmd commands, you can play a large sound resource using a small buffer. In this case, each buffer must contain a sampled sound header. In most cases, the sound will play smoothly, without audible gaps. It's generally easier, however, to play large sampled sounds from disk by using the play-from-disk routines or the SndPlayDoubleBuffer function. See "Managing Double Buffers" on page 2-147 for complete details.

Note
Using the bufferCmd command to play several consecutive compressed samples on the Macintosh Plus, the Macintosh SE, or the Macintosh Classic is not guaranteed to work without an audible pause or click.
The pointer in the param2 field of a bufferCmd command is the location of a sampled sound header. A bufferCmd command is queued in the channel until the preceding commands have been processed. If the bufferCmd command is contained within an 'snd ' resource, the high bit of the command must be set. If the sound was loaded in from an 'snd ' resource, your application is expected to unlock this resource and allow it to be purged after using it. Listing 2-30 shows how your application can play a sampled sound stored in a resource using the bufferCmd command.

Listing 2-30 Playing a sound using the bufferCmd command

FUNCTION MyLowLevelSampledSndPlay (chan: SndChannelPtr; sndHandle: Handle): 
                                    OSErr;
CONST
   kWaitIfFull = TRUE;                 {wait for room in queue?}
VAR
   mySndHeader:   SoundHeaderPtr;
   mySndCmd:      SndCommand;          {a sound command}
BEGIN
   mySndHeader := MyGetSoundHeader(sndHandle);
   WITH mySndCmd DO
   BEGIN
      cmd := bufferCmd;                {command is bufferCmd}
      param1 := 0;                     {unused with bufferCmd}
      param2 := LongInt(mySndHeader);  {pointer to sound header}
   END;
   IF mySndHeader <> NIL THEN
      MyLowLevelSampledSndPlay := 
                              SndDoCommand(chan, mySndCmd, NOT kWaitIfFull)
   ELSE
      MyLowLevelSampledSndPlay := badFormat;
END;
For the MyLowLevelSampledSndPlay function defined in Listing 2-30 to play a sound, the channel passed to it must already be configured to play sampled-sound data. Otherwise, the function returns a badChannel result code. Also, because the bufferCmd command works asynchronously, you might want to associate a callback procedure with the sound channel when you create the channel. For more information on playing sounds asynchronously, see "Playing Sounds Asynchronously" on page 2-46.

You can use the bufferCmd command to handle compressed sound samples in addition to sounds that are not compressed. To expand and play back a buffer of compressed samples, you pass the Sound Manager a bufferCmd command where param2 points to a compressed sound header.

To play sampled sounds that are not compressed, pass bufferCmd a standard or extended sound header. The extended sound header can be used for stereo sampled sounds. The standard sampled sound header is used for all other noncompressed sampled sounds.

Finding a Chunk in a Sound File

Sound files are not as tightly structured as sound resources. As explained in "Sound Files" on page 2-81, the chunks in a sound file can appear in any order, except that the Form Chunk is always first. Most information about a sampled sound stored in a sound file is contained in the Common Chunk. Thus, to be able to access this information, you must be able to find a particular kind of chunk in a sound file. Listing 2-31 defines a procedure that you can use to find the location of the first chunk of a specified type beginning at the chunk you specify.

IMPORTANT
The techniques illustrated in this section are provided primarily to help you understand the structure of sound files. Most sound-producing applications don't need to parse sound files.
Listing 2-31 Finding a chunk in a sound file

FUNCTION MyFindChunk (myFile: Integer;       {file reference number}
                     myChunkSought: ID;      {ID of chunk sought}
                     startPos: LongInt;      {file position to start at}
                     VAR chunkFPos: LongInt) {file position of found chunk}
                     : OSErr;
VAR
   myLength:         LongInt;                {number of bytes to read}
   myChunkHeader:    ChunkHeader;            {characteristics of chunk}
   found:            Boolean;                {flag variable}
   myErr:            OSErr;                  {error from File Manager calls}
BEGIN
   found := FALSE;                           {initialize flag variable}
                                             {set file mark at start}
   myErr := SetFPos(myFile, fsFromStart, startPos);

   {Search file's chunks for desired chunk ID.}
   WHILE (NOT found) AND (myErr = noErr) DO
   BEGIN                                     {check current chunk}
      myLength := SizeOf(myChunkHeader);
      {Load chunk header.}
      myErr := FSRead(myFile, myLength, @myChunkHeader);
      IF myErr = noErr THEN                  {chunk header loaded okay}
         IF myChunkHeader.ckID = myChunkSought THEN
         BEGIN
            found := TRUE;                   {chunk has been found}
                                             {find position in file}
            myErr := GetFPos(myFile, chunkFPos);
                                             {compute chunk's start position}
            chunkFPos := chunkFPos - SizeOf(myChunkHeader);
         END
         ELSE
         BEGIN                               {move to next chunk}
            IF myChunkHeader.ckID = ID(FormID) THEN
               {Adjust Form Chunk's size to size of formType field.}
               myChunkHeader.ckSize := SizeOf(ID);
            IF myChunkHeader.ckSize MOD 2 = 1 THEN
               {Compensate for pad byte.}
               myChunkHeader.ckSize := myChunkHeader.ckSize + 1;
            myErr := SetFPos(myFile, fsFromMark, myChunkHeader.ckSize);
         END;
   END; {WHILE}
   MyFindChunk := myErr;
END;
The MyFindChunk function defined in Listing 2-31 accepts four parameters. The myFile parameter is the file reference number of an open sound file. (For information on file reference numbers, see Inside Macintosh: Files.) In the myChunkSought parameter, you pass the ID of the type of chunk you wish to find. For example, you might pass ID(FormID) to find the Form Chunk. The third parameter, startPos, is the file position at which MyFindChunk should start searching for a chunk. This file position must be the beginning of a chunk. To start at the beginning of a file, specify 0. Finally, if the MyFindChunk function is successful, it returns in the chunkFPos parameter the file position of the first chunk of the specified type that it found. If the function is unsuccessful, it returns the appropriate File Manager result code (such as an end-of-file error) and the chunkFPos parameter is undefined.

The MyFindChunk function works by looking at each chunk of the sound file, beginning at the file position startPos and checking to see if the chunk is of the type sought. If a chunk matches, the MyFindChunk function returns the file position of the start of the chunk; otherwise, the function moves onto the next chunk. For each chunk, the MyFindChunk function reads in the chunk header, checks for a match, and then moves to the next chunk.

The MyFindChunk function moves from one chunk to the next by identifying the size of the current chunk, not including the chunk header, from the ckSize field of the chunk header. Whenever you parse sound files, you should always use the ckSize field of the chunk header to determine the size of a chunk if the size of the chunk could vary in size. The MyFindChunk function adjusts the value in the ckSize field before advancing to the next chunk in two cases. First, the ckSize field for the Form Chunk reflects the size of the entire sound file, so this function changes it to the size of the formType field so that the function does not skip the file's local chunks. Second, if the ckSize field is odd, 1 byte is added because the number of bytes in a chunk is always even.

After using the MyFindChunk function defined in Listing 2-31, you might still need to read the data contained in a chunk into memory. For example, you might read in the Form and Common Chunks to obtain information about a sound file. Listing 2-32 uses the MyFindChunk function to find a chunk in a sound file, allocates an appropriately sized block of memory for that chunk, and reads the chunk into that block.

Listing 2-32 Loading a chunk from a sound file

FUNCTION MyGetChunkData (myFile: Integer;       {file reference number}
                           myChunkSought: ID;   {ID of chunk sought}
                           startPos: LongInt):  {file position to start at}
                           Ptr;                 {pointer to data or NIL}
VAR
   myFPos:           LongInt;                   {position in file}
   myLength:         LongInt;                   {number of bytes to read}
   myChunkHeader:    ChunkHeader;               {characteristics of a chunk}
   myChunkData:      Ptr;                       {pointer to chunk data}
   myErr:            OSErr;
BEGIN
   myChunkData := NIL;                          {initialize variable}
   myErr := MyFindChunk(myFile, myChunkSought, startPos, myFPos);
   IF myErr = noErr THEN
                                                {move to start of chunk}
      myErr := SetFPos(myFile, fsFromStart, myFPos);
   IF myErr = noErr THEN
   BEGIN                               {determine how much data to copy}
      myLength := SizeOf(ChunkHeader);
      myErr := FSRead(myFile, myLength, @myChunkHeader);
      IF myChunkHeader.ckID = ID(FormID) THEN
         myChunkHeader.ckSize := SizeOf(ID);    {don't return local chunks}
      myLength := myChunkHeader.ckSize + SizeOf(ChunkHeader);
      IF myErr = noErr THEN
                                                {return to chunk's start}
         myErr := SetFPos(myFile, fsFromStart, myFPos);
   END;
   IF myErr = noErr THEN
   BEGIN                                  {read chunk data into RAM}
      myChunkData := NewPtr(myLength);
      IF myChunkData <> NIL THEN
         myErr := FSRead(myFile, myLength, myChunkData);
   END;
   IF myErr <> noErr THEN
      IF myChunkData <> NIL THEN
         DisposePtr(myChunkData);
   MyGetChunkData := myChunkData;
END;
The MyGetChunkData function defined in Listing 2-32 attempts to find a chunk in a file. If it finds the chunk, it reads the chunk header to determine the chunk's size, and if the chunk is the Form Chunk, adjusts the chunk size so that the sound file's local chunks are not included in the chunk size. Then the function attempts to allocate memory for the chunk and read the chunk into the memory. If a problem occurs at any time, the function simply returns NIL.

Note
The format of a sound file might not be the same as its operating-system type. In particular, a file might have an operating-system type 'AIFC' but be formatted as an AIFF file because the sampled-sound data contained in the file is noncompressed.

Previous Book Contents Book Index Next

© Apple Computer, Inc.
2 JUL 1996