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

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more

Jobs Board

*Apple* Systems Administrator - JAMF - Syste...
Title: Apple Systems Administrator - JAMF ALTA is supporting a direct hire opportunity. This position is 100% Onsite for initial 3-6 months and then remote 1-2 Read more
Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.