TweetFollow Us on Twitter

Apr 99 Getting Started

Volume Number: 15 (1999)
Issue Number: 4
Column Tag: Getting Started

Sound Recording and Playback

by Dan Parks Sydow

How a Mac program records and plays back user-entered sounds

In the last two Getting Started columns we covered sound-playing basics and asynchronous sound playing (the playing of sound while other action takes place). Getting Started has a wealth of other topics to cover, but before moving on we'll wrap up our study of sound by exploring how a Mac program can easily allow a user to record a sound and then make that sound a part of the program.

Mac users aren't a passive bunch - they want to interact with applications. Giving the user the ability to record something (such as a short clip of music from a CD or the user's own voice) provides the user with a feeling of power. It's also just plain cool! Giving the user the ability to then play back that sound at any time furthers the user's feeling of control. In this article you'll see how a single Toolbox call makes sound recording possible, and how a few dozen lines of code make it possible to save the user's sound to a resource and then play that sound back on demand.

Sound Recording and the User

Interface consistency across applications has been important to the success of the Macintosh. When a user wants to open a file, he knows that choosing the Open item in the File menu results in the appearance of the very familiar standard Open dialog box. The same holds true for sound recording. If the user of a program wants to record a sound, choosing a Record menu item or clicking on a Record button should result in the appearance of the standard Sound Recording dialog box - the dialog box shown in Figure 1.


Figure 1. The standard Sound Recording dialog box.

Whatever sound input device is active serves as the supplier of the sound to the Sound Recording dialog box. Usually that device is a microphone (like the one supplied with current Mac models) that's plugged into the Mac's sound input port - but it can be a different device, such as a CD drive. Using this dialog box is intuitive and straightforward. Clicking the Record button starts sound recording. Clicking the Pause button temporarily halts recording, while clicking the Stop button ends recording. The Play button is used to listen to the just-recorded sound. When it comes time to dismiss the Sound Recording dialog box, a click on the Cancel button does the job - without saving whatever was just recorded. Clicking the Save button also dismisses the dialog box, but not until the last recorded sound is saved (more on that later).

Because adding sound recording to a program is easy, you'll want to play around with the code in this column just to have a little fun. When it comes time to incorporate sound recording into your own program, though, you'll want to give some thought to the matter. Your own application may not have a need for this feature, but your program may find that it's a useful option. Consider recent versions of the word processor Microsoft Word.

Microsoft Word allows a user to add comments to a document. Typically a person who is reviewing a document written by someone else uses this feature. When the original author of the document gets the reviewed copy back, he can choose to view the comments to get the reviewer's input. In Microsoft Word, the author can read what the reviewer wrote or, if the reviewer chose the voice comments option, the author can actually hear what the reviewer said. Figure 2 shows a Word document after a comment that includes both text and voice has been added to it.


Figure 2. An example use of sound recording.

To add a comment, a reviewer highlights a questionable word or phrase in the document, chooses Comment from the Insert menu, and then clicks on the Insert Sound Object button (the button that looks like a little cassette tape) in the Comment pane (the lower area of the document). Upon clicking the Insert Sound Object the standard Sound Recording dialog appears - the same dialog box shown back in Figure 1. (Yes, sometimes Microsoft does take advantage of the Macintosh interface when developing a Macintosh application!) Here the reviewer gets to record a comment - one that can be heard by anyone who eventually opens the document. Recording a comment is done by clicking on the small speaker icon that appears just below whatever text the reviewer typed in the Comment pane (see Figure 2).

Microsoft's use of sound recording of course isn't the only situation when this feature can be added to a program - but it's a great example of how sound recording can be included in an application to enhance the user's experience.

Sound Input Devices

All Macintosh models have had, and have, one or more built-in speakers. That means that your program doesn't have to check for the presence of sound-playing hardware before it plays a sound. The same is not true for sound input devices. While the user's Mac certainly supports some form of sound input, there's no guarantee that a sound input device (such as a microphone) is connected to the computer. So before your program attempts to record a sound, it should first make a check to verify that the host computer does indeed have at least one sound input device. A call to the Toolbox function Gestalt(), with a selector code of gestaltSoundAttr, starts off the test:

OSErr      err;
long       response;
long       mask;

err = Gestalt( gestaltSoundAttr, &response );
if ( err != noErr )
  DoError( "\pError calling Gestalt()" );

If Gestalt() does its work without a hitch it places a value of noErr in err, and we can move on to examine the value returned in the response parameter response:

   mask = 1 << gestaltHasSoundInputDevice;   
   if ( response & mask == 0 )
       DoError( "\pNo sound input source" );

Gestalt() uses the bits of the response parameter to return several pieces of sound-related information. Here we're only interested in whether the system the program is running on has a sound input device. That information is held in one bit in the response parameter - the bit defined by the constant gestaltHasSoundInputDevice. To determine if one particular bit in a variable is set (on), a logical AND operation is necessary. In the previous snippet we're looking to see if the bit number defined by the constant gestaltHasSoundInputDevice is set in the response value returned by Gestalt(). A little bit shifting sets up mask so that it can be used in the test of response. If the logical AND operation results in a non-zero value, then the test passes and we know the user's Mac has support for a sound input device. If the operation results in a value of 0, we call DoError() to post an error message and exit.

Bit shifting is an operation commonly performed by many programmers. But a novice Mac programmer may have encountered it only occasionally - if ever. If you know all about bit shifting, go ahead and skip this section. If the above code snippet has you confused, read on...

