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