August 92 - THE ASYNCHRONOUS SOUND HELPER
THE ASYNCHRONOUS SOUND HELPER
BRYAN K. ("BEAKER") RESSLER
In system software version 6.0.7 and later, the Sound Manager has impressive sound
input and output capabilities that are largely untapped by the existing body of
application software. This article presents a code module called the Asynchronous
Sound Helper that's designed to make asynchronous sound input and output easily
accessible to the application programmer, yet provide an interface flexible enough to
facilitate extensive application features.
Of all the Managers inInside Macintosh, the Sound Manager may be the winner of the Most Startling
Metamorphosis contest. On the earliest Macintosh computers, sound was produced by direct calls to
a Sound Driver, as described inInside Macintosh Volume II. Later, with the advent of system software
version 4.1 and the more powerful sound-generation hardware of the Macintosh SE and Macintosh
II, the Sound Driver was superseded by a fairly buggy initial implementation of the Sound Manager,
which was first documented inInside Macintosh Volume V. The new Sound Manager presented a
problem for developers, because there was a large installed base of Macintosh 128K, 512K, 512K
enhanced, and Plus computers that didn't have the ROMs or system software to support the Sound
Manager. At this point, all but the heartiest developers decided the tradeoffs for including sound in a
non-sound-related application were too severe.
By the time version 6.0.7 rolled around, many of the details of the Sound Manager had changed, and
sound input support was added. In fact, the Sound Manager in 6.0.7 and System 7 is relatively stable.
So if you've been waiting for the right moment to add sound support to your application, the
moment has arrived.
The Macintosh Sound Manager acts as a buffer between your application and the complexities of the
sound hardware (see Figure 1). Sounds are produced by sendingsound commands to a sound channel .
The sound channel sends the commands through asynthesizer that knows how to control the audio
hardware. Among the Sound Manager's current 38 commands are operations such as playing simple
frequencies, playing complex recorded sounds, and changing sound volume. The Sound Manager
also allows you to record new sounds if the appropriate hardware is available. Recording is performed
through a sound input driver.
Figure 1The Sound Manager
Sound playback and recording through the Sound Manager can be performedsynchronously or asynchronously . When you make a synchronous call to the Sound Manager, the function doesn't return
control to your application until the entire operation (sound playback, for instance) is complete. In
general, it's easy to use the Sound Manager to play or record sound synchronously. Asynchronous
calls return control immediately to your application and perform their operations in the background,
which makes asynchronous operations somewhat trickier. Many developers feel that there are too
many details to make asynchronous sound worthwhile in an application not specifically oriented
toward sound. However, with sound input devices becoming more common, the market impetus to
add sound is growing.
This article presents the Asynchronous Sound Helper, a code module designed to take much of the
heartburn out of asynchronous sound input and output. The goals of Helper, as we'll be calling it
from now on, are threefold:
- Provide a straightforward and uncomplicated interface for asynchronous sound
I/O specifically tailored toward common application requirements.
- Encourage developers to include support for sound as a standard type of data, just
like text or graphics.
- Function as a tutorial on how to perform asynchronous sound input and output
using the Sound Manager.
Helper provides two-tiered support--"easy" calls for basic operations and "advanced" calls for more
complex operations. You choose which calls to use depending on your application's specific needs
and user interface. For simple asynchronous recording and playback, only a few routines are required.
Or go all out and use Helper routines to easily provide a "sound palette" with tape-deck-like controls
for your application.
To top it off, the overhead for Helper is fairly small. The code compiles to about 4K, and it adds 86
bytes of global data to your application. At run time, it uses around 4K in your application's heap.
Helper uses clean Sound Manager techniques--nothing skanky that might cause compatibility
problems in the future.
HOOKING UP WITH HELPER
First let's take a quick look at how Helper works and how your application uses it. We'll leave the
details for later.
Before you can use Helper you need to add a global Boolean flag to your application --the attention
flag. At initialization time, your application calls Helper's initialization routine and provides theaddress of the attention flag. In its main event loop, your application checks the value of the attention
flag and, if true, calls Helper's idle routine.
Because Helper's main function is to spawn asynchronous sound tasks, communication between your
application and Helper is carried out on an as-needed basis. Here are the basic phases of
communication for a typical sound playback sequence (the numbers correspond to Figure 2).
- Your application tells Helper to play some sound.
- Helper uses the Sound Manager to allocate a sound channel and begins
asynchronous playback of your sound.
- The application goes on its merry way, with the sound playing asynchronously in
the background.
- The sound completes playback. Helper has set up a sound command that causes
Helper to be informed immediately upon completion of playback (this occurs at
interrupt time). At that time, Helper sets the application's global attention flag.
- The next time through your application's event loop, the application notices that
the attention flag is set and calls SHIdle to free up the sound channel.
Figure 2 Application-Sound Manager Interface
When your application terminates, it calls Helper's kill routine. Helper's method of communication
with the application minimizes processing overhead. By using the attention flag scheme, your
application calls Helper's idle routine only when it's really necessary. This could be important in
game and multimedia applications where CPU bandwidth is pushed to the limit.
HELPER'S INTERFACE
Now let's take a look at the interfaces and the basic uses of the routines provided by Helper. Later
we'll go into more detail about how the routines work and how to use them.
INITIALIZATION, IDLE, AND TERMINATION
pascal OSErr SHInitSoundHelper(Boolean *attnFlag, short numChannels);
pascal void SHIdle(void);
pascal void SHKillSoundHelper(void);
SHInitSoundHelper initializes Helper. It allocates memory, so you should call it near the beginning
of your application. The application passes to SHInitSoundHelper the address of the Boolean
attention flag that Helper uses to inform the application when it needs attention.
SHIdle performs various cleanup tasks. Call SHIdle when the attention flag goes true.
At application termination, call SHKillSoundHelper. It stops current recording and playback and
deallocates Helper's memory.
EASY SOUND OUTPUT
pascal OSErr SHPlayByID(short resID, long *refNum);
pascal OSErr SHPlayByHandle(Handle sound, long *refNum);
pascal OSErr SHPlayStop(long refNum);
pascal OSErr SHPlayStopAll(void);
SHPlayByID and SHPlayByHandle provide an easy way to begin asynchronous sound playback.
These routines return a reference number via the refNum parameter. This reference number can be
used to stop playback and can be used with the advanced routines described later. If you intend to
simply trigger a sound that you want to run to completion (like a gunshot sound in a game), you can
pass nil for the refNum parameter, thereby ignoring the reference number.
To stop a given sound or stop all playback, use SHPlayStop or SHPlayStopAll.
ADVANCED SOUND OUTPUT
pascal OSErr SHPlayPause(long refNum);
pascal OSErr SHPlayContinue(long refNum);
pascal SHPlayStat SHPlayStatus(long refNum);
pascal OSErr SHGetChannel(long refNum, SndChannelPtr *channel);
If you want more control over the playback process, these routines will be of interest. SHPlayPause
pauses the playback of a sound, like the pause button on a tape deck. SHPlayContinue continues
playback of a sound that was previously paused. Use SHPlayStatus to find out the status of a sound--
finished, playing, or paused. If you want to send commands directly to a sound channel that was
allocated by Helper, use SHGetChannel. You might want to send sound commands in your
application, for example, to play continuous looped background music.
EASY SOUND INPUT
pascal OSErr SHRecordStart(short maxK, OSType quality,
Boolean *doneFlag);
pascal OSErr SHGetRecordedSound(Handle *theSound);
pascal OSErr SHRecordStop(void);
These are the three basic routines for recording sound through a sound input device. To begin
asynchronous sound recording, use SHRecordStart. The application passes the address of a
Boolean--a recording-completed flag--that tells the application when the recording is complete.
Once complete, the application calls SHGetRecordedSound to retrieve a sound handle. The handle
is suitable for playback with SHPlayByHandle or to be written out as a 'snd ' resource. To stop
recording immediately (as with the stop button on a tape recorder), use SHRecordStop.
ADVANCED SOUND INPUT
pascal OSErr SHRecordPause(void);
pascal OSErr SHRecordContinue(void);
pascal OSErr SHRecordStatus(SHRecordStatusRec *recordStatus);
To pause recording, use SHRecordPause. To continue previously paused recording, use
SHRecordContinue. Use SHRecordStatus to get information about the status of recording. This
status information includes the current input level (which could be used to draw a tape-deck-likelevel meter), the amount of sound that's been recorded (with respect to the maximum), and whether
the recording is finished, recording, or paused.
HELPER'S DATA STRUCTURES
Helper uses three internal data structures to keep track of recording and playback.
typedef struct {
SndChannel channel; // Our sound channel.
long refNum; // Our Helper reference number.
Handle sound; // The sound we're playing.
Fixed rate; // Sampled sound playback rate.
char handleState;// The handle state for this handle.
Boolean inUse; // Tells whether this SHOutRec is in use.
Boolean paused; // Tells whether this sound is paused.
} SHOutRec, *SHOutPtr;
The SHOutputVars record contains an array of SHOutRec records. The numOutRecs field tells how
many are in the array. These records, one for each allocated channel, hold information about
currently playing sounds. They're reused when sounds have completed. The SHOutputVars record
also keeps track of the next available output reference number, in the field nextRef. The reference
numbers are unique (modulo 2,147,483,647).
typedef struct {
long inRefNum; // Sound Manager's input device refNum.
SPB inPB; // The input parameter block.
Handle inHandle; // The handle we're recording into.
short headerLength; // The length of the sound's header.
Boolean recording; // Tells that we're actually recording.
Boolean recordComplete; // Tells that recording is complete.
OSErr recordErr; // Error, if error terminated recording.
short numChannels; // Number of channels for recording.
short sampleSize; // Sample size for recording.
Fixed sampleRate; // Sample rate for recording.
OSType compType; // Compression type for recording.
Boolean *appComplete; // Tells caller when recording is done.
Boolean paused; // Tells that recording has been paused.
} SHInputVars;
An SHOutRec record's first field, channel, contains the actual Sound Manager SndChannel used to
play the sound. The sound reference number associated with this sound (the one passed back to the
application) is stored in the refNum field. A handle to the sound we're playing is stored in the sound
field. The rate field holds the sample playback rate of sampled sounds, which is used when pausing
sampled sounds. The handleState field contains the original handle state (derived via a call to
SHGetState), so Helper can reset the handle's state after playback is complete. The inUse field tells
whether a given SHOutRec is in use by a playing sound (as opposed to available for reuse). Finally,
the paused flag lets Helper remember when a sound has been paused.
typedef struct {
long inRefNum; // Sound Manager's input device refNum.
SPB inPB; // The input parameter block.
Handle inHandle; // The handle we're recording into.
short headerLength; // The length of the sound's header.
Boolean recording; // Tells that we're actually recording.
Boolean recordComplete; // Tells that recording is complete.
OSErr recordErr; // Error, if error terminated recording.
short numChannels; // Number of channels for recording.
short sampleSize; // Sample size for recording.
Fixed sampleRate; // Sample rate for recording.
OSType compType; // Compression type for recording.
Boolean *appComplete; // Tells caller when recording is done.
Boolean paused; // Tells that recording has been paused.
} SHInputVars;
The SHInputVars record contains information pertaining to a sound being recorded. When the
sound input device is opened, its reference number is stored in inRefNum. The sound input
parameter block, inPB, is part of SHInputVars. The sound being recorded is stored in inHandle until
complete. The recording flag tells whether we're actually in the act of asynchronous recording, and
the recordComplete flag (set by the record completion routine, described later) tells us whenrecording has completed. If an error occurs during recording, it's saved in recordErr so that it can be
returned to the application later, when it calls SHGetRecordedSound. The next four fields--
numChannels, sampleSize, sampleRate, and compType--hold information that's used to construct
the sound's header. The appComplete field points to a Boolean that the application may optionally
use to be informed of recording termination (the application may use repeated calls to the
SHRecordStatus routine instead). The paused flag lets Helper keep track of when recording has been
paused.
Helper declares its global storage as shown on the following page. As we go on, you'll see the use of
these globals in context, which will clarify their function.
static Boolean gsSHInited = false; // Has Helper been initialized?
static Boolean *gsSHNeedsTime; // Pointer to application's
// attention flag.
static SHOutputVarsgsSHOutVars; // Sound output variables.
static SHInputVarsgsSHInVars; // Sound input variables.
HELPER'S INTERNAL UTILITY ROUTINES
Helper uses twelve static utility routines to help it carry out its job. Many of these routines are trivial,
but let's go over a few of the more important ones in detail-- SHPlayCompletion,
SHRecordCompletion, and SHOutRecFromRefNum.
When Helper performs asynchronous sound playback, it depends on a callback routine that signals to
the application that playback has completed. Here's the playback callback routine,
SHPlayCompletion:
pascal void SHPlayCompletion(SndChannelPtr channel,
SndCommand *command)
{
long otherA5;
// Look for our "callback signature" in the sound command.
if (command->param1 == kSHCompleteSig) {
otherA5 = SetA5(command->param2); // Set up our A5.
channel->userInfo = kSHComplete;
*gsSHNeedsTime = true;
// Tell application to give us an SHIdle call.
SetA5(otherA5); // Restore old A5.
}
}
When Helper begins the sound playback, it queues up a sound command--
a callBackCmd--in the sound channel. The callBackCmd tells the Sound Manager to call the
callback routine, SHPlayCompletion. We place a verifiable "signature" in the sound command
record so that the application can verify that the call occurred as a result of a specific callBackCmd,
and not as a result of some spurious one. When such a blessed callback occurs, Helper uses another
handy value stuffed into the sound command--a pointer to the A5 global world--to set up access to
the globals. Helper then sets the channel's userInfo field to a value that flags the sound as complete.
Helper also sets the application's attention flag so that later, in the main event loop, the application
sees that the attention flag is set and calls SHIdle. SHIdle then skips through the SHOutRec array
looking for sound channels that are in use and have kSHComplete in their userInfo fields, and
disposes of their sound channels. This is how Helper cleans up after sound playback has completed.
Asynchronous sound recording also relies on a callback routine that signals when recording has
completed. Here's the callback routine, SHRecordCompletion:
pascal void SHRecordCompletion(SPBPtr inParams)
{
long otherA5;
otherA5 = SetA5(inParams->userLong); // Set up our A5.
*gsSHNeedsTime = true;
// Tell application to give us// an SHIdle call.
gsSHInVars.recordComplete = true; // Make a note to ourselves.
SetA5(otherA5); // Restore old A5.
}
When recording has completed (for any reason--we filled the entire buffer, an error occurred, or the
user manually stopped recording), the Sound Manager calls the record callback routine. Like the
playback callback routine, it first sets up the A5 world. Then it sets the application's attention flag
and the recordComplete flag inside the global SHInputVars structure. Later, the application will
notice its attention flag is set and call SHIdle. SHIdle checks the recordComplete flag and notices
that recording is complete, closes the sound input device, and prepares for the application to call
SHGetRecordedSound to retrieve the recorded sound. This is how Helper cleans up after
asynchronous sound recording.
Another heavily used static utility routine is SHOutRecFromRefNum. It maps a sound reference
number into a pointer to the appropriate SHOutRec.
SHOutPtr SHOutRecFromRefNum(long refNum)
{
short i;
// Search for the specified refNum.
for (i = 0; i < gsSHOutVars.numOutRecs; i++)
if (gsSHOutVars.outArray[i].inUse &&
gsSHOutVars.outArray[i].refNum == refNum)
break;
// If we found it, return a pointer to that record, otherwise
// nil.
if (i == gsSHOutVars.numOutRecs)
return(nil);
else return(&gsSHOutVars.outArray[i]);
}
SHOutRecFromRefNum simply does a linear search through the output records, looking for a
record that is in use and has a matching reference number. If none is found, nil is returned.
We'll investigate a few more utility routines as we delve into the details of the public routines in the
sections that follow.
HELPER'S INITIALIZATION, TERMINATION, AND IDLE ROUTINES
Let's take a closer look at the SHInitSoundHelper, SHKillSoundHelper, and SHIdle routines.
SHINITSOUNDHELPER
pascal OSErr SHInitSoundHelper(Boolean *attnFlag, short numChannels)
{
OSErr err;
// Use default number of channels if zero was specified.
if (numChannels == 0)
numChannels = kSHDefChannels;
// Remember the address of the application's attention flag.
gsSHNeedsTime = attnFlag;
// Allocate the channels.
gsSHOutVars.numOutRecs = numChannels;
gsSHOutVars.outArray = (SHOutPtr)NewPtrClear(numChannels *
sizeof(SHOutRec));
// If successful, flag that we're initialized and exit.
if (gsSHOutVars.outArray != nil) {
gsSHInited = true;
return(noErr);
} else {
// Return some kind of error (MemError if there is one,
// otherwise make one up).
err = MemError();
if (err == noErr)
err = memFullErr;
return(err);
}
}
SHInitSoundHelper is fairly uncomplicated. The attnFlag parameter points to the application's
attention flag, which is used to tell the application that a call to SHIdle is needed. The numChannels
parameter tells Helper how many channels to allocate. The number of simultaneous sounds that can
be played back by Helper is limited by the number of channels allocated (via numChannels) and the
number of simultaneous sound channels the Sound Manager allows. So use a numChannels that's
appropriate to your needs. If you specify zero, a reasonable default (four) is used.
SHInitSoundHelper allocates the output records and stores a pointer to the array in gsSHOutVars. If
the memory allocation is successful, gsSHInited is set to true.
SHKILLSOUNDHELPER
pascal void SHKillSoundHelper(void)
{
short i;
long timeout;
Boolean outputClean, inputClean;
if (!gsSHInited)
return;
SHPlayStopAll(); // Kill all playback.
SHRecordStop(); // Kill recording.
// Now sync-wait for everything to clean itself up.
timeout = TickCount() + kSHSyncWaitTimeout;
do {
if (*gsSHNeedsTime)
SHIdle(); // Clean up when required.
// Check whether all our output channels are cleaned up.
outputClean = true;
for (i = 0; i < gsSHOutVars.numOutRecs && outputClean; i++)
if (gsSHOutVars.outArray[i].inUse)
outputClean = false;
// Check whether our recording is cleaned up.
inputClean = !gsSHInVars.recording;
if (inputClean && outputClean)
break;
} while (TickCount() < timeout);
// Lose our preallocated sound channels.
DisposePtr((Ptr)gsSHOutVars.outArray);
}
SHKillSoundHelper first stops any asynchronous sound input or output in progress. It waits for all
the output channels to be free and for recording to stop before continuing. Finally, it disposes of the
output record array.
SHIDLE
pascal void SHIdle(void)
{
short i;
OSErr err;
long realSize;
// Immediately turn off the application's attention flag.
*gsSHNeedsTime = false;
// Do playback cleanup.
for (i = 0; i < gsSHOutVars.numOutRecs; i++)
if (gsSHOutVars.outArray[i].inUse &&
gsSHOutVars.outArray[i].channel.userInfo ==
kSHComplete)
// We've found a channel that needs closing.
SHReleaseOutRec(&gsSHOutVars.outArray[i]);
// Do recording cleanup.
if (gsSHInVars.recording && gsSHInVars.recordComplete) {
HUnlock(gsSHInVars.inHandle);
if (gsSHInVars.inPB.error &&
gsSHInVars.inPB.error != abortErr) {
// An error (other than a manual stop) occurred during
// recording. Kill the handle and save the error code.
gsSHInVars.recordErr = gsSHInVars.inPB.error;
DisposeHandle(gsSHInVars.inHandle);
gsSHInVars.inHandle = nil;
} else {
// Recording completed normally (which includes abortErr,
// the "error" that occurs when recording is stopped
// manually).
gsSHInVars.recordErr = noErr;
realSize = gsSHInVars.inPB.count +
gsSHInVars.headerLength;
err = SetupSndHeader(
gsSHInVars.inHandle, gsSHInVars.numChannels,
gsSHInVars.sampleRate, gsSHInVars.sampleSize,
gsSHInVars.compType, kSHBaseNote, realSize,
&gsSHInVars.headerLength);
SetHandleSize(gsSHInVars.inHandle, realSize);
}
// Error or not, close the recording device and tell the
// application that recording is complete through the
// recording-completed flag that the caller originally
// passed into SHRecordStart.
SPBCloseDevice(gsSHInVars.inRefNum);
gsSHInVars.recording = false;
gsSHInVars.inRefNum = 0;
if (gsSHInVars.appComplete != nil)
*gsSHInVars.appComplete = true;
}
}
}
SHIdle is one of the most important routines in Helper. It performs cleanup of completed sound
playback and recording. First SHIdle clears the application's attention flag. For playback cleanup, it
iterates through the output records looking for records that have their inUse flag set and have
kSHComplete in their sound channel's userInfo field. These sounds have been flagged as completed
by the callback routine. When such an output record is found, its channel is closed with a call to
SHReleaseOutRec.
void SHReleaseOutRec(SHOutPtr outRec)
{
short i;
Boolean found = false;
// An SHOutRec's inUse flag gets set only if SndNewChannel has
// been called on the record's sound channel. So if it's in use,
// we call SndDisposeChannel and ignore the error. (What else
// can we do?)
if (outRec->inUse)
SndDisposeChannel(&outRec->channel, kSHQuietNow);
// If this sound handle isn't being used by some other output
// record, kindly restore the original handle state.
if (outRec->sound != nil) {
for (i = 0; i < gsSHOutVars.numOutRecs && !found; i++)
if (&gsSHOutVars.outArray[i] != outRec &&
gsSHOutVars.outArray[i].inUse &&
gsSHOutVars.outArray[i].sound == outRec->sound)
found = true;
if (!found)
HSetState(outRec->sound, outRec->handleState);
}
outRec->inUse = false;
}
The SHReleaseOutRec routine has two important functions. First, it calls SndDisposeChannel to
free up the sound channel. Second, it restores the handle state of the sound that was playing if that
same sound isn't currently playing on some other channel.
Recording cleanup is also performed back in SHIdle. If the recording flagand the recordComplete
flag are set, the record callback has informed Helper that recording is complete. Right away, Helper
unlocks the sound handle. Next Helper checks for errors. If the application called SHRecordStop to
manually stop recording before the buffer was full, the error abortErr is generated. We don't really
consider this an error, so we expressly allow abortErr. If an error did occur, Helper saves the error
code. This way, later, when the application calls SHGetRecordedSound, Helper can return an
appropriate OSErr. If no error occurred, Helper calculates the actual size of the sampled sound and
builds an appropriate sound header, including the correct length.
After checking for errors, Helper resizes the handle to exactly the size it should be. Then it calls
SPBCloseDevice to close the sound input device, clears the recording flag, and sets the application's
recording-completed flag, if one was provided.
As you can see, it's important to call SHIdle when the attention flag goes true; otherwise subsequent
requests for playback or recording may fail.
EASY PLAYBACK ROUTINES
Now we'll look more closely at Helper's easy playback routines, SHPlayByID, SHPlayByHandle,
SHPlayStop, and SHPlayStopAll.
SHPLAYBYID AND SHPLAYBYHANDLE
pascal OSErr SHPlayByID(short resID, long *refNum)
{
Handle sound;
char oldHandleState;
short ref;
OSErr err;
SHOutPtr outRec;
// First, try to get the caller's 'snd ' resource.
sound = GetResource(soundListRsrc, resID);
if (sound == nil) {
err = ResError();
if (err == noErr)
err = resNotFound;
return(err);
}
oldHandleState = SHGetState(sound);
HNoPurge(sound);
// Now let's get a reference number and an output record.
ref = SHNewRefNum();
err = SHNewOutRec(&outRec);
if (err != noErr) {
HSetState(sound, oldHandleState);
return(err);
}
// Now let's fill in the output record. This routine also
// initializes the sound channel and flags outRec as "in use."
err = SHInitOutRec(outRec, ref, sound, oldHandleState);
if (err != noErr) {
HSetState(sound, oldHandleState);
SHReleaseOutRec(outRec);
return(err);
}
// We're in pretty good shape. We've got a reference number, an
// initialized output record, and the sound handle. Let's party.
MoveHHi(sound);
HLock(sound);
err = SHBeginPlayback(outRec);
if (err != noErr) {
HSetState(sound, oldHandleState);
SHReleaseOutRec(outRec);
return(err);
} else {
if (refNum != nil) // refNum is optional--the caller may not
*refNum = ref; // want it.
return(noErr);
}
}
SHPlayByID starts asynchronous playback of the 'snd ' resource with ID resID. First the resource is
loaded and set to be nonpurgeable. Notice the call to SHGetState. This utility routine searches the
output record array looking for the given sound handle in some output record that's flagged as
inUse. If the handle is found, SHGetState returns the handle state that's stored in the output record.
If the sound handle isn't found, the function returns HGetState(sound). See "Why SHGetState?" for
details on why this is necessary. Then SHPlayByID calls SHNewRefNum to get the next consecutive sound reference number, and
SHNewOutRec to find the first available output record in the list. Next, SHPlayByID calls
SHInitOutRec to fill out the output record.
OSErr SHInitOutRec(SHOutPtr outRec, long refNum, Handle sound,
char handleState)
{
short i;
OSErr err;
SndChannelPtr channel;
// Initialize the sound channel inside outRec. Clear the bytes to
// zero, install the proper queue size, and then call
// SndNewChannel.
for (i = 0; i < sizeof(SndChannel); i++)
((char *)&outRec->channel)[i] = 0;
outRec->channel.qLength = stdQLength;
channel = &outRec->channel;
err = SndNewChannel(&channel, kSHNoSynth, kSHNoInit,
(SndCallBackProcPtr)SHPlayCompletion);
if (err != noErr)
return(err);
// Initialize the rest of the record and return noErr. Note that
// we set the record's inUse flag only if the SndNewChannel call
// was successful.
outRec->refNum = refNum;
outRec->sound = sound;
outRec->rate = 0;
outRec->handleState = handleState;
outRec->inUse = true;
outRec->paused = false;
return(noErr);
}
The SHInitOutRec routine calls SndNewChannel to open the sound channel that's associated with
this output record. The constant kSHNoSynth is passed as the synthesizer and kSHNoInit is passed
as the synthesizer initializer value. These
values are passed because Helper doesn't have any idea what kind of sound will be played on this
channel, so it must assume nothing. (See "Types of Sound" for an overview of the different
synthesizers.) SHInitOutRec also passes the address of the playback completion routine,
SHPlayCompletion, to SndNewChannel. If
successful, the rest of the output record is filled out and the output record's inUse
flag is set.
If the SHInitOutRec call is successful, SHPlayByID moves the handle high in the heap, locks it, and
begins playback with a call to SHBeginPlayback.
OSErr SHBeginPlayback(SHOutPtr outRec)
{
OSErr err;
// First, initiate playback. If an error occurs, return it
// immediately.
err = SndPlay(&outRec->channel, outRec->sound, kSHAsync);
if (err != noErr)
return(err);
// Playback started OK. Let's queue up a callback command so that
// we'll know when the sound is finished.
SHQueueCallback(&outRec->channel);
// Ignore error. (What can we do?)
return(noErr);
}
The SHBeginPlayback routine calls SndPlay to start the sound playing asynchronously, passing as
parameters the sound handle and the flag kSHAsync. Since the only way to tell that an asynchronous
sound has completed is via a callback, Helper must queue up a callBackCmd after beginning
playback. This is done with a call to SHQueueCallback.
Finally, SHPlayByID returns the sound reference number, if the application wants it. (You can pass
nil if you don't care about the reference number.)
The SHPlayByHandle routine is similar to SHPlayByID, except that it supports a special case: you
can pass SHPlayByHandle a nil handle. This means "go ahead and open a sound channel, but don't
call SndPlay." Normally an application that does this subsequently calls SHGetChannel to retrieve
the sound channel pointer and sends sound commands directly to the channel itself. This is covered
in more detail later in the section "Advanced Playback Routines."
SHPLAYSTOP AND SHPLAYSTOPALL
pascal OSErr SHPlayStopAll(void)
{
short i;
// Look for output records that are in use and stop their
// playback with SHPlayStopByRec.
for (i = 0; i < gsSHOutVars.numOutRecs; i++)
if (gsSHOutVars.outArray[i].inUse)
SHPlayStopByRec(&gsSHOutVars.outArray[i]);
return(noErr);
}
SHPlayStop stops playback of a given sound by looking up the reference number. The routine tries
to find the output record associated with refNum by a call to SHOutRecFromRefNum. If one is
found, SHPlayStop calls SHPlayStopByRec to do the actual work.
pascal OSErr SHPlayStopAll(void)
{
short i;
// Look for output records that are in use and stop their
// playback with SHPlayStopByRec.
for (i = 0; i < gsSHOutVars.numOutRecs; i++)
if (gsSHOutVars.outArray[i].inUse)
SHPlayStopByRec(&gsSHOutVars.outArray[i]);
return(noErr);
}
SHPlayStopAll is not much different, but instead of looking up a reference number, it calls
SHPlayStopByRec on all output records that have their inUse flag set. Let's take a look at
SHPlayStopByRec.
void SHPlayStopByRec(SHOutPtr outRec)
{
SndCommand cmd;
// Dump the rest of the commands in the queue (including our
// callBackCmd).
cmd.cmd = flushCmd;
cmd.param1 = 0;
cmd.param2 = 0;
SndDoImmediate(&outRec->channel, &cmd);
// Shut up and go to your room! No dessert for you, little boy.
cmd.cmd = quietCmd;
cmd.param1 = 0;
cmd.param2 = 0;
SndDoImmediate(&outRec->channel, &cmd);
// It's now safe to manually dump our channel (we'll just skip
// the whole callback thing in this case).
SHReleaseOutRec(outRec);
}
To stop a playing sound, Helper sends a flushCmd, which flushes all subsequent (currently
unprocessed) sound commands from a channel's queue, and a quietCmd, which tells the channel to
stop making sound. The flushCmd also flushes the callBackCmd we previously queued up. After
these two commands, we can safely call SHReleaseOutRec to dispose of the sound channel for the
sound.
Now that we've seen the basic stuff, on to the advanced sound output routines.
ADVANCED PLAYBACK ROUTINES
Helper's easy calls are enough to satisfy the demands of many applications. If finer control is desired,
a few other playback routines can be used. Let's take a closer look at the advanced playback routines,
SHPlayPause, SHPlayContinue, SHPlayStatus, and SHGetChannel.
SHPLAYPAUSE
pascal OSErr SHPlayPause(long refNum)
{
SHOutPtr outRec;
SndCommand cmd;
OSErr err;
outRec = SHOutRecFromRefNum(refNum);
if (outRec != nil) {
// Don't bother with this if we're already paused.
if (outRec->paused)
return(kSHErrAlreadyPaused);
// Get the current playback rate for this sound.
cmd.cmd = getRateCmd;
cmd.param1 = 0;
cmd.param2 = &outRec->rate;
err = SndDoImmediate(&outRec->channel, &cmd);
if (err != noErr)
return(err);
// Now pause with either a rateCmd or a pauseCmd, as
// appropriate.
cmd.param1 = 0;
cmd.param2 = 0;
if (outRec->rate != 0) {
// If we get something nonzero, it's safe to assume that
// whatever synthesizer we're talking to will be able to
// understand a rateCmd to restore the rate (probably the
// sampled synthesizer). To pause the sound, we'll set
// the rate to zero.
cmd.cmd = rateCmd;
err = SndDoImmediate(&outRec->channel, &cmd);
if (err != noErr)
return(err);
} else {
// This synthesizer doesn't understand rateCmds. So
// instead we'll just pause command queue processing with
// a pauseCmd. This is how we pause command-type sounds
// (e.g., Simple Beep).
cmd.cmd = pauseCmd;
err = SndDoImmediate(&outRec->channel, &cmd);
if (err != noErr)
return(err);
}
outRec->paused = true;
return(noErr);
} else return(kSHErrBadRefNum);
}
There are two basic methods of pausing a sound: one uses a pauseCmd, the other uses a rateCmd.
Sounds that are composed of a lot of little sound commands (like Simple Beep) are paused by pausing
command-queue processing with a pauseCmd. Most sampled sounds, however, have only one
command, a bufferCmd, which plays the sampled sound. A pauseCmd is ineffective for this type of
sound because it pauses command-queue processing after the completion of the bufferCmd; in
essence, the sound plays to completion before pausing. Therefore, a different approach is taken with
sampled sounds: a rateCmd is used to set the sample playback rate to 0.0, effectively stopping the
bufferCmd in its tracks.
SHPlayPause first retrieves the output record associated with the given refNum, and then checks that
the sound is not already paused. SHPlayPause then sends a getRateCmd to establish the current
playback rate of the sound. If getRateCmd returns a nonzero rate, SHPlayPause knows that a
rateCmd can be used to pause the sound; otherwise a pauseCmd is used. Either way, SHPlayPause
sets the paused flag in the output record.
SHPLAYCONTINUE
pascal OSErr SHPlayContinue(long refNum)
{
SHOutPtr outRec;
SndCommand cmd;
OSErr err;
outRec = SHOutRecFromRefNum(refNum);
if (outRec != nil) {
// Don't even bother with this stuff if the channel isn't
// paused.
if (!outRec->paused)
return(kSHErrAlreadyContinued);
// Now continue playback with a rateCmd or a resumeCmd, as
// appropriate.
cmd.param1 = 0;
if (outRec->rate != 0) {
// Resume sampled sound playback by restoring the
// synthesizer's playback rate with a rateCmd.
cmd.cmd = rateCmd;
cmd.param2 = outRec->rate;
err = SndDoImmediate(&outRec->channel, &cmd);
if (err != noErr)
return(err);
} else {
// Resume sound queue processing with a resumeCmd.
cmd.cmd = resumeCmd;
cmd.param2 = 0;
err = SndDoImmediate(&outRec->channel, &cmd);
if (err != noErr)
return(err);
}
outRec->paused = false;
return(noErr);
} else return(kSHErrBadRefNum);
}
SHPlayContinue continues the playback of a previously paused sound, checking whether there's a
nonzero rate in the output record. This is the indicator of whether to send a resumeCmd or
rateCmd. If the rate is zero, SHPlayContinue sends a resumeCmd to resume the sound. If the rate is
nonzero, SHPlayContinue sends a rateCmd to restore the sample playback rate for the sound.
SHPLAYSTATUS
pascal SHPlayStat SHPlayStatus(long refNum)
{
SHOutPtr outRec;
if (refNum >= gsSHOutVars.nextRef)
return(shpError);
else {
outRec = SHOutRecFromRefNum(refNum);
if (outRec != nil) {
// We found an SHOutRec for the refNum (so it's in use).
return((outRec->paused) ? shpPaused : shpPlaying);
} else {
// Although we've used the reference number in the past,
// it's not in use, so we can assume whatever sound it
// was associated with has stopped. Therefore, we'll
// return shpFinished.
return(shpFinished);
}
}
}
SHPlayStatus returns status information about a given sound, by reference number. The SHPlayStat
enum looks like this:
typedef enum {
shpError = -1,
shpFinished = 0,
shpPaused = 1,
shpPlaying = 2
} SHPlayStat;
SHPlayStatus uses the fact that sound reference numbers are sequential and unique to infer the status
of a sound, even if its record is no longer in the output record array. If refNum is greater than the
next available reference number, SHPlayStatus returns shpError, since refNum is invalid. If refNum
can be found in the output record list, SHPlayStatus returns shpPlaying or shpPaused, depending on
the state of the output record's paused flag. And finally, if refNum is not in use by an existing output
record but has been used in the past, it's safe to assume that playback has completed for that
reference number, and SHPlayStatus returns shpFinished.
SHGETCHANNEL
Finally, there's SHGetChannel. This routine allows you to use Helper to do sound channel
management but retain the ability to send sound commands to the channel yourself. This is most
commonly done to play looped continuous music in the background.
To use Helper to play looped continuous music, the application calls the SHPlayByHandle routine
with nil as the sound handle. This tells Helper to open
the channel without a subsequent call to SndPlay. Then the application calls SHGetChannel to
retrieve a pointer to the sound channel that Helper has set up. The application loads a sound
resource containing a soundCmd, which installs a sampled sound as a voice. It plays this sound in the
channel with a call to SndPlay, then issues a freqCmd to start it playing indefinitely. The
demonstration program SHDemo provided on theDeveloper CD Series disc gives a specific example
of this technique.
pascal OSErr SHGetChannel(long refNum, SndChannelPtr *channel)
{
SHOutPtr outRec;
// Look for the output record associated with refNum.
outRec = SHOutRecFromRefNum(refNum);
// If we found one, return a pointer to the sound channel.
if (outRec != nil) {
*channel = &outRec->channel;
return(noErr);
} else return(kSHErrBadRefNum);
}
SHGetChannel simply searches for the output record associated with refNum. If one is found, a
pointer to the sound channel is returned via the channel parameter.
EASY RECORDING ROUTINES
Helper provides routines to simplify the process of asynchronous sound recording. Most applications'
needs will be satisfied by the three easy routines, SHRecordStart, SHGetRecordedSound, and
SHRecordStop.
pascal OSErr SHRecordStart(short maxK, OSType quality,
Boolean *doneFlag)
{
Boolean deviceOpened = false;
Boolean allocated = false;
OSErr err;
short canDoAsync;
short metering;
long allocSize;
// 1. Try to open the current sound input device.
err = SPBOpenDevice(nil, siWritePermission,
&gsSHInVars.inRefNum);
if (err == noErr)
deviceOpened = true;
// 2. Now let's see if this device can handle asynchronous
// recording.
if (err == noErr) {
err = SPBGetDeviceInfo(gsSHInVars.inRefNum, siAsync,
(Ptr)&canDoAsync);
if (err == noErr && !canDoAsync)
err = kSHErrNonAsyncDevice;
}
// 3. Try to allocate memory for the application's sound.
if (err == noErr) {
allocSize = (maxK * 1024) + kSHHeaderSlop;
gsSHInVars.inHandle = NewHandle(allocSize);
if (gsSHInVars.inHandle == nil) {
err = MemError();
if (err == noErr)
err = memFullErr;
}
if (err == noErr)
allocated = true;
}
// 4. Set up various recording parameters (metering and quality).
if (err == noErr) {
metering = 1;
SPBSetDeviceInfo(gsSHInVars.inRefNum, siLevelMeterOnOff,
(Ptr)&metering);
err = SPBSetDeviceInfo(gsSHInVars.inRefNum,
siRecordingQuality, (Ptr)&quality);
}
// 5. Call SHGetDeviceSettings to determine a bunch of
// information we'll need to make a header for this sound.
if (err == noErr) {
err = SHGetDeviceSettings(gsSHInVars.inRefNum,
&gsSHInVars.numChannels, &gsSHInVars.sampleRate,
&gsSHInVars.sampleSize, &gsSHInVars.compType);
}
// 6. Create a header for this sound.
if (err == noErr) {
err = SetupSndHeader(gsSHInVars.inHandle,
gsSHInVars.numChannels, gsSHInVars.sampleRate,
gsSHInVars.sampleSize, gsSHInVars.compType,
kSHBaseNote, allocSize, &gsSHInVars.headerLength);
}
// 7. Lock the input sound handle and set up the input parameter
// block.
if (err == noErr) {
MoveHHi(gsSHInVars.inHandle);
HLock(gsSHInVars.inHandle);
allocSize -= gsSHInVars.headerLength;
gsSHInVars.inPB.inRefNum = gsSHInVars.inRefNum;
gsSHInVars.inPB.count = allocSize;
gsSHInVars.inPB.milliseconds = 0;
gsSHInVars.inPB.bufferLength = allocSize;
gsSHInVars.inPB.bufferPtr = *gsSHInVars.inHandle +
gsSHInVars.headerLength;
gsSHInVars.inPB.completionRoutine =
(ProcPtr)SHRecordCompletion;
gsSHInVars.inPB.interruptRoutine = nil;
gsSHInVars.inPB.userLong = SetCurrentA5();
// For our completion routine.
gsSHInVars.inPB.error = noErr;
gsSHInVars.inPB.unused1 = 0;
err = noErr;
}
// 8. Finally, if all went well, set our recording flag, make
// sure our recording-completed flag is clear, and initiate
// asynchronous recording.
if (err == noErr) {
gsSHInVars.recording = true;
gsSHInVars.recordComplete = false;
gsSHInVars.appComplete = doneFlag;
gsSHInVars.paused = false;
if (gsSHInVars.appComplete != nil)
*gsSHInVars.appComplete = false;
err = SPBRecord(&gsSHInVars.inPB, kSHAsync);
}
// 9. Now clean up any errors that might have occurred.
if (err != noErr) {
gsSHInVars.recording = false;
if (deviceOpened)
SPBCloseDevice(gsSHInVars.inRefNum);
if (allocated) {
DisposeHandle(gsSHInVars.inHandle);
gsSHInVars.inHandle = nil;
}
}
return(err);
}
This routine, the most lengthy in Helper, is staged, and nearly every stage can fail. Each stage does
its function and sets err to some error code. Subsequent stages execute only if the result of the
previous stage was noErr. Significant stages (like opening the sound input device and memory
allocation) set flags that allow SHRecordStart to clean up if an error occurs after one of those
operations.
The first stage tries to open the sound input device with SPBOpenDevice. The device's reference
number is stored in the inRefNum field of the input variables record. The second stage tests the
device to see if it can handle asynchronous recording. The third stage attempts to allocate the
memory buffer for the recorded sound based on the parameter maxK.
The fourth stage turns on metering (which allows Helper to retrieve the instantaneous record level)
and sets the recording quality based on the quality parameter (the Sound Manager recording values--
'good', 'betr', or 'best'). The fifth stage retrieves device settings that Helper uses to construct the
sound header. The sixth stage actually creates the header with a call to the Sound Manager routine
SetupSndHeader.
The seventh stage moves the recording handle high in the heap and locks it in preparation for
recording. SHRecordStart then sets up inPB, the sound input parameter block, in preparation for
recording. Finally, the eighth stage flags that recording is under way, clears the application's
recording-completed flag, and then initiates recording with a call to SPBRecord. If some failure
occurred, the sound handle is deallocated if necessary, and the sound input device is closed if it was
opened.
SHGETRECORDEDSOUND
pascal OSErr SHGetRecordedSound(Handle *theSound)
{
if (gsSHInVars.recordComplete) {
if (gsSHInVars.recordErr != noErr) {
*theSound = nil;
return(gsSHInVars.recordErr);
} else {
*theSound = gsSHInVars.inHandle;
return(noErr);
}
} else {
*theSound = nil;
return(kSHErrNoRecording);
}
}
SHGetRecordedSound is used by the application to retrieve the handle of a sound that has finished
recording. Once the application's recording-completed flag goes true (or SHRecordStatus indicates
"finished") it's OK to call SHGetRecordedSound. If an error terminated recording,
SHGetRecordedSound returns the error. If no error occurred, theSound is set as a handle to therecorded sound. The recorded sound can be played back with the Sound Manager or any of Helper's
playback routines, or can be written out as a 'snd ' resource.
SHRECORDSTOP
pascal OSErr SHRecordStop(void)
{
if (gsSHInVars.recording)
return(SPBStopRecording(gsSHInVars.inRefNum));
}
SHRecordStop stops recording like the stop button on a tape deck. If recording was stopped before
the entire input buffer was filled, SHIdle will shorten the sound handle to the correct size.
ADVANCED RECORDING ROUTINES
Three advanced routines give you more control over the recording process. SHRecordPause and
SHRecordContinue pause and continue recording. SHRecordStatus returns status information about
a recording sound, as well as its progress (how much has been recorded with respect to the total
space that has been allocated) and the instantaneous input level.
SHRECORDPAUSE AND SHRECORDCONTINUE
pascal OSErr SHRecordPause(void)
{
OSErr err;
if (gsSHInVars.recording) {
if (!gsSHInVars.paused) {
err = SPBPauseRecording(gsSHInVars.inRefNum);
gsSHInVars.paused = (err == noErr);
return(err);
} else return(kSHErrAlreadyPaused);
} else return(kSHErrNotRecording);
}
SHRecordPause simply pauses recording with the routine SPBPauseRecording, assuming the
recording is not already paused.
pascal OSErr SHRecordContinue(void)
{
OSErr err;
if (gsSHInVars.recording) {
if (gsSHInVars.paused) {
err = SPBResumeRecording(gsSHInVars.inRefNum);
gsSHInVars.paused = !(err == noErr);
return(err);
} else return(kSHErrAlreadyContinued);
} else return(kSHErrNotRecording);
}
SHRecordContinue resumes recording of a previously paused recording with the routine
SPBResumeRecording.
SHRECORDSTATUS
SHRecordStatus uses an SHRecordStatusRec record to provide detailed information about the
progress of a sound while it's being recorded.
typedef struct {
SHRecordStat recordStatus; // Current recording status.
unsigned long totalRecordTime;
// Total (maximum) record time in ms.
unsigned long currentRecordTime // Current recorded time in ms.
short meterLevel;
// 0..255, the current input level.
} SHRecordStatusRec;
pascal OSErr SHRecordStatus(SHRecordStatusRec *recordStatus)
{
short recStatus;
OSErr err;
unsigned long totalSamplesToRecord, numberOfSamplesRecorded;
if (gsSHInVars.recording) {
err = SPBGetRecordingStatus(gsSHInVars.inRefNum, &recStatus,
&recordStatus->meterLevel, &totalSamplesToRecord,
&numberOfSamplesRecorded,
&recordStatus->totalRecordTime,
&recordStatus->currentRecordTime);
if (err == noErr)
recordStatus->recordStatus =
(gsSHInVars.paused ? shrPaused : shrRecording);
else recordStatus->recordStatus = shrError;
return(err);
} else if (gsSHInVars.recordComplete) {
recordStatus->recordStatus = shrFinished;
recordStatus->meterLevel = 0;
// Don't know about the other fields--just leave 'em.
return(noErr);
} else return(kSHErrNotRecording);
}
An SHRecordStatusRec record contains a recordStatus field that's analogous to the playback status.
SHRecordStatus calls SPBGetRecordingStatus to get status information from the Sound Manager.
The meter level, total record time, and current record time are placed directly in the output record.
The SHRecordStat enum looks like this:
typedef enum {
shrError = -1,
shrFinished = 0,
shrPaused = 1,
shrRecording = 2
} SHRecordStat;
The recording status is set to shrError if an error occurred on the SPBGetRecordingStatus call,
shrFinished if the recordComplete flag is set, shrRecording if the sound is currently recording, or
shrPaused if the sound is recording but is paused. The information in an SHRecordStatusRec, along
with
the other routines described in this article, is enough to support an on-screen
tape deck.
USING HELPER
The best way to get a feeling for how to use Helper is to look over the source code for the small
demonstration program, SHDemo, on the CD. It demonstrates triggered sounds using SHPlayByID;
continuous background music using SHPlayByHandle and SHGetChannel; and a mini tape deck
with a level meter, progress bar, and record, stop, play, and pause buttons that work for both
recording and playback. SHDemo exercises all of Helper's calls, so you're likely to find appropriate
examples somewhere inside SHDemo. For a practical example of what Helper can do, take a look at
the RapMaster application on the CD.
JOIN THE NOISY REVOLUTION
Consider how sound, as a data type, might fit into and enhance your application. You'll still need to
implement the user interface, but Helper can shield you from many of the ugly Sound Manager
details described above, and can also form the basis for a customized sound package better suited to
the specific needs of your application. Either way, join the Noisy Revolution today!
WHY SHGETSTATE?
SHGetState is necessary because your application might trigger the same sound handle twice, the second
time while the first is still playing. If SHPlayByID used only HGetState, here's what would happen:
- At time t0 your application calls SHPlayByID. The handle's state is retrieved--unlocked and purgeable--
and stored in output record 0. So far, all is well, and the sound begins playing.
- Later, at time t1, your application makes a new call to SHPlayByID to trigger the same sound again while
the first call is still playing. SHPlayByID calls HGetState to get the handle's state--locked, nonpurgeable--
and stores it in output record 1 (perhaps you see the problem already). The sound begins playing a
second time, over the one that's already playing.
- At time t2, the first sound completes. Your application's attention flag gets set, and you dutifully call
SHIdle. SHIdle retrieves the sound's original state--unlocked and purgeable--from output record 0 and
sets the sound handle to that state.
- At time t3, the second sound completes. Again, SHIdle sets the sound handle's state according to what's
stored in the output record--locked and nonpurgeable.
We're left with the sound handle in the wrong state. So instead of HGetState, SHPlayByID uses SHGetState.
SHGetState looks to see if the sound has already been triggered, and if so, returns the state stored in the
previous trigger's output record. Also, SHReleaseOutRec doesn't reset the handle's state if the sound handle
is found to be currently playing on some other channel.
TYPES OF SOUND
The Sound Manager supports three basic types of sound. First is simple
square-wave synthesis . You can
specify
the amplitude (volume), frequency (pitch), approximate timbre, and duration of sounds for a square-wave
synthesizer with the Sound Manager commands ampCmd, timbreCmd, and freqDurationCmd.
The second type of sound is wave-table synthesis , which allows you to specify a waveform as 512 samples .
These samples specify the relative output voltage over time for one period of the waveform. Sounds with
more complex timbre can be created using a wave-table synthesizer. You control the frequency and
amplitude of wave-table sound in the same way as square-wave sound.
The most interesting sounds can be produced via the third type-- sampled synthesis . Sampled sounds are a
continuous list of relative voltages over time that allow the Sound Manager to reconstruct an arbitrary analog
waveform. This could be a recording of music, your voice --anything.
Helper allows you to easily play any of these types of sound asynchronously.
QUALITY OF SAMPLED SOUND
Two basic characteristics affect the quality of sampled sound: sample rate and sample size.
Sample rate, or the rate at which voltage samples are taken, determines the highest possible frequency that
can be recorded. Specifically, for a given sample rate, you can sample sounds of up to half that frequency.
For instance, if the sample rate is 22,254 samples per second (hertz, or Hz), the highest frequency you
could record would be around 11,000 Hz.
A commercial compact disc is sampled at 44,100 samples per second, providing a frequency response of
up to around 20,000 Hz, the limit of human hearing. Your dog, however, may find your CD player a bit
wanting.
Sample size, or quantization, determines the dynamic range of the recording (the difference between the
quietest and the loudest sound). If the sample size is eight bits, there are 256 discrete voltage levels that can
be recorded. This provides approximately 48 decibels (dB) of dynamic range. A CD's sample size is 16 bits, which provides about 96 dB of dynamic range. Humans with good hearing
are sensitive to ranges greater than 100 dB, so you're likely to see 18- or 20-bit digital audio in the next ten
years.
RELATED READING
- Inside Macintosh Volume VI (Addison-Wesley, 1991), Chapter 22, provides comprehensive information on
the latest version of the Sound Manager, including information on sound input.
- Inside Macintosh Volume V (Addison-Wesley, 1988), Chapter 2, provides user interface guidelines for the
inclusion of sound in Macintosh applications.
- Inside Macintosh Volume II (Addison-Wesley, 1985), Chapter 8, and Volume V, Chapter 27, provide a
historical perspective on sound on the Macintosh, if
you're curious. The information in these chapters is superseded by Volume VI, Chapter 22.
BRYAN K. ("BEAKER") RESSLER (AppleLink: ADOBE.BEAKER) Looking back, it seems clear that Bryan sacrificed quality
time with his wife during the writing of this article. So, in the spirit of fairness, develop asked Bryan's wife, Nicole, to
contribute the bio for her husband. Here it is: "I owe it all to my wife, without whom I wouldn't be the man I am today.
The End."*
Helper is written in C, but all public routines are declared as Pascal so that they can be called from other languages. *
A note to MacApp users: Set PermAllocation to true before calling SHInitSoundHelper; otherwise the outArray pointer may
be allocated from temporary storage.*
A note to MacApp users: You should set PermAllocation to true before calling SHRecordStart; otherwise the sound input
handle may be allocated from temporary storage. *
THANKS TO OUR TECHNICAL REVIEWERS Rich Collyer, Neil Day, Kip Olson, Jim Reekes *