A program may declare a flag variable that is used to indicate the state of a single condition. You've seen an example of this - the Boolean variable named gDone is used by our example programs to indicate whether or not a program has finished executing. A program may also declare a variable whose purpose is to contain a number of flags - each bit in the byte or bytes of the variable tell the program whether certain things are true or not. A variable of this type may consist of any number of bits. If such a variable is declared to be of type short, it is eight bits, so it can hold up to eight flags. If the variable is declared to be of type long, it is 32 bits, so it can hold up to 32 flags. Consider if the eight bits of flag look like this:

00100011

Bit numbering is from right to left, with the first bit considered bit number 0. In this example bit 0 has a value of 1, bit 1 has a value of 1, bit 2 has a value of 0, bit 3 has a value of 0, and so forth. Thus if we want to know the value held in, say, bit 5, we'd need to look at the sixth bit from the right. Starting with the rightmost bit and counting from zero we see that bit 5, the sixth bit, has a value of 1. Of course we can tell that by looking at the written values - the program needs to use a mask and the logical AND operation to determine this. By placing a value of 1 in bit 5 (the sixth bit) of a mask variable, and then ANDing that mask with a variable that holds a number of flags, the value of only the sixth bit of the flag is revealed. Here the above flag variable is ANDed with a mask that has just the sixth bit set:

00100011
00100000
00100000

An AND operation looks at the corresponding bits of two operands and returns a value of 1 for that bit position if and only if both bits have a value of 1. In the above example, only bit 5, the sixth bit from the right, has a value of 1 in both operands. That's shown in the result, which has only a single value of 1.

Now let's see how this discussion pertains to the call to Gestalt() in our example. Gestalt() sets the bits of response to reflect the state of various sound-related attributes on the user's machine. We're interested only in one bit - the bit represented by the constant gestaltHasSoundInputDevice. Apple defined this constant to have a value of 5 (I know that from searching the Universal Header files), so to clarify this example let's substitute 5 for gestaltHasSoundInputDevice:

mask = 1 << 5;
if ( response & mask == 0 )
  DoError( "\pNo sound input source" );

The << left shift operator shifts the bits of the value of the left operand (1) to the left by the number of places given by the right operand (5). The vacated positions are filled with zeros. So mask becomes the result of shifting the number 1 (which in binary is 00000001) five places to the left. Thus mask in binary is 00100000. When we AND mask with response, the result is a value of 1 if bit 5 in response is 1, or 0 if bit 5 in response is 0. A value of 1 means that the gestaltHasSoundInputDevice bit in response is set, the user's machine has sound input device capabilities, and our program can carry on. One final note. Both the mask and the response variables are declared to be long integers, so each is a 32-bit value. For simplicity I've only shown the lower eight bits of each of these variables. Keep in mind that each really consists of 32 bits (so, for instance, after the bit shifting mask really looks like this: 00000000000000000000000000100000). Whew!

While your input device checking work is now done, near program start up you may want to remind the user that your program offers sound-recording capabilities, and that the user should have the appropriate sound input source set in the Monitors & Sound control panel of his or her machine. Figure 3 shows that control panel with the External Mic option selected. This is the option appropriate for a Mac that has an external microphone (like the one supplied with recent Mac models) plugged into its sound input jack.


Figure 3. The Monitors & Sound control panel.

Recording A Sound

The Toolbox function SndRecord() is responsible for displaying the standard Sound Recording dialog box. Once this dialog box appears, SndRecord() takes control and is responsible for handling all interaction between the user and the dialog box. Before calling SndRecord(), your program needs to allocate a block of heap memory in which the recorded sound data will be stored. Just how much memory to allocate is a bit tricky - at this point the sound hasn't been recorded, so your program doesn't know how much memory will be needed. If the program allocates too little memory, the user will be limited in the length of the recording that can be made. If the program allocates too much memory, the application may experience memory-related problems later on. The solution is to determine how much free space the application can spare at this point in its execution, and then reserve that amount for use by the SndRecord() function. Making that available memory determination is handled by the Toolbox function PurgeSpace():

long      totalHeap;
long      contigMem;
  
PurgeSpace( &totalHeap, &contigMem );

Contrary to its name, PurgeSpace() doesn't purge, or deallocate, any memory. Instead, a call to this function tells your application how much free space would exist if the heap were to be purged. After a call to PurgeSpace(), the first parameter holds the total free space (in bytes) in the heap if the heap were to be purged. The second parameter holds the size of the largest contiguous block of memory (in bytes) that will exist in the heap if the heap were to be purged. Because the sound data must reside in a contiguous block, it is the value of the second parameter that is of interest to your program. To make sure that your application has access to the entire available application heap, it's important that your program call the Toolbox function MaxApplZone() near the start of the program.

A call to NewHandle() allocates the block of memory needed for the sound data. The number of available contiguous bytes of memory is held in the variable contigMem, so that variable could be used as the parameter to NewHandle() - but it's not. Reserving the entire amount of available contiguous memory would leave the application with little free memory, so some amount of the total available contiguous memory should be subtracted out. In this next snippet, 100 KB are reserved for other, non-sound recording use by the program. The 100 KB is enough for a small example program like the one we'll develop in this column, but your more sophisticated application will want to keep more of its heap free. You can use a debugger or a memory-watching tool such as Metrowerks' ZoneRanger to get an idea of how your application makes use of memory.

#define            kHeapReserve      100 * 1024

long               sndDataMem;
SndListHandle   theSound;

