Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
SoundUnit.c
/* |
File: SoundUnit.c |
Contains: This is the "new" SoundUnit which adds some new features. |
¥ Some knowledge of the new Sound Manager is present |
in areas that were work-arounds for old Sound Manager bugs |
¥ Conversion to MPW 3.2 was established (with some amount of pain) |
¥ Added the new Sound Manager error strings |
¥ Checking of the sound header for supported encode values |
¥ The amp value is ignored (never used) in a freqDurationCmd |
¥ Added functions to test sound hardware/software features |
such as stereo, MACE, Sound Input |
Written by: |
Copyright: Copyright © 1994-1999 by Apple Computer, Inc., All Rights Reserved. |
You may incorporate this Apple sample source code into your program(s) without |
restriction. This Apple sample source code has been provided "AS IS" and the |
responsibility for its operation is yours. You are not permitted to redistribute |
this Apple sample source code as "Apple sample source code" after having made |
changes. If you're going to re-distribute the source, we require that you make |
it clear in the source that the code was descended from Apple sample source |
code, but that you've made changes. |
Change History (most recent first): |
7/29/1999 Karl Groethe Updated for Metrowerks Codewarror Pro 2.1 |
*/ |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
//includes |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
#include <Errors.h> |
#include <FixMath.h> |
#include <fp.h> |
#include <Gestalt.h> |
#include <LowMem.h> |
#include <Memory.h> |
#include <MixedMode.h> |
#include <Resources.h> |
#include <limits.h> |
#include <stddef.h> |
#include <Sound.h> |
#include <SoundInput.h> |
#include "SoundUnit.h" |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
// private constants |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
enum { |
kMaxChannels = 4, //maximum number of supported channels |
kSoundComplete = 0x1234 //flag for callBackCmd |
}; |
/* |
These are used as flags in the sound channel to determine the state |
of that channel. |
*/ |
enum { |
kChanFreeState = 0, //channel is not in use |
kChanCompleteState //channel has completed |
}; |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
// macros |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
#if GENERATINGCFM |
#define CreateRoutineDescriptor(info, proc) \ |
RoutineDescriptor g##proc##RD = BUILD_ROUTINE_DESCRIPTOR(info, proc) |
#define GetRoutineAddress(proc) (&g##proc##RD) |
#else |
#define GetRoutineAddress(proc) proc |
#endif |
// this belongs in LowMem.h |
extern unsigned short LMGetSoundActive( void ) |
TWOWORDINLINE( 0x1038, 0x027E ); /* MOVE.B $027E, D0 */ |
// For Power Macs, there is no Sound Driver and therefore SoundActive in |
// low memory should not be a problem. This is sometimes set by third parties |
// that are writting directly to the hardware, and also set by the old |
// sound driver when it is active. When this is happening, the Sound Manager |
// cannot work. So we check the low memory global to be less than 0. But |
// for Power Mac builds, we just return 0. |
#if USESROUTINEDESCRIPTORS |
#define SoundDriverActive() false |
#else |
#define SoundDriverActive() (LMGetSoundActive() < 0) |
#endif |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
// private types |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is an element of a globals array that is used to keep track of |
sound channels created by the sound unit. It contains all the useful |
information associated with the channel. I keep the 'snd ' resource |
handle associated to this channel too. This allows me to dispose of |
the data once the channel has completed its duties. |
*/ |
struct ChanInfo { |
SndChannelPtr chan; |
SndListHandle dataHandle; |
short chanState; |
short chanType; |
}; |
typedef struct ChanInfo ChanInfo; |
typedef ChanInfo *ChanInfoPtr; |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
// private prototypes |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
void FreeChan(ChanInfoPtr info); |
OSErr ChanAvailable(ChanInfoPtr info); |
Boolean CompatibleChan(ChanInfoPtr info); |
OSErr InstallSampleSnd(ChanInfoPtr info, SndListHandle sndHandle); |
OSErr NewWaveChan(ChanInfoPtr info, short init); |
pascal void DoCallBack(SndChannelPtr chan, SndCommand *theCmd); |
Boolean IsMyChan(SndChannelPtr chan); |
OSErr SndDataAvailable(SndListHandle sndHandle); |
ModRef GetSynthInfo(SndListHandle sndHandle); |
Boolean SupportedSH(SoundHeaderPtr sndPtr); |
OSErr ReleaseSynch(SndChannelPtr chan); |
OSErr Synch1Chan(SndChannelPtr chan, short count); |
OSErr SynchChans(SndChannelPtr chan1, SndChannelPtr chan2, SndChannelPtr chan3, SndChannelPtr chan4); |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
//globals (The ÒgÓ prefix is used to emphasize that a variable is global.) |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is the global array of sound channel information. |
*/ |
ChanInfoPtr gChanInfo; |
/* |
gSoundMgrVersion is used to determine if the application is running |
with the old Sound Manager. This was shipped prior to System 6.0.6. |
This flag is setup in the InitSoundUnit routine and used by the rest of this |
source file. There are workarounds to problems based on this condition. |
*/ |
short gSoundMgrVersion; |
// allocate the RoutineDescriptors for Power Mac toolbox calls |
#if GENERATINGCFM |
CreateRoutineDescriptor(uppSndCallBackProcInfo, DoCallBack); |
#endif |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is a dummy routine to allow the application to unload this segment. |
*/ |
#pragma segment SoundUnit |
pascal void _SoundUnit(void) |
{ |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Create storage for each of the four channels (4 * 1064 bytes) and |
initialize them. If any of the channels cannot be allocated, return an |
error. If the user only wants one channel, then we could just allocate |
one instead of four. These channels are used at interrupt time. |
VERSION 1.1: Added the new Sound Manager flag, gNewSndMgr. First I have to |
test if the _SoundDispatch trap is available, since SndManagerVersion is |
a selector for _SoundDispatch. This this trap isn't available, then calling |
SndManagerVersion would result in an unimplemented instruction. If an error |
is returned, it is the old Sound Manager. If it is zero, then the call |
is available (via-MIDI Mgr?) but it isn't the new Sound Manager. Greater |
than zero means it is the new Sound Manager. |
VERSION 1.2: Only supports Sound Manager 2.0 or later. I would personally |
require version 3.0 or later in my own products. |
*/ |
#pragma segment Initialize |
pascal OSErr InitSoundUnit(void) |
{ |
OSErr theErr; |
// check if the supported Sound Manager is present, this is the one |
// that has Sound Input and supports the _SoundDispatch trap |
theErr = noErr; |
gSoundMgrVersion = GetSoundMgrVersion(); |
if (gSoundMgrVersion > 1) |
{ |
gChanInfo = (ChanInfoPtr)NewPtrClear(sizeof(ChanInfo[kMaxChannels])); |
if (gChanInfo == nil) |
theErr = MemError(); |
} |
return(theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
To determine if MACE is available, there are two case. If the new Sound Manager |
is running then MACE may be built in. This is easy enough to test for by calling |
the new trap call. Otherwise I have to test for the presence of the MACE |
snth resources. The old MACE snths could only be present if the user ran the |
MACE Installer Scripts which are available from APDA. This was only supported |
on some versions of System 6.0.x. |
VERSION 1.2: Forget about dealing with MACE prior to Sound Manager 2, it was |
just a hack and didn't work on all machines. |
*/ |
#pragma segment Main |
pascal Boolean HasMACE(void) |
{ |
NumVersion version; |
Boolean result; |
result = false; |
if (GetSoundMgrVersion() > 1) |
{ |
version = MACEVersion(); |
//result = (version.majorRev > 0); //is the built-in MACE here? |
result = (*(UInt8*)&version > 0); // added by MW (see comments, above) |
} |
return (result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is used to determine if Sound Input is available. The Gestalt flag |
gestaltHasSoundInputDevice can be used to determine if an input device is |
available. This flag did not exist prior to System 7. The other related |
flag gestaltBuiltInSoundInput can only be used to determine if the machine |
has built-in sound input hardware, so don't be mislead. In System 6.0.7 you |
would have to use SPBGetIndexdDevice to find the fist one. If this returns |
noErr then Sound Input is available. Also, the icon handle returned by this |
call has to be disposed of by you the caller. |
VERSION 1.1: This is the external routine for users to determine if |
sound input is available. |
VERSION 1.2: Use the Gestalt method since we know that we're running Sound |
Manager 2 or later which supports the gestaltHasSoundInputDevice flag. |
*/ |
#pragma segment Main |
pascal Boolean HasSoundInput(void) |
{ |
long response; |
OSErr theErr; |
Boolean result; |
theErr = Gestalt(gestaltSoundAttr, &response); |
if ( (theErr == noErr) && (response & (1<<gestaltHasSoundInputDevice)) ) |
result = true; |
else |
result = false; |
return (result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
VERSION 1.1: This is the external routine for users to determine if |
sound input is available. Sound Manager 3.0 can always support stereo, |
even on mono hardware. |
*/ |
#pragma segment Main |
pascal Boolean HasStereoSupport(void) |
{ |
long response; |
OSErr theErr; |
Boolean result; |
theErr = Gestalt(gestaltSoundAttr, &response); |
if ( (theErr == noErr) && (response & (1<<gestaltStereoCapability)) ) |
result = true; |
else |
result = false; |
return (result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This will return the version of the sound manager that is currently running. |
A special case is necessary because the first sound manager that reported |
its version didn't have the SndSoundManagerVersion() implemented. So that means |
older versions are really 1.0, and the first oldest version returned by |
SndSoundManagerVersion is 2.0. Anything older than 2.0 has a few problems. |
*/ |
pascal short GetSoundMgrVersion(void) |
{ |
NumVersion version; |
long response; |
short result; |
OSErr theErr; |
result = 1; |
theErr = Gestalt(gestaltSoundAttr, &response); |
if ( (theErr == noErr) && (response & (1<<gestaltSoundIOMgrPresent)) ) |
{ |
version = SndSoundManagerVersion(); |
//result = version.majorRev; |
result = *(UInt8*)&version; // added by MW (see comments, above) |
} |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine can be called to determine if the sound has completed. When |
this is true, the data can be disposed of. It is set by the sound |
channel's completion routine. It is placed in the Main segment because |
it is called by the event loop. |
*/ |
#pragma segment Main |
pascal Boolean HasSoundCompleted(void) |
{ |
short i; |
Boolean result; |
result = true; //assume true, then check for busy channels |
for (i = 0; i < kMaxChannels; i++) |
{ |
// if we have a channel and it is not completed, then we're still busy playing |
if ((gChanInfo[i].chan != nil) && (gChanInfo[i].chanState != kChanCompleteState)) |
{ |
result = false; |
break; |
} |
} |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine can be called at any time. It will return true when the |
SoundUnit has an open channel. This can be can considered the same as |
sound being active. As long as the channel is open, no other channels can |
be opened. It is placed in the Main segment because it is called by the |
event loop. |
*/ |
#pragma segment Main |
pascal Boolean HasChannelOpen(void) |
{ |
short i; |
Boolean result; |
result = false; |
for (i = 0; i < kMaxChannels; i++) |
{ |
if (gChanInfo[i].chan != nil) |
{ |
result = true; |
break; |
} |
} |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel and timbre(sounds like "tom burr"), this will adjust the |
tone quality of the square wave synthesizer. Changing the tone can only be done |
before playing a square wave. On a Mac with the Apple Sound Chip, this can be |
done in real time while a note is playing. But, since there's no |
supported method for determining if the ASC is available I have to assume |
that it's not. I use the immediate flag to determine if the user wants to |
change the timbre now, or queue the command. If the queue is full, it |
will wait for the command to be accepted. |
BUG NOTE: There is a bug in the Sound Manager running on the Mac Plus or |
SE where sending a timbreCmd with a timbre of 255(a legal value)will |
crash. The difference between 254 and 255 isn't audible, so I only allow |
a maximum of 254 in any case. |
*/ |
#pragma segment SoundUnit |
pascal OSErr SetSquareWaveTimbre(SndChannelPtr squareChan, short timbre, Boolean immediate) |
{ |
SndCommand theCmd; |
OSErr result; |
if (timbre > 254) |
timbre = 254; |
theCmd.cmd = timbreCmd; |
theCmd.param1 = timbre; |
theCmd.param2 = 0; |
if (immediate) |
result = SndDoImmediate(squareChan, &theCmd); |
else |
result = SndDoCommand(squareChan, &theCmd, kWait); |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel and note information, this will place the note into the |
channel's queue. The note contains the amplitude and note value. It is a |
four byte parameter with the high byte containing the amplitude. I use |
SndDoCommand with the noWait flag set to wait for the channel to except |
the command in the case the queue is currently full. |
*/ |
#pragma segment SoundUnit |
pascal OSErr SendNote(SndChannelPtr chan, short duration, long note) |
{ |
SndCommand theCmd; |
theCmd.cmd = freqDurationCmd; |
theCmd.param1 = duration; |
theCmd.param2 = note; |
return(SndDoCommand(chan, &theCmd, kWait)); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel, this will place a quietCmd into the channel's queue. I |
use SndDoCommand with the noWait flag set to wait for the channel to |
except the command in the case it is currently full. |
BUG NOTE: A sequence of notes and rests will not work unless quietCmds |
are between them. Rests have to be made quiet before they rest, if that |
makes any more sense. A freqDurationCmd will loop, causing the sound in progress |
to continue, until a quietCmd is received. |
*/ |
#pragma segment SoundUnit |
pascal OSErr SendQuiet(SndChannelPtr chan, Boolean immediate) |
{ |
SndCommand theCmd; |
OSErr result; |
theCmd.cmd = quietCmd; |
theCmd.param1 = 0; |
theCmd.param2 = 0; |
if (immediate) |
result = SndDoImmediate(chan, &theCmd); |
else |
result = SndDoCommand(chan, &theCmd, kWait); |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel and duration, this will place the rest into the channel's |
queue. Before sending a rest a quietCmd is needed. Rests don't work |
unless you tell the Sound Manager to be quiet too. I use SndDoCommand |
with the noWait flag set to wait for the channel to except the command in |
the case it is currently full. |
*/ |
#pragma segment SoundUnit |
pascal OSErr SendRest(SndChannelPtr chan, short duration) |
{ |
SndCommand theCmd; |
OSErr theErr; |
theErr = SendQuiet(chan, kWait); |
if (theErr == noErr) { |
theCmd.cmd = restCmd; |
theCmd.param1 = duration; |
theCmd.param2 = 0; |
theErr = SndDoCommand(chan, &theCmd, kWait); |
} |
return(theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Test if the channel is free, and if not then call SndDisposeChannel. |
This will release the synthesizer(snth resource)code and the |
required hardware. If we didn't do this, no other channels would |
work. I also test myChan for having a snd resource attached to the |
channel. If so, then I mark it as purgeable and reset the data to nil. |
BUG NOTE: Calling SndDisposeChannel while or immediately after playing |
a sequence of notes would often hang/crash a non-Apple Sound Chip based Mac. |
Issuing a quietCmd first kept the Sound Manager happy and my Mac from |
crashing. |
*/ |
#pragma segment SoundUnit |
void FreeChan(ChanInfoPtr info) |
{ |
OSErr theErr; |
if (info->chan != nil) { |
theErr = SendQuiet(info->chan, !kWait); // ignore error |
theErr = SndDisposeChannel(info->chan, !kWait); |
info->chan = nil; |
info->chanState = kChanFreeState; |
} |
if (info->dataHandle != nil) { |
HUnlock((Handle)info->dataHandle); |
HPurge((Handle)info->dataHandle); |
info->dataHandle = nil; |
} |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine is called by an application that established a sound to be |
played asynchronously. This is in effect, the routine to be used after |
the completion routine has been called. The application should call this |
once HasSoundCompleted() returns true. In the case the application is using |
multiple channels, we will only free a channel once it has been marked as |
kChanCompleteState. |
*/ |
#pragma segment SoundUnit |
pascal void DoSoundComplete(void) |
{ |
short i; |
for (i = 0; i < kMaxChannels; i++) |
{ |
if (gChanInfo[i].chanState != kChanFreeState) |
FreeChan(&gChanInfo[i]); |
} |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is the routine that will force all channels to be released This is used by |
all routines just before opening a new channel to force all channels to be disposed. |
*/ |
#pragma segment SoundUnit |
pascal void FreeAllChans(void) |
{ |
short i; |
for (i = 0; i < kMaxChannels; i++) |
FreeChan(&gChanInfo[i]); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is the final routine to be called by the application when it is has |
finished using this SoundUnit. This will dispose of all the channels and |
memory used by this SoundUnit. |
*/ |
#pragma segment SoundUnit |
pascal void FreeSoundUnit(void) |
{ |
FreeAllChans(); |
DisposePtr((Ptr)gChanInfo); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This will be called at interrupt time by the Sound Manager when it |
receives a callBackCmd. I use the second parameter of the command to hold |
my application's A5 reference. I first set up A5 so that I can access my |
globals. I mark the given channel as being complete. This lets the |
application know that the callBackCmd has been processed. The callBackCmd |
can be used for other purposes, and the first parameter of the command |
could be a flag to a more extensive routine. Synchronizing the application |
with the channel is possible with this method. |
WARNING: This routine MUST be resident in memory and cannot make a call |
to a non-resident segment. I put this into the Main segment because of |
this. |
BUG NOTE: System 6.0.4 has a bug in _SndPlay when using a sampled sound |
'snd '. A bogus callBackCmd is placed into the queue immediately after |
the bufferCmd used to play the sound. This bogus callBackCmd will cause |
my callBackProc to be called when I wasn't expecting it. I have been |
using the command's second parameter to contain my A5 address. If I'm |
given a bogus callBackCmd, it would be really bad to set A5 address to |
this bogus parameter in the command. I found that the bogus callBackCmd |
contains the handle to the 'snd ' passed in to _SndPlay. I also found |
that param1 contains the handle's state bits(results of HGetState). To |
work with this bug I set my real callBackCmd's param1 to a specific value |
when I installed it into the queue. See the SoundComplete routine. Then |
I test the callBackCmd to make sure I'm dealing with the real one. |
VERSION 1.2: No longer using A5 globals. Get the address that we need |
in param2, instead of passing in our application's A5 address. |
*/ |
#pragma segment Main |
pascal void DoCallBack(SndChannelPtr chan, SndCommand *theCmd) |
{ |
#pragma unused (chan) |
ChanInfoPtr info; |
if (theCmd->param1 == kSoundComplete) // if it's my callBackCmd |
{ |
info = (ChanInfoPtr)theCmd->param2; |
info->chanState = kChanCompleteState; // this channel is done |
} |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
I use this to install the callBackCmd into the given channel. I need to |
pass in our A5 along with the command so that the callBack routine can |
access my globals. I also wait until the channel is ready for another |
command in the case of the channel being full. Once the Sound Manager |
calls my call back procedure I will dispose of the channel. So, this is |
the last sound command to be sent to a channel. I pass to the call back |
A5 in the second parameter of the callBackCmd. Refer to Tech Note #208. |
VERSION 1.2: No longer using A5 globals. Get the address that we need |
in param2, instead of passing in our application's A5 address. |
*/ |
#pragma segment SoundUnit |
pascal OSErr SoundComplete(SndChannelPtr chan) |
{ |
SndCommand theCmd; |
OSErr result; |
short i; |
theCmd.cmd = callBackCmd; |
theCmd.param1 = kSoundComplete; |
theCmd.param2 = 0; // initialize value |
for (i = 0; i < kMaxChannels; i++) |
{ |
if (gChanInfo[i].chan == chan) |
{ |
theCmd.param2 = (long)(&gChanInfo[i]); |
break; |
} |
} |
if (theCmd.param2 == 0) |
result = badChannel; // channel wasn't one of ours |
else |
result = SndDoCommand(chan, &theCmd, kWait); |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine will test the given channel to see if it will really produce |
sound. The Sound Manager in System 6.0x will return noErr even if the |
channel isn't going to work. In the future the Sound Manager will return |
the proper error. Until then, I use this routine to determine this for me. |
There can only be a single channel at any time, unless I have the wave |
table synthesizer open. This will allow four channels. Channels have |
a pointer to the next channel, and if this is not nil I suspect the |
given channel will not work. I test the given channel for being a |
wave type, and if so I need to see if the other channels I've got are |
also wave type. If it doesn't look like the channels is available, I |
return badChannel. |
This routine assumes the channel passed in is the first channel allocated. |
Channels are held in a linked list, and the first one to be tested has |
to be the first one allocated. |
BUG NOTE: If an application is not using the Sound Manager and instead |
uses the older Sound Driver, any given channel will fail. Or if the other |
application does not release is channels, then my channels will not work. |
The most noticeable offender of this is HyperCard. Friendly applications |
will dispose of their channels at suspend/resume times or ASAP. |
VERSION 1.1: Added the new Sound Manager test. The new Sound Manager will |
allow multiple sound channels, and returns proper error codes. |
VERSION 1.2: This is not used since we only support Sound Manager 2.0 or later. |
*/ |
#pragma segment SoundUnit |
OSErr ChanAvailable(ChanInfoPtr info) |
{ |
OSErr result; |
short i; |
if (gSoundMgrVersion >= 3) |
return(noErr); |
result = noErr; |
if (SoundDriverActive()) // check for sound driver |
return(notEnoughHardwareErr); |
if (info->chan->nextChan != nil) // looks bad |
{ |
result = badChannel; // prepart to fail |
if (info->chanType == waveTableSynth) // last attempt |
{ |
if (IsMyChan(info->chan->nextChan)) |
{ |
for (i = 0; i < kMaxChannels; i++) |
{ |
if (! (CompatibleChan(&gChanInfo[i]))) |
break; |
result = noErr; // got lucky |
} |
} |
} |
} |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is a utility routine that works with ChanAvailable. It checks to |
see if the given channel is one of the four channels we opened. If it |
is, IsMyChan returns true. If it isn't, false is returned. |
*/ |
#pragma segment SoundUnit |
Boolean IsMyChan(SndChannelPtr chan) |
{ |
short i; |
Boolean result; |
result = false; |
for (i = 0; i < kMaxChannels; i++) |
{ |
if (gChanInfo[i].chan == chan) |
{ |
result = true; |
break; |
} |
} |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is a utility routine that works with ChanAvailable. It returns |
true if the given channel is either a wave type of channel. It also |
returns true if the channel is free. Otherwise, it returns false. |
*/ |
#pragma segment SoundUnit |
Boolean CompatibleChan(ChanInfoPtr info) |
{ |
Boolean result; |
if ( (info->chanType == waveTableSynth) // wave or.. |
|| (info->chanState == kChanFreeState) ) // free chan |
result = true; |
else |
result = false; |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a resource handle, this will attempt to load it into memory. If the |
data is not available, then return an error. |
*/ |
#pragma segment SoundUnit |
OSErr SndDataAvailable(SndListHandle sndHandle) |
{ |
OSErr result; |
result = noErr; |
if (sndHandle != nil) { |
LoadResource((Handle)sndHandle); |
if (*sndHandle == nil) |
result = nilHandleErr; // master pointer is nil |
} |
else |
result = nilHandleErr; // user passed a nil handle |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is used to put the given sound resource into memory and hold it there. |
I also use a MoveHHi to keep the heap from being fragmented. If this |
fails, then I return an error. I dereference the handle and check if the |
master pointer is nil. This would mean the data could not be loaded. |
*/ |
#pragma segment SoundUnit |
pascal OSErr HoldSnd(SndListHandle sndHandle) |
{ |
OSErr result; |
if (sndHandle != nil) { |
LoadResource((Handle)sndHandle); |
if (*sndHandle == nil) |
result = nilHandleErr; // master pointer is nil |
else { |
result = noErr; |
HLockHi((Handle)sndHandle); |
} |
} |
else |
result = nilHandleErr; // user passed a nil handle |
return(result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine will return the 'snth' resource ID specified by the sound. |
I use this to determine if the given sound will work with _SndPlay. |
This routine does not require the data to be in memory when called. It |
also doesn't lock it down while looking for the information. |
VERSION 1.1: If no synth information is found in the snd then by default |
it is assumed to be for the squareWaveSynth. |
*/ |
#pragma segment SoundUnit |
ModRef GetSynthInfo(SndListHandle sndHandle) |
{ |
SndListPtr soundPtr; |
OSErr theErr; |
ModRef currSynth; |
currSynth.modNumber = kNoSynth; //initialize to no synth, and no init |
currSynth.modInit = 0; |
theErr = SndDataAvailable(sndHandle); |
if (theErr == noErr) |
{ |
soundPtr = (*sndHandle); |
if (soundPtr->format == firstSoundFormat) |
{ |
if (soundPtr->numModifiers != 0) |
currSynth = soundPtr->modifierPart[0]; |
else |
currSynth.modNumber = squareWaveSynth; |
} |
else //snd is a format 2 for HyperCard |
currSynth.modNumber = sampledSynth; |
} |
return(currSynth); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine will cruise through the given snd resource It will locate |
the sound data, if any, and return its type and offset into the resource. |
I prefer to return an offset instead of a pointer because I don't want |
to have the data locked in memory. If I return an offset, the caller |
can decided when and if it wants the resource locked down to access the |
sound data. The first step in finding this data is to determine if I'm |
looking at a format 1 or 2 type snd. A type 2 is easy, but a type 1 will |
require me to find the number of snths specified and then to skip over |
each one including the init option. Once this is done, I have a pointer |
to the number of commands in the snd. When I've found the first one, I |
examine it to find out if it is a sound data command. Being it's a sound |
resource, the command will also have its dataPointerFlag set. Once I've |
found a command I'm looking for I return its type and offset, then get out |
of the do-while block. Otherwise I go on to the next command. All of this |
makes it possible to get the sound data for use as an instrument sound. |
Typically this will be a sampled sound. |
*/ |
#pragma segment SoundUnit |
pascal long GetSndDataOffset(SndListHandle sndHandle, short *dataType, short *waveLength) |
{ |
Ptr cruisePtr; |
long sndDataOffset; |
short synths; |
short howManyCmds; |
sndDataOffset = 0; // initialize to defaults |
*dataType = kNoSynth; |
*waveLength = 0; |
if (sndHandle == nil) |
return (sndDataOffset); // return no data |
if (*sndHandle != nil) { |
if ((**sndHandle).format == firstSoundFormat) { |
synths = (**sndHandle).numModifiers; |
cruisePtr = (Ptr)&(**sndHandle).modifierPart; |
cruisePtr += (sizeof(ModRef) * synths); |
} |
else |
cruisePtr = (Ptr)&((**(Snd2ListHandle)sndHandle).numCommands); |
howManyCmds = *(short *)cruisePtr; // pointing at number of cmds |
cruisePtr += sizeof(howManyCmds); |
// cruisePtr is now at the first sound command |
// cruise all commands and find a soundCmd or bufferCmd |
do { |
switch (((SndCmdPtr)cruisePtr)->cmd) { |
case soundCmd | dataOffsetFlag: |
case bufferCmd | dataOffsetFlag: |
*dataType = sampledSynth; |
sndDataOffset = ((SndCmdPtr)cruisePtr)->param2; |
howManyCmds = 0; // done, get out of loop |
break; |
case waveTableCmd | dataOffsetFlag: |
*dataType = waveTableSynth; |
*waveLength = ((SndCmdPtr)cruisePtr)->param1; |
sndDataOffset = ((SndCmdPtr)cruisePtr)->param2; |
howManyCmds = 0; // done, get out of loop |
break; |
default: // catch any other type of cmd |
cruisePtr += sizeof(SndCommand); |
howManyCmds -= 1; |
break; |
} |
} while (howManyCmds >= 1); // done with all the commands |
} |
return(sndDataOffset); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
VERSION 1.1: Check the given sound header as being supported by the |
running Sound Manager. The encode fields of the header are tested. |
The standard encode always works. The MACE compressed sound will only work if |
MACE is present. A MACE sound can also be a stereo sound, which will only |
work on stereo hardware. The expanded sound is for a stereo sound and/or |
16bit samples and is only supported by Sound Manager 2.0 or later. |
If the sound is a stereo sound, then it requires stereo support. |
VERSION 1.2: Support for Sound Manager 3 and beyond. Any compressed |
sound may work if Sound Manager 3.0 or later is present. |
*/ |
#pragma segment SoundUnit |
Boolean SupportedSH(SoundHeaderPtr sndPtr) |
{ |
Boolean result; |
result = false; |
switch (sndPtr->encode) |
{ |
case stdSH: |
result = true; |
break; |
case cmpSH: |
if (gSoundMgrVersion < 3) |
{ |
//Sound Manager 2.0 will only support MACE compressed sound |
//and only stereo sound on stereo machines. |
if ( (((CmpSoundHeaderPtr)sndPtr)->snthID == MACE3snthID) |
|| (((CmpSoundHeaderPtr)sndPtr)->snthID == MACE6snthID) ) |
{ |
if (((CmpSoundHeaderPtr)sndPtr)->numChannels == 1) |
result = true; |
else |
result = HasStereoSupport(); |
} |
} |
else |
result = true; //Sound Manager 3 and later does it all |
break; |
case extSH: // first check for 8 bit sounds |
if (gSoundMgrVersion < 3) |
{ |
if (((ExtSoundHeaderPtr)sndPtr)->sampleSize == 8) |
{ |
if (((ExtSoundHeaderPtr)sndPtr)->numChannels == 1) |
result = true; // it's a mono sound, no problem |
else |
result = HasStereoSupport(); // only if we have stereo |
} |
else |
result = false; // non-8 bit not allowed |
} |
else // must be a 16 bit sound, Sound Manager 3 and later does it all |
result = true; |
break; |
} |
return (result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel and sampled sound resource, this routine will install the |
sound into the channel for use as an instrument. This allows an |
application to send freqDurationCmds to the channel and play a melody. If I sent |
a bufferCmd instead of the soundCmd, the Sound Manager would play the |
sampled sound. This is basically what _SndPlay would do with a format 2 |
snd. I insure that I am using only the proper buffer format having the |
standard encode option. If I were to support compressed sounds, I would |
have to call the MACE synthesizers to expand the buffer before I can use |
it as an instrument. If I don't get a sampled sound of standard encoding |
I'll return a bad format error. I use _SndDoImmediate to get the sound |
installed because I don't want this command to be queued. |
VERSION 1.1: Check for a supported sound header. |
*/ |
OSErr InstallSampleSnd(ChanInfoPtr info, SndListHandle sndHandle) |
{ |
SndCommand theCmd; |
SoundHeaderPtr dataPtr; |
long dataOffset; |
short sndDataType; |
short ignore; |
OSErr theErr; |
theErr = HoldSnd(sndHandle); |
if (theErr == noErr) { |
dataOffset = GetSndDataOffset(sndHandle, &sndDataType, &ignore); |
if (sndDataType == sampledSynth) { |
dataPtr = (SoundHeaderPtr)((long)(*sndHandle) + dataOffset); |
if (stdSH == dataPtr->encode) { |
theCmd.cmd = soundCmd; |
theCmd.param1 = 0; |
theCmd.param2 = (long)dataPtr; |
info->dataHandle = sndHandle; |
theErr = SndDoImmediate(info->chan, &theCmd); |
} else |
theErr = badFormat; //return a bad format error |
} else |
theErr = badFormat; //return a bad format error |
if (theErr != noErr) { |
HUnlock((Handle)sndHandle); //and free up the resource |
HPurge((Handle)sndHandle); |
} |
} |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine will create a sampled sound channel using the INIT option |
given. Typically this will be 0. In any case with System 6.0x this |
option is ignored by the sampled sound synthesizer. The given sound |
resource will be installed into the channel for use as an instrument. |
WARNING: If the application does not want an instrument sound, then the |
sndInstrument handle MUST be passed in as nil. |
BUG NOTE: The sampled sound synthesizer in System 6.0x does not check for |
a Memory Manager error when allocating its internal buffer. There is a |
call to NewPtr(1316)and if a nil is returned, the Sound Manager will |
write randomly to low memory. This can occur when calling _SysBeep under |
low memory conditions. Also, this pointer is allocated into the |
application's heap instead of the system's. |
VERSION 1.2: We no longer need to call ChanAvailable since we're only |
supporting Sound Manager 2.0 or later. |
*/ |
#pragma segment SoundUnit |
pascal OSErr GetSampleChan(SndChannelPtr *sampleChan, long init, SndListHandle sndInstrument) |
{ |
OSErr theErr; |
FreeAllChans(); |
theErr = SndNewChannel(&(gChanInfo[0].chan), sampledSynth, |
init, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[0].chanType = sampledSynth; |
theErr = InstallSampleSnd(&gChanInfo[0], sndInstrument); |
} |
if (theErr != noErr) |
FreeAllChans(); |
*sampleChan = gChanInfo[0].chan; |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Only supported by Sound Manager 2.0 and later |
*/ |
#pragma segment SoundUnit |
pascal OSErr Get4SampleInstruments(SndChannelPtr *sampleChan1, SndChannelPtr *sampleChan2, |
SndChannelPtr *sampleChan3, SndChannelPtr *sampleChan4, |
SndListHandle sndInstrument1, SndListHandle sndInstrument2, |
SndListHandle sndInstrument3, SndListHandle sndInstrument4) |
{ |
OSErr theErr; |
FreeAllChans(); |
theErr = SndNewChannel(&(gChanInfo[0].chan), sampledSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[0].chanType = sampledSynth; |
theErr = InstallSampleSnd(&gChanInfo[0], sndInstrument1); |
if (theErr == noErr) { |
theErr = SndNewChannel(&(gChanInfo[1].chan), sampledSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[1].chanType = sampledSynth; |
theErr = InstallSampleSnd(&gChanInfo[1], sndInstrument2); |
if (theErr == noErr) { |
theErr = SndNewChannel(&(gChanInfo[2].chan), sampledSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[2].chanType = sampledSynth; |
theErr = InstallSampleSnd(&gChanInfo[2], sndInstrument3); |
if (theErr == noErr) { |
theErr = SndNewChannel(&(gChanInfo[3].chan), sampledSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[3].chanType = sampledSynth; |
theErr = InstallSampleSnd(&gChanInfo[3], sndInstrument4); |
} |
} |
} |
} |
} |
} |
} |
if (theErr == noErr) |
{ |
*sampleChan1 = gChanInfo[0].chan; |
*sampleChan2 = gChanInfo[1].chan; |
*sampleChan3 = gChanInfo[2].chan; |
*sampleChan4 = gChanInfo[3].chan; |
} |
else |
FreeAllChans(); |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a channel and pointer to a wave table, this will install the wave |
for use as an instrument into the channel. If I find the application |
giving me a nil pointer, I'll return an error. I use _SndDoImmediate |
to get the sound installed because I don't want this to be queued. |
*/ |
#pragma segment SoundUnit |
pascal OSErr InstallWave(SndChannelPtr waveChan, Ptr aWavePtr, short waveLength) |
{ |
SndCommand theCmd; |
OSErr result; |
if (aWavePtr != nil) { |
theCmd.cmd = waveTableCmd; |
theCmd.param1 = waveLength; |
theCmd.param2 = (long)aWavePtr; |
result = SndDoImmediate(waveChan, &theCmd); |
} |
else |
result = memPCErr; // Pointer Check failed |
return (result); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
NewWaveChan creates a new wave table channel and sets myChan to point |
to it. If any error occurs, the error code is returned as a function |
result. |
*/ |
#pragma segment SoundUnit |
OSErr NewWaveChan(ChanInfoPtr info, short init) |
{ |
OSErr theErr; |
theErr = SndNewChannel(&(info->chan), waveTableSynth, |
init, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) |
info->chanType = waveTableSynth; |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This will return four wave table channels with their waves installed. |
This must be done before calling ChanAvailable. Otherwise that test will |
fail. If I cannot obtain all four wave channels I will dispose of the |
ones I did get before returning the error. This routine expects to find |
four wave table pointers, or it will fail. |
Versions 1.2: After calling NewWaveChan, we do not need to call ChanAvailable |
since we know that Sound Manager 2 or later will not do the wrong thing. |
*/ |
#pragma segment SoundUnit |
pascal OSErr GetWaveChans(SndChannelPtr *waveChan1, SndChannelPtr *waveChan2, |
SndChannelPtr *waveChan3, SndChannelPtr *waveChan4) |
{ |
OSErr theErr; |
FreeAllChans(); |
theErr = NewWaveChan(&gChanInfo[0], initStereo); |
if (theErr == noErr) |
{ |
theErr = NewWaveChan(&gChanInfo[1], initStereo); |
if (theErr == noErr) |
{ |
theErr = NewWaveChan(&gChanInfo[2], initStereo); |
if (theErr == noErr) |
theErr = NewWaveChan(&gChanInfo[3], initStereo); |
} |
} |
if (theErr != noErr) |
FreeAllChans(); // we didn't make it |
else { |
*waveChan1 = gChanInfo[0].chan; |
*waveChan2 = gChanInfo[1].chan; |
*waveChan3 = gChanInfo[2].chan; |
*waveChan4 = gChanInfo[3].chan; |
} |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This will create a channel for the square wave synthesizer. There |
are no INIT options used by this synthesizer, but I will set the timbre |
to adjust the tone quality. |
VERSION 1.2: We no longer need to call ChanAvailable since we're only |
supporting Sound Manager 2.0 or later. |
*/ |
#pragma segment Sound |
pascal OSErr GetSquareWaveChan(SndChannelPtr *squareChan, short timbre) |
{ |
OSErr theErr; |
FreeAllChans(); |
theErr = SndNewChannel(&(gChanInfo[0].chan), squareWaveSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) { |
gChanInfo[0].chanType = squareWaveSynth; |
theErr = SetSquareWaveTimbre(gChanInfo[0].chan, timbre, !kWait); |
} |
if (theErr != noErr) |
FreeAllChans(); |
*squareChan = gChanInfo[0].chan; |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is the routine to create a channel that isn't associated with any |
synthesizer. Why? Because if you wanted to use _SndPlay asynchronously |
you need to get such a channel. |
BUG NOTE: Do not use a channel already associated to a snth with |
_SndPlay. This causes the Sound Manager to install a second copy of the |
same snth. |
VERSION 1.2: We no longer need to call ChanAvailable since we're only |
supporting Sound Manager 2.0 or later. |
*/ |
#pragma segment SoundUnit |
pascal OSErr GetNoSynthChan(SndChannelPtr *chan) |
{ |
OSErr theErr; |
FreeAllChans(); |
theErr = SndNewChannel(&(gChanInfo[0].chan), kNoSynth, |
kInitNone, GetRoutineAddress(DoCallBack)); |
if (theErr == noErr) |
gChanInfo[0].chanType = kNoSynth; |
if (theErr != noErr) |
FreeAllChans(); |
*chan = gChanInfo[0].chan; |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This routine will use the given channel and snd resource with _SndPlay. |
This is used to play a sound, which is a series of sound commands commonly |
referred to as a sequence. First thing I do is make sure the song fits in |
memory. _SndPlay will lock this resource in memory and then pump the snd |
for all of its worth. I am calling it asynchronously, and if I was using |
a snd that contained sound data I wouldn't mark the snd as being |
purgeable. But in this case, _SndPlay will be done with the snd as soon |
as it returns because it copied all of the commands into the channel. |
(There's no data associated with a sequence, just commands.) _SndPlay |
will not return until it has done so. After _SndPlay I need to work |
around a bug in the freqDurationCmd. The last thing to do is to send a |
callBackCmd to signal me that the channel has completed. If any Sound |
Manager errors are encountered, I return them to the application. If the |
application passed me a nil snd handle, I'll return an error. |
WARNING: Make sure you are using a snd that only has note type commands |
in it and not something such as a bufferCmd. |
BUG NOTE: There is problem when the final sound command is a freqDurationCmd. |
The note will continue to sound, looping forever, until a quietCmd is sent |
or the channel is disposed of. To prevent unwanted looping, I send a |
quietCmd after all notes. Also read a related bug note when disposing of |
channels in the routine FreeChan(). |
*/ |
#pragma segment SoundUnit |
pascal OSErr PlaySong(SndChannelPtr chan, SndListHandle sndSong) |
{ |
OSErr theErr; |
theErr = SndDataAvailable(sndSong); // get the data loaded |
if (theErr == noErr) { |
theErr = SndPlay(chan, sndSong, kSMAsynch); // pump the sound |
HUnlock((Handle)sndSong); |
HPurge((Handle)sndSong); |
if (theErr == noErr) { |
theErr = SendQuiet(chan, kWait); // work around bug |
if (theErr == noErr) |
theErr = SoundComplete(chan); |
} |
else |
theErr = nilHandleErr; // snd data was not available |
} |
if (theErr != noErr) |
FreeAllChans(); |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is used to send a syncCmd to a channel and causes the other channels |
that are being held by a synchCmd to be released. Of course, this assumes |
the application has already called SynchChans. _SndDoImmediate is used |
to get the command directly to the synthesizer bypassing the queue. |
BUG NOTE: I've found that immediately clearing the channels and starting |
new ones may cause the channels to startup playing out of synch? This |
happens while disposing the wave channels and starting them immediately. |
*/ |
#pragma segment SoundUnit |
OSErr ReleaseSynch(SndChannelPtr chan) |
{ |
SndCommand theCmd; |
theCmd.cmd = syncCmd; |
theCmd.param1 = 1; |
theCmd.param2 = kSyncID; |
return (SndDoImmediate(chan, &theCmd)); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is a utility routine for SynchChans. It sends a syncCmd command |
to the channel specified by chan using the count parameter specified |
by count. Synch1Chan returns whatever error that SndDoImmediate returns. |
*/ |
#pragma segment SoundUnit |
OSErr Synch1Chan(SndChannelPtr chan, short count) |
{ |
SndCommand theCmd; |
theCmd.cmd = syncCmd; |
theCmd.param1 = count; |
theCmd.param2 = kSyncID; |
return (SndDoImmediate(chan, &theCmd)); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
This is used to synchronize four wave table channels. By first sending |
the synchCmd, I can send a sequence of other commands to the channel and |
not have the channel attempt to start processing any of them. That is until |
another synchCmd is sent causing all of the previous synchCmd's counter |
to be decremented. After getting all the channels in synch and sending |
the sequence of further commands, then use the ReleaseSynch routine to |
start all of the channels processing their respective queues. |
_SndDoImmediate is used to get the command directly to the synthesizer |
bypassing the queue. |
*/ |
#pragma segment SoundUnit |
OSErr SynchChans(SndChannelPtr chan1, SndChannelPtr chan2, |
SndChannelPtr chan3, SndChannelPtr chan4) |
{ |
OSErr theErr; |
theErr = Synch1Chan(chan4, 5); |
if (theErr == noErr) { |
theErr = Synch1Chan(chan3, 4); |
if (theErr == noErr) { |
theErr = Synch1Chan(chan2, 3); |
if (theErr == noErr) |
theErr = Synch1Chan(chan1, 2); |
} |
} |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
In order to synchronize channels, the synchCmd is needed. Once all of the |
song has been sent into each channel, a final synchCmd is issued to |
release them. Don't send more commands into a channel that it can hold at |
one time while the channel is in synch mode. |
*/ |
#pragma segment SoundUnit |
pascal OSErr Play4ChanSongs(SndChannelPtr chan1, SndChannelPtr chan2, |
SndChannelPtr chan3, SndChannelPtr chan4, |
SndListHandle song1, SndListHandle song2, |
SndListHandle song3, SndListHandle song4) |
{ |
OSErr theErr; |
theErr = SynchChans(chan1, chan2, chan3, chan4); |
if (theErr == noErr) { |
theErr = PlaySong(chan1, song1); |
if (theErr == noErr) { |
theErr = PlaySong(chan2, song2); |
if (theErr == noErr) { |
theErr = PlaySong(chan3, song3); |
if (theErr == noErr) { |
theErr = PlaySong(chan4, song4); |
if (theErr == noErr) |
theErr = ReleaseSynch(chan1); |
} |
} |
} |
} |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
WARNING: IT IS RECOMMENDED THAT YOU DO NOT USE THIS CODE. I've provided |
this routine because people have asked me how HyperCard performs its PLAY |
command and why their HyperCard sounds do not sound right using the |
_SndPlay routine. The correct answer is that _SndPlay plays the sound |
correctly. HyperCard is attempting to change the frequency by adjusting |
the sample rate. This is NOT the correct approach. Define the sound |
buffer as it should be played. _SndPlay plays the sound as it is defined. |
If the result from _SndPlay is not what you want, then it is the sample |
that is incorrect and should be edited. The sample rate is the rate at |
which the sound was recorded. If you didn't record it, then how do you |
know what's the correct rate? Set the baseFrequency to the note that was |
recorded. If you recorded middle C at 22k, then the rate is 22k and the |
baseFrequency is middle C. HyperCard is incorrect in using the sample rate as |
the frequency of the sound. Furthermore, using this technique of |
calculating a new sample rate can introduce errors. The resulting sample |
rate will not be the proper pitch. Also, the sample rate for high pitches |
will be very inaccurate and impossible for the Mac to reproduce. Such a |
problem can happen if the given sample rate was 22k and is to be played |
back at three octaves higher. Even 44k samples transposed up a half |
octave will fail. Using the soundCmd and freqDurationCmd will not have this |
problem. |
Given a sound resource, this routine will play it in the manner that |
HyperCard does. HyperCard assumes that a sound is to be played at middle |
C when the user does not specify a note value in the PLAY command. I |
don't know why. (What's middle C when I want to hear speech or the sound |
of crickets?) At any rate(pun intended), I get a sampled sound channel. |
I find the sound data offset in the resource, which has to be locked down |
at this time. Once I have the sound data, I get its original sample rate. |
I have to calculate what a new sample rate would be based on its baseFrequency. |
The baseFrequency is the note at which the sound was recorded. I'm not sure |
what this means to crickets, but if this is set to middle C then HyperCard |
doesn't attempt to modify the sample rate. (If you're wondering how the |
math works in this routine, buy a book on music theory. I'm here to |
provide Mac support.) Once I've adjusted the sample rate, I use the |
bufferCmd to play it. Then I restore the sound resource to its original |
state. If I didn't do this it would be possible that the resource was |
still in memory the next time I use it having the adjusted sample rate. |
This would cause me to incorrectly adjust it again. Unlike HyperCard, I |
can do this for both a format 1 and 2. |
BUG NOTE: Do not call SANE while the Sound Manager is running. Refer to |
Tech Note #235. |
VERSION 1.1: Replaced the previous test of the sound header's encode |
value. The previous version was incorrect. The bufferCmd will automatically |
de-code a MACE compressed sound if MACE is available. Otherwise, the sound |
will not be able to be used. So, a new routine is being used to check for |
supported sound headers. The conflict with the Sound Manager and SANE was |
resolved in the new Sound Manager. The Sound Manager no longers uses extended |
numbers at interrupt level, and instead uses fixed math. |
*/ |
#pragma segment SoundUnit |
pascal OSErr HyperSndPlay(SndListHandle sndHandle) |
{ |
SndCommand theCmd; |
OSErr theErr; |
long dataOffset; |
short sndDataType; |
short ignore; |
SoundHeaderPtr dataPtr; |
short thePower; |
double_t newRate; |
Fixed oldRate; |
theErr = HoldSnd(sndHandle); |
if (theErr == noErr) { |
theErr = GetSampleChan(&(gChanInfo[0].chan), kInitNone, nil); |
gChanInfo[0].dataHandle = sndHandle; // so FreeAllChans can dispose of data |
if (theErr == noErr) { |
dataOffset = GetSndDataOffset(sndHandle, &sndDataType, &ignore); |
if (sndDataType == sampledSynth) { |
dataPtr = (SoundHeaderPtr)((long)*sndHandle + dataOffset); |
if (SupportedSH(dataPtr)) { |
oldRate = dataPtr->sampleRate; // save original sample rate |
if (dataPtr->baseFrequency != kMiddleC) { |
if (dataPtr->sampleRate > (SHRT_MAX << 16)) // large positive number |
newRate = Fix2X(dataPtr->sampleRate - (SHRT_MAX << 16)) |
+ (double_t)SHRT_MAX; |
else |
newRate = Fix2X(dataPtr->sampleRate); |
thePower = (kMiddleC) - (dataPtr->baseFrequency); |
dataPtr->sampleRate = X2Fix(newRate * |
pow((double_t)twelfthRootTwo, (double_t)thePower)); |
} |
theCmd.cmd = bufferCmd; |
theCmd.param1 = 0; |
theCmd.param2 = (long)dataPtr; |
theErr = SndDoImmediate(gChanInfo[0].chan, &theCmd); |
if (theErr == noErr) |
theErr = SoundComplete(gChanInfo[0].chan); |
dataPtr->sampleRate = oldRate; // restore original sample rate |
} else |
theErr = badFormat; //not SupportedSH |
} else |
theErr = badFormat; //sndDataType not sampledSynth |
} |
} |
if (theErr != noErr) |
FreeAllChans(); |
return (theErr); |
} |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
/* |
Given a sound resource, this routine will call _SndPlay. The snd must |
be either a format 2 or format 1 that contains snth information. |
Using _SndPlay asynchronously requires us to lock the snd prior to |
calling the trap. The reason being is _SndPlay calls HSetState as soon |
as the trap exits, and restores the handle to whatever is was just before |
the call. This would be bad when using the sound asynchronously. If the |
sound being passed in happens to be a compressed sound created with MACE, |
it will "do the right thing." If MACE isn't around the Sound Manager |
will pretend to play a sound but nothing will be heard. |
BUG NOTE: The sampled sound synthesizer in System 6.0x does not check for |
a Memory Manager error when allocating its internal buffer. There is a |
call to NewPtr(1316)and if a nil is return, the Sound Manager will write |
randomly to memory. Also, the pointer is allocated into the application's |
heap instead of the system's. |
BUG NOTE: _SndPlay when using System 6.0.4 and a sampled sound will send |
a bogus callBackCmd into the channel. This will cause the user's call |
back procedure to be called as soon as the sound has completed. Refer |
to the DoCallBack routine for details. |
VERSION 1.1: Add the check for the sound header being supported and |
replaced the check of the snth information. No synth information in the |
snd is valid and would mean to play the snd using the squareWaveSynth. |
*/ |
#pragma segment SoundUnit |
pascal OSErr AsynchSndPlay(SndListHandle sndHandle) |
{ |
SoundHeaderPtr dataPtr; |
OSErr theErr; |
long dataOffset; |
short sndDataType; |
short ignore; |
theErr = HoldSnd(sndHandle); //hold on to the sound |
if (theErr == noErr) { |
theErr = GetNoSynthChan(&(gChanInfo[0].chan)); |
gChanInfo[0].dataHandle = sndHandle; //so FreeAllChans can dispose of data |
if (theErr == noErr) { |
gChanInfo[0].chanType = GetSynthInfo(sndHandle).modNumber; |
dataOffset = GetSndDataOffset(sndHandle, &sndDataType, &ignore); |
if (sndDataType == sampledSynth) { |
dataPtr = (SoundHeaderPtr)((long)*sndHandle + dataOffset); |
if ( !(SupportedSH(dataPtr)) ) |
theErr = badFormat; |
} |
if (theErr == noErr) { |
theErr = SndPlay(gChanInfo[0].chan, sndHandle, kSMAsynch); |
if (theErr == noErr) |
theErr = SoundComplete(gChanInfo[0].chan); |
} |
} |
} |
if (theErr != noErr) |
FreeAllChans(); |
return(theErr); |
} |
Copyright © 2003 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2003-03-14