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);
}