sndDataMem = contigMem - kHeapReserve;
theSound = ( SndListHandle )NewHandle( sndDataMem );

NewHandle() allocates the block of memory and returns a handle to it. SndRecord() requires a SndListHandle rather than a generic handle, so typecasting of the returned handle is in order. After that, the call to SndRecord() can be made:

Point   corner = { 60, 30 }; 
SndRecord( nil, corner, siBestQuality, &theSound );

The first parameter to SndRecord() is a pointer to an optional filter function that's used to handle user actions (such as keystrokes). SndRecord() handles sound recording, pausing, stopping, and playing, so it won't be often that you'll need a filter function. Passing a nil pointer tells SndRecord() no filter function is to be used.

The second parameter specifies the screen placement of the standard Sound Recording dialog box. Pass a Point variable that holds the coordinates of the upper-left corner of the dialog box and SndRecord() takes care of the rest. The above snippet specifies that the upper-left corner of the dialog box appears 60 pixels from the top of the screen and 30 pixels from the left of the screen.

The third parameter specifies a quality level at which to record incoming sound data. Here you use one of three Apple-defined constants (siGoodQuality, siBetterQuality, or siBestQuality). The choice of quality level determines the duration of the sound that can be recorded: the lower the recording quality, the longer the sound that can be recorded. The degree of quality determines the amount of compression that the Sound Manager will use - a lower-quality sound has more compression performed on it. Compression conserves memory, but sacrifices sound quality. As a rule of thumb, use siGoodQuality for voice recording, siBestQuality for sound that demands higher quality (such as music being recorded from a CD player), and siBetterQuality when a compromise between sound quality and storage space is acceptable.

The final parameter to SndRecord() is a handle to the block of memory that is to be devoted to holding the newly recorded sound data. This is the SndListHandle returned by the prior call to NewHandle().

Playing a Recorded Sound

When the user records a sound in the Sound Recording dialog box, that sound can be played back from within that same dialog box (by clicking on the Play button). If the user wants the recorded sound to be preserved after the Sound Recording dialog box has been dismissed, a click on the dialog box Save button does the trick.

When SndRecord() finishes executing, its fourth parameter holds a handle that references the recorded sound data. From January's Getting Started article on sound-basics you know that once a program has a handle to sound data in memory, a call to SndPlay() plays the sound:

      SndPlay( nil, theSound, false );

As a reminder, the first parameter to SndPlay() is a pointer to a sound channel (or nil if the Sound Manager is to take care of the sound channel allocation), the second parameter is the handle to the sound data, and the final parameter indicates whether the sound should be played asynchronously (true) or synchronously (false).

If your program doesn't need to save the user-recorded sound after the program quits, you can keep the sound data in memory and play the sound at any time during the running of the program by making use of the handle in a call to SndPlay(). One way of doing this is to make the sound handle a global variable (in our example we'd declare theSound as a global variable).

Saving A Sound To A Resource

After the user clicks the Save button in the standard Sound Recording dialog box, the recorded sound is held in a block of memory that can be referenced by the SndListHandle returned by the call to SndRecord(). As long as this variable is preserved, your program can play the sound, save it to disk, or both. You've just seen that playing the sound involves a call to SndPlay(). Saving the sound to disk requires a little more (but not too much) work..

To save a sound as a snd resource in the application's resource fork, begin by calling the Toolbox function CurResFile() to get a reference number for the application's resource fork. Here we're making the assumption that your program hasn't explicitly opened a separate resource file and made that file current:

short   resourceFileRef;
resourceFileRef = CurResFile();

Next, call the Toolbox function AddResource() to create a new resource and to add that resource to the resource map in memory. When a program runs all of its resources aren't automatically loaded into memory - many remain on disk. When a program uses a resource it loads a copy of that resource into memory. The resource map serves as a guide to resources on disk.

AddResource( ( Handle )theSound, 'snd ', 
                  9000, "\pUser Sound" );

The first AddResource() parameter is a handle to the data to save, the second parameter is the type of resource to save the data to, the third parameter is the ID to assign to the new resource, and the last parameter is the name to store the resource under.

Next, call the Toolbox function UpdateResFile() to, yes, update the resource file. The call to AddResource() added the new resource to the resource map in memory, but it didn't actually add the new resource to a resource file. UpdateResFile() adds the resource to the resource file specified by the passed reference number. Use the number that was previously obtained from the call to CurResFile():

UpdateResFile( resourceFileRef );

With the new sound resource safely tucked away in the application's resource fork, your program can play the sound at any time - even in subsequent runnings of the program. To do that you'll again use SndPlay(). Before doing that, load the sound resource into memory:

Handle     theHandle;
OSErr      err;
  
theHandle = GetResource( 'snd ', 9000 );
SndPlay( nil, (SndListHandle)theHandle, false );

SoundSaver

This month's program is called SoundSaver. Running SoundSaver results in the appearance of the menu bar shown in Figure 4. The only menu that holds items of real significance is the Sound menu. Figure 4 shows this menu's two items. Choosing the Record Sound item brings on the standard Sound Recording dialog box. After recording a sound (via the Record button) and then saving the sound to a resource (via the Save button), the Play Recorded Sound item in the Sound menu becomes enabled. Choosing that item plays the last sound that was recorded. The Record Sound and Play Recorded Sound items can be selected as often as desired. After testing both items a few times, choose Quit from the File menu to end the program.


Figure 4. The SoundSaver menu.

Creating the SoundSaver Resources

Begin your work on SoundSaver by creating a folder named SoundSaver in your CodeWarrior development folder. Start up ResEdit and create a new resource file named SoundSaver.rsrc, making sure to designate the SoundSaver folder as the resource file's destination. Figure 5 shows the four types of resources that will be in the SoundSaver.rsrc file.


Figure 5. The SoundSaver resources.

If you're a regular reader of Getting Started, then the one ALRT and one DITL resource used by SoundSaver should be familiar to you - they're used in the display of an error-handling alert posted by the DoError() routine that's a part of each column's example program.

Figure 5 shows the four MENU resources the SoundSaver program uses. Note that the second item in MENU 131 is initially marked as disabled. After creating the MENU resources you'll create a single MBAR resource that references the ID of each of the four menus.

That completes the SoundSaver.rsrc file. Now save the file and quit ResEdit - we're ready to create the SoundSaver project.

Creating the SoundSaver Project

Launch CodeWarrior and choose New Project from the File menu to create a new project. Base the new project on the MacOS:C_C++:MacOS Toolbox:MacOS Toolbox Multi-Target project stationary. Uncheck the Create Folder check box before clicking the OK button. Now give the project the name SoundSaver.mcp, and make sure the SoundSaver folder will be the project's destination.

When the new project window appears add the SoundSaver.rsrc file to it, then remove the SillyBalls.rsrc file. The SoundSaver project doesn't use any standard ANSI libraries, so you can optionally remove the ANSI Libraries folder from the project window.

Choose New from the File menu to create a new, empty source code window. Save the new file, giving it the name SoundSaver.c. Choose Add Window from the Project menu to add this empty file to the project. Now remove the SillyBalls.c placeholder file from the project window. At this point you're ready to type in the source code.

If you want to get right down to business, then you can forego the above steps and instead download the entire SoundSaver project from MacTech's ftp site at <ftp://ftp.mactech.com/src/mactech/volume15_1999/15.04.sit>.

Walking Through the Source Code

SoundSaver begins with the usual list of constant definitions, most of which define resource IDs. Exceptions include kHeapReserve, which is used to reserve a block of memory for sound data, and kSndResIDMaxRsrvd, which specifies the largest ID that Apple reserves for use with its own system sound resources.

/********************* constants *********************/

#define kMBARResID            128
#define kALRTResID            128

#define kSleep                7
#define kMoveToFront          (WindowPtr)-1L

#define mApple                128
#define iAbout                1

#define mFile                 129
#define iQuit                 1

#define mSound                131
#define iRecord               1
#define iPlay                 2

#define kHeapReserve         100 * 1024
#define kSndResIDMaxRsrvd   8191

SoundSaver declares four global variables. The Boolean flag gDone tells the program that the user has elected to quit. Another Boolean variable, gUserRecordedSound, tells the program when a sound has been recorded by the user. The MenuHandle variable gSoundMenu is used during the enabling of the Play Recorded Sound menu item (which first occurs after the user records a sound). The long variable gCurrentUserSndID holds the ID of the snd resource after it's added to the program's resource fork.

/****************** global variables *****************/

Boolean            gDone;
Boolean            gUserRecordedSound = false;
MenuHandle         gSoundMenu;
long               gCurrentUserSndID;

Next come the program's function prototypes.

/********************* functions *********************/

void      ToolBoxInit( void );
void      MenuBarInit( void );

void      RecordSoundToMemory( void );
void      SaveSoundFromMemoryToResource( SndListHandle );
void      PlaySoundResource( void );

void      EventLoop( void );
void      DoEvent( EventRecord *eventPtr );
void      HandleMouseDown( EventRecord *eventPtr );
void      EnableDisableMenuItems( void );
void      HandleMenuChoice( long menuChoice );
void      HandleAppleChoice( short item );
void      HandleFileChoice( short item );
void      HandleSoundChoice( short item );
void      DoError( Str255 errorString );

The main() function begins by initializing the Toolbox. After that a call to the Toolbox function MaxApplZone() is made to set the application's heap to its maximum size. Later on in the program when a determination of available free memory is made, this call to MaxApplZone() will be of importance.

void   main( void )
{
   NumVersion      theSndMgrVers;
   OSErr           err;
   long            response;
   long            mask;

   ToolBoxInit();

   MaxApplZone();

As in the previous two Getting Started examples, a check is made to ensure that the user has version 3.0 or later of the Sound Manager. Versions prior to 3.0 don't include some of the functionality that your sound-intensive program might require, so your program may find this an important test.

   theSndMgrVers = SndSoundManagerVersion();  
   if ( theSndMgrVers.majorRev < 3 )
      DoError( "\pSound Manager is outdated" );

A call to Gestalt() with a selector code of gestaltSoundAttr tells the Toolbox to return a variety of sound-related information in the response variable. As described at length earlier in this article, a little bit shifting is performed in order to check the value of the bit that signals whether the user's machine supports a sound input device. After that the program's menu is displayed and the event loop begins.

   err = Gestalt( gestaltSoundAttr, &response );
   if ( err != noErr )
      DoError( "\pError calling Gestalt()" );

   mask = 1 << gestaltHasSoundInputDevice;   
   if ( response & mask == 0 )
      DoError( "\pNo sound input source" );

   MenuBarInit();
   
   EventLoop();
}

ToolBoxInit()is the same as prior versions. MenuBarInit() is essentially the same - there's just one addition. In this program's version of MenuBaRInit() a call to GetMenuHandle() is made to obtain a handle to the Sound menu and to store that handle in the global variable gSoundMenu. Later, when it comes time to enable the Play Recorded Sound menu item from this menu, we'll have a means of accessing the Sound menu.

/******************** ToolBoxInit ********************/

void      ToolBoxInit( void )
{
   InitGraf( &qd.thePort );
   InitFonts();
   InitWindows();
   InitMenus();
   TEInit();
   InitDialogs( nil );
   InitCursor();
}

/******************** MenuBarInit ********************/

void      MenuBarInit( void )
{
   Handle            menuBar;
   MenuHandle      menu;
   
   menuBar = GetNewMBar( kMBARResID );
   SetMenuBar( menuBar );
   gSoundMenu = GetMenuHandle( mSound );
   menu = GetMenuHandle( mApple );
   AppendResMenu( menu, 'DRVR' );
   
   DrawMenuBar();
}

When the user chooses Record Sound from the Sound memory, the application-defined function RecordSoundToMemory() is invoked. This function begins with the code that checks for free memory and reserves an appropriately sized block of that memory.

/**************** RecordSoundToMemory ****************/

void RecordSoundToMemory( void )
{
   OSErr               err;
   SndListHandle   theSound; 
   long                  totalHeap;
   long                  contigMem;
   long                  sndDataMem;
   Point               corner = { 60, 30 }; 
     
   PurgeSpace( &totalHeap, &contigMem);

   sndDataMem = contigMem - kHeapReserve;
   theSound = (SndListHandle)NewHandle( sndDataMem );

Next, SndRecord() is called to display the standard Sound Recording dialog box. After the user is finished with this dialog box, SndRecord() fills the variable err with an error value that the program examines:

   err = SndRecord( nil, corner, siBestQuality, &theSound );
   if ( err != noErr )
   {
      if ( err != userCanceledErr )
         DoError( "\pError recording sound" );
   }
   else   
   {
      SaveSoundFromMemoryToResource( theSound );
   }
}

If err doesn't have a value of noErr (err != noErr), then an error occurred. At this point we normally call DoError() to post an error message and exit the program. We will in fact do this, but not before making one further test of the value of err. If the user clicked the Cancel button in the standard Sound Recording dialog box, SndRecord() returns an err value of userCanceledErr. That's not a "real" error, so in that one instance we won't call DoError(). Any other err value is considered a true error and DoError() is invoked.

If err has a value of noErr, then two things happened: SndRecord() worked flawlessly and the user clicked the Save button in the standard Sound Recording dialog box. In such a case the else section of the if-else statement executes. Here we call the application-defined function SaveSoundFromMemoryToResource().

SaveSoundFromMemoryToResource() does just that - it place the new sound data in a resource. The function begins by obtaining a reference number for the application's resource fork:

/*********** SaveSoundFromMemoryToResource ***********/

void SaveSoundFromMemoryToResource( SndListHandle theSound )
{
   OSErr      err;
   short      resourceFileRef;

   resourceFileRef = CurResFile();

Before adding the sound data to the resource file an appropriate resource ID needs to be determined. We could simply choose an ID and use that in the call to AddResource() (as was done in the AddResource() snippet earlier in this article), but that method has a couple of drawbacks. Using a predetermined value for the sound resource ID has the potential for an ID conflict - your application may already have a snd resource with the same ID. That's not a worry in our very trivial SoundSaver application, but in a much larger program that allows snd resources to be added it could be a concern. The second drawback to using a predefined value for the sound ID is that each time the user records a sound, the new sound overwrites the previous sound. If we come up with a plan for using a value that's guaranteed to be unique each time AddResource() is called, each sound the user records can be saved as a resource. Here's that plan:

   do
   {
      gCurrentUserSndID = UniqueID( 'snd ' );
   } while ( gCurrentUserSndID <= kSndResIDMaxRsrvd );

The Toolbox routine UniqueID() searches all open resource files (including the resource fork of the application) for resources of the type specified in the parameter passed to it. UniqueID() takes note of the IDs of all such resources and returns an ID that is not used. The temptation exists to just call UniqueID() once, but we'll instead place the call in a loop to call it (potentially) a number of times. The reasoning here is that we want to reject some ID values that UniqueID() returns. Specifically, Apple reserves values less than 8192 for the IDs of system sound resources. So any value that UniqueID() returns that is less than 8192 should be rejected, and UniqueID() should be invoked again to try to come up with a value that is both greater than 8191 and that is unique to the snd resources in any open resource file.

With an appropriate ID found, it's time to call AddResource() to add the sound data to the resource map in memory. A call to the Toolbox function ResError() is then made to verify that no resource-related error occurred.

   AddResource( (Handle)theSound, 'snd ', 
                 gCurrentUserSndID, "\pNew Sound" );
   err = ResError();
   if ( err != noErr )
      DoError( "\pError adding sound resource to program" );

Now the memory resource map has been altered, so we need to call UpdateResFile() to make the resource file aware of the change. If this task is performed without error we set the global flag gUserRecordedSound to true so that the program knows that a sound has been successfully recorded. This flag will be used later to enable the initially disabled Play Recorded Sound item in the Sound menu.

   UpdateResFile( resourceFileRef );
   err = ResError();
   if ( err != noErr )
      DoError( "\pError updating program resource file" );
   else
      gUserRecordedSound = true;
}

When the user selects Play Recorded Sound from the Sound menu, the PlaySoundResource() function is invoked. The code in this function should look familiar to you - it's standard resource sound-playing code like that described in the January Getting Started column. PlaySoundResource() begins by loading the newly added sound resource to memory:

/***************** PlaySoundResource *****************/

void PlaySoundResource( void )
{
   Handle      theHandle;
   OSErr      err;
  
   theHandle = GetResource( 'snd ', gCurrentUserSndID );
   if ( theHandle == nil )
      DoError( "\pAttempt to load sound resource failed" );

With the sound data back in memory SndPlay() is called to play the sound. The playing of a sound can be a lengthy task, so we'll lock the handle that references the data to ensure that the system doesn't move the data during its periodic compacting of memory. After verifying that no error occurred, the memory that holds the sound data can be marked as available to the program by calling ReleaseResource():

   HLock( theHandle );
      err = SndPlay( nil, (SndListHandle)theHandle, false );
   HUnlock( theHandle );

   if ( err != noErr )
      DoError( "\pPlaying of sound failed" );
  
   ReleaseResource( theHandle );
}

The remainder of the SoundSaver code is the event-handling code included in all of our Getting Started examples. Much of the code is similar to, or exactly the same as, code found in previous examples. But you'll still want to carefully read the text preceding a function description to pick up on the purpose of the few new lines of code.

/********************** EventLoop ********************/

void      EventLoop( void )
{      
   EventRecord      event;
   
   gDone = false;
   while ( gDone == false )
   {
      if ( WaitNextEvent( everyEvent, &event, kSleep, nil ) )
         DoEvent( &event );
   }
}

/*********************** DoEvent *********************/

void      DoEvent( EventRecord *eventPtr )
{
   char   theChar;
   
   switch ( eventPtr->what )
   {
      case mouseDown: 
         HandleMouseDown( eventPtr );
         break;
      case keyDown:
      case autoKey:
         theChar = eventPtr->message & charCodeMask;
         if ( (eventPtr->modifiers & cmdKey) != 0 ) 
            HandleMenuChoice( MenuKey( theChar ) );
         break;
      case updateEvt:
         BeginUpdate( (WindowPtr)(eventPtr->message) );
         EndUpdate( (WindowPtr)(eventPtr->message) );
         break;
   }
}

A click of the mouse button results in DoEvent() calling HandleMouseDown(). The SoundSaver version of HandleMouseDown() includes one new line of code - a call to the application-defined function EnableDisableMenuItems(). This routine is called when the mouse button click occurs in the menu bar. This function (explained next) is responsible for determining which menu items need to be enabled or disabled, and for then carrying out the appropriate enabling or disabling. The reasoning for calling this function from HandleMouseDown() is quite simple. The only time a user sees a menu item is when he clicks the mouse in the menu bar - so that's the only time your program needs to worry about each menu item being in its proper state.

/******************* HandleMouseDown *****************/

void      HandleMouseDown( EventRecord *eventPtr )
{
   WindowPtr   window;
   short         thePart;
   long            menuChoice;
   
   thePart = FindWindow( eventPtr->where, &window );
   
   switch ( thePart )
   {
      case inMenuBar:
         EnableDisableMenuItems();
         menuChoice = MenuSelect( eventPtr->where );
         HandleMenuChoice( menuChoice );
         break;
      case inSysWindow : 
         SystemClick( eventPtr, window );
         break;
   }
}

A click in the menu bar results in EnableDisableMenuItems() being invoked. In the SoundSaver program our only concern is the state of the Play Recorded Sound item, but your program may need to keep track of the state of several menu items in several menus. For ease of changing, and correcting, menu item state code, place all such code in a single function. Devote a global MenuHandle variable (such as gSoundMenu) to each menu that holds items that will have state changes and a global Boolean flag variable (such as gUserRecordedSound) to each menu item that can have its state toggled. For our particular example recall that gUserRecordedSound is initialized to false (so that the Play Recorded Sound menu item remains disabled until a sound is in fact recorded). This flag variable is set to true in SaveSoundFromMemoryToResource() after a sound has successfully been recorded and saved.

/**************** EnableDisableMenuItems *************/

void      EnableDisableMenuItems( void )
{
   if ( gUserRecordedSound == true )
      EnableItem( gSoundMenu, iPlay );
   else
      DisableItem( gSoundMenu, iPlay );
}

/******************* HandleMenuChoice ****************/

void      HandleMenuChoice( long menuChoice )
{
   short   menu;
   short   item;
   
   if ( menuChoice != 0 )
   {
      menu = HiWord( menuChoice );
      item = LoWord( menuChoice );
      
      switch ( menu )
      {
         case mApple:
            HandleAppleChoice( item );
            break;
         case mFile:
            HandleFileChoice( item );
            break;
         case mSound:
            HandleSoundChoice( item );
            break;
      }
      HiliteMenu( 0 );
   }
}

/****************** HandleAppleChoice ****************/

void      HandleAppleChoice( short item )
{
   MenuHandle      appleMenu;
   Str255            accName;
   short            accNumber;
   
   switch ( item )
   {
      case iAbout:
         SysBeep( 10 );
         break;
      default:
         appleMenu = GetMenuHandle( mApple );
         GetMenuItemText( appleMenu, item, accName );
         accNumber = OpenDeskAcc( accName );
         break;
   }
}

/******************* HandleFileChoice ****************/

void      HandleFileChoice( short item )
{
   switch ( item )
   {
      case iQuit:
         gDone = true;
         break;
   }
}

When the user chooses one of the two items from the Sound menu, HandleSoundChoice() gets called. Choosing Record Sound results in RecordSoundToMemory() being called, while choosing Play Recorded Sound results in a the invocation of PlaySoundResource().

/****************** HandleSoundChoice ****************/

void      HandleSoundChoice( short item )
{
   switch ( item )
   {
      case iRecord:
         RecordSoundToMemory();
         break;
      case iPlay:
         PlaySoundResource();
         break;
   }
}

/*********************** DoError *********************/

void      DoError( Str255 errorString )
{
   ParamText( errorString, "\p", "\p", "\p" );
   
   StopAlert( kALRTResID, nil );
   
   ExitToShell();
}

Running SoundSaver

Run SoundSaver by selecting Run from CodeWarrior's Project menu. After the code compiles, a menu appears. Verify that the Play Recorded Sound item in the Sound menu is disabled (because you haven't yet recorded a sound to play), then choose Record Sound from the Sound menu. Doing that brings up the standard Sound Recording dialog box. Record a sound, then click the Save button. With a sound recorded and the dialog box now dismissed, the Play Recorded Sound item should be enabled. Choose it to hear the sound. You can repeat this process as often as you want before quitting.

To verify that the Sound Recording dialog box can record from whatever sound input device is current, open the Monitors & Sound control panel. Click on the Sound button and then select CD from the Sound Monitoring Source pop-up menu (this is assuming your Mac has a CD-ROM drive). Close the control panel and place an audio CD in your Mac's CD-ROM drive. With the CD playing, run the SoundSaver program and select Record Sound from the Sound menu. Click the Record button and whatever sound is playing on the audio CD will be recorded. Click the Stop button, then click Save to save the sound as a resource. You can play back the audio clip by choosing Play Recorded Sound from the Sound menu.

After you quit the SoundSaver program, launch ResEdit and open the SoundSaver program. Make sure to open the program itself, not the SoundSaver.rsrc resource file. Here you'll see that the SoundSaver program contains one or more snd resources - resources that didn't exist before you ran the program. Figure 6 shows the SoundSaver application opened in ResEdit.


Figure 6. The resources in the SoundSaver program.

Till Next Month...

Years ago, a user didn't expect a computer program to include sound - other than perhaps an annoying beep when he did something wrong! Now, a computer program without sound is a bit like one without graphics. That's why we've devoted the last three columns to the art of adding sound to your own Mac programs. To learn still more about sound files, sound playing, and sound recording, thumb through the Sound volume of Inside Macintosh. Then go ahead and experiment with the SoundSaver code. One improvement you may want to make is to include a scheme for saving the resource number of a recorded sound so that on subsequent runnings of SoundSaver the sound can be retrieved. If you do that you'll also want to change the way the global flag gUserRecordedSound works. As it stands, from one execution to another the program doesn't remember that a sound was recorded.

If your program would be better served by saving a recorded sound to a sound file, try substituting the call to SndRecord() with a call to its companion Toolbox function SndRecordToFile(). SndRecordToFile() saves a recorded sound to an AIFF or AIFF-C format sound file. The playing back of a sound file was described in detail two issues ago - in the January Getting Started column. You can also read about sound file playing in Inside Macintosh: Sound.

By now you should have the topic of sound mastered. Which means it's time to move on to something completely different. And that's just what we'll do next month...

 
AAPL
$567.77
Apple Inc.
+43.02
MSFT
$39.86
Microsoft Corpora
+0.17
GOOG
$525.16
Google Inc.
-1.78

MacTech Search:
Community Search:

Software Updates via MacUpdate

Ember 1.5.1 - Versatile digital scrapboo...
Ember (formerly LittleSnapper) is your digital scrapbook of things that inspire you: websites, photos, apps or other things. Just drag in images that you want to keep, organize them into relevant... Read more
Cyberduck 4.4.4 - FTP and SFTP browser....
Cyberduck is a robust FTP/FTP-TLS/SFTP browser for the Mac whose lack of visual clutter and cleverly intuitive features make it easy to use. Support for external editors and system technologies such... Read more
TechTool Pro 7.0.3 - Hard drive and syst...
TechTool Pro is now 7, and this is the most advanced version of the acclaimed Macintosh troubleshooting utility created in its 20-year history. Micromat has redeveloped TechTool Pro 7 to be fully 64... Read more
MacFamilyTree 7.1.6 - Create and explore...
MacFamilyTree gives genealogy a facelift: it's modern, interactive, incredibly fast, and easy to use. We're convinced that generations of chroniclers would have loved to trade in their genealogy... Read more
EtreCheck 1.9.9 - For troubleshooting yo...
EtreCheck is a simple little app to display the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support... Read more
TeamViewer 9.0.28116 - Establish remote...
TeamViewer gives you remote control of any computer or Mac over the Internet within seconds, or can be used for online meetings. Find out why more than 200 million users trust TeamViewer! Free for... Read more
DiskAid 6.6.3 - Use your iOS device as a...
DiskAid is the ultimate transfer tool for accessing the iPod, iPhone or iPad directly from the desktop. Access data such as: music, video, photos, contacts, notes, call history, text messages (SMS),... Read more
Viber 4.1.0 - Send messages and make cal...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device,... Read more
Apple iOS 7.1.1 - The latest version of...
The latest version of iOS can be downloaded through iTunes. Apple iOS 7 brings an all-new design and all-new features. Simplicity Simplicity is often equated with minimalism. Yet true simplicity is... Read more
1Password 4.3 - Powerful password manage...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more

Latest Forum Discussions

See All

Pen & Ink Review
Pen & Ink Review By Jennifer Allen on April 24th, 2014 Our Rating: :: CONVENIENT ARTISTRYiPad Only App - Designed for the iPad Pen & Ink is an ideal way to sketch down some visual ideas or simply spark your imagination.   | Read more »
Sonic & All-Stars Racing Transformed...
Sonic & All-Stars Racing Transformed Now Free, to Add New Iconic Characters and Elements Posted by Tre Lawrence on April 24th, 2014 [ | Read more »
Soccer Rally 2 Review
Soccer Rally 2 Review By Carter Dotson on April 24th, 2014 Our Rating: :: GOALKEEPINGUniversal App - Designed for iPhone and iPad Soccer Rally 2 is the most serious vehicular soccer game ever created.   | Read more »
Galaxy Conquerors Review
Galaxy Conquerors Review By Jennifer Allen on April 24th, 2014 Our Rating: :: RETRO SHOOTINGUniversal App - Designed for iPhone and iPad Old school shooting is fun but inaccurate in Galaxy Conquerors.   | Read more »
Yomi Review
Yomi Review By Rob Thomas on April 24th, 2014 Our Rating: :: C-C-C-COMBO BREAKERiPad Only App - Designed for the iPad Round One – Fight! No quarters required for this iOS adaptation of a tabletop adaptation of the arcade fighting... | Read more »
Injustice: Gods Among Us Updated with Ne...
Injustice: Gods Among Us Updated with New Characters, Leaderboards, Gear, and Online Multiplayer Posted by Rob Rich on April 24th, 2014 [ | Read more »
Spin It Review
Spin It Review By Jordan Minor on April 24th, 2014 Our Rating: :: SPIN ME RIGHT ROUNDUniversal App - Designed for iPhone and iPad Spin It has a fine puzzle game model, but its execution lacks energy.   | Read more »
Productivity App NoteSuite is Having its...
Productivity App NoteSuite is Having its Biggest Sale Ever, Just for One Week Posted by Rob Rich on April 24th, 2014 [ permalink ] | Read more »
Wayward Souls Review
Wayward Souls Review By Carter Dotson on April 24th, 2014 Our Rating: :: CARRY ON, WAYWARD SONUniversal App - Designed for iPhone and iPad Wayward Souls is a roguelike-inspired action-RPG that sets a high bar for other games to... | Read more »
The Sandbox Gets Update, Receives New Ca...
The Sandbox Gets Update, Receives New Campaign and New Elements Posted by Tre Lawrence on April 24th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »

Price Scanner via MacPrices.net

Strong iPhone Sales Drive Apple Record March...
Apple on Wednesday announced financial results for its fiscal 2014 second quarter ended March 29, 2014. The Company posted quarterly revenue of $45.6 billion and quarterly net profit of $10.2 billion... Read more
Award-Winning NoteSuite Productivity App is $...
Minneapolis based Theory.io has announced an 80-Percent Markdown NoteSuite for iPad. NoteSuite helps users stay organized by capturing their notes, to-dos and documents in one organized place.... Read more
16GB 1st generation iPad mini available for $...
Radio Shack has a select number of refurbished 1st generation 16GB WiFi iPad minis available for $199.99 on their online store. Choose free shipping or free ship-to-store. We expect these to sell out... Read more
13-inch 2.5GHz MacBook Pro on sale for $100 o...
B&H Photo has the 13″ 2.5GHz MacBook Pro on sale for $1099 including free shipping plus NY sales tax only. Their price is $100 off MSRP. Read more
iPad Sales “Lull” A Reality Correction Of Unm...
I have lots of time for Jean-Louis Gassée, the former Apple Computer executive (1981 to 1990) who succeeded Steve Jobs as head of Macintosh development when the latter was dismissed in 1985. Mr.... Read more
Apple Makes OS X Betas Available To All – Wit...
Apple’s OS X Beta Seed Program, which lets you install the latest pre-release builds, try it out, and submit your feedback, is now open to anyone who wants to sign on rather than to developers and... Read more
Apple Releases iOS 7.1.1 Update
The latest iOS 7.1.1 update contains improvements, bug fixes and security updates, including: • Further improvements to Touch ID fingerprint recognition • Fixes a bug that could impact keyboard... Read more
Logitech Announces Thinner, Lighter, More Fle...
Logitech has announced an update to its Ultrathin for iPad Air, iPad mini and iPad mini with Retina display, improving the flexibility and design of its award-winning predecessor with an even thinner... Read more
Logitech Introduces Hinge, Big Bang and Turna...
Logitech has announced expansion of its tablet product line with three new cases – the Logitech Hinge, the Logitech Big Bang and the Logitech Turnaround – each for the iPad Air, iPad mini and iPad... Read more
WaterField’s Rough Rider Leather Messenger Ba...
WaterField Designs have announced the new 15-inch size of their popular Rough Rider leather messenger bag, a vintage-looking bag that combines Old West charm and ruggedness with distinctly modern... Read more

Jobs Board

*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Position Opening at *Apple* - Apple (United...
…customers purchase our products, you're the one who helps them get more out of their new Apple technology. Your day in the Apple Store is filled with a range of Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
*Apple* Inc. Research Data Specialist - Appl...
…of Worldwide Market Research & Intelligence. The team is responsible for conducting Apple branded consumer market research. It is also responsible for analyzing data Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.