GameSource/GameSounds.c

/*
    This file manages the asynchronous sounds used by the game.
    It demonstrates one of the simplest techniques of playing asynch sounds,
    using SndPlay with a callback.  Notice this code does NOT depend on register a5
    for flagging that a sound is done, instead I stuff a pointer to the flag
    in the callback parameters.  I am pointing this out because I think it is a much better
    way to do things, and the same technique can apply to any interrupt time code.
    
    This also manages each channel by using a channel priority.  When a sound play request
    is received for a specific channel, if the sound has a higher priority it is played,
    otherwise it is ignored.
    
    This code is pretty re-usable,  I have used it in a few other programs as well.
    
    Call InitSounds( filename )  to start it up.  The file passed is a resource file
    that contains all the snds you need.
    
    Then use PlaySndAsynchChannel to play a sound, specifying the sound resource ID, the channel
    number, and the priorty.  Right now this handles kNumChannels of sound.
    
    Call SoundKeeper() from your idle routine to make sure that the sound channels are
    disposed of when needed.
    
    Call FreeSounds before your program quits.  Thats really the minimum you need in order
    to use this code.  There is more to it, read the code and comments that follow.
    
    
    
    A point to mention here.  There are so many different Macintosh sound managers out there,
    that it is hard to write good code that runs on all of them.  This code SHOULD work on all
    of them.  If not, please let me know.  See the feedback forms.
    
    This code has two forks in it.  If Sound Manager 3.0 is running, then it creates the
    sound channels at the start of the program, and keeps using them until the program quits.
    Earlier versions of the Sound Manager contained bugs, which caused intermittent failures
    unless a fresh sound channel was used for each asynchronous sound.  So, if a sound manager
    earlier than 3.0 is installed, then this unit will create and dispose of sound channels
    on the fly.
    
    UNDER Sound Manager 3.0 on an AV macintosh with a DSP, it is important to open the channel
    once and keep using it until you are done.  This is because every time the channel is opened
    (or closed) the disk may be hit as the DSP Synth code is loaded.
    
    Creating and disposing of a sound channel is not the most efficient way to play sounds, because the channel is created and
    disposed of for each sound, requiring the memory manager to scam a block from the heap.
 
    It would be better to open a channel, leave it open, and keep jamming bufferCmds in to 
    play each sound instead.  However - this does not work on all macs, like MacClassics, plusses
    and some others.  If Sound Manager 3.0 is installed, then this code will work well.
 
    I have made this the simplest way of playing asynchronous sounds, but it is NOT the best for 
    each particular Macintosh.
 
    Double buffering is good because it can use less RAM, but it is bad because if you are spooling
    the sound in then you are reading the disk or custom un-compressing, or otherwise doing
    some amount of processing to fill the buffer, and this can slow the program down, not to mention
    increase the complexity of any sound playing code.  
    
*/
 
#include "GameSounds.h"
 
#include "ZAMProtos.h"
 
unsigned char   gSoundMgrVersion;
SndChanInfo     gChan[kNumChan];
short           gSndResFile;
Boolean         gSoundEnable;
 
void SoundEnable(void);
 
void InitSounds(Str255  sndFileName)
/*
    open up the sound resource file, and preserve the resref num and stuff
    
*/
{
    short   i;
    OSErr   err;
    short   prevResFile;
    
    
    SoundEnable();
     
    /* get the version number of the sound manager */
    gSoundMgrVersion = SndSoundManagerVersion().majorRev;
 
    for(i = 0; i < kNumChan; i++) {
        gChan[i].channel = nil;
        gChan[i].priority = 0;
 
        if(gSoundMgrVersion >= 3) {
            /* we can pre-allocate the channels and leave them open till we quit */
            err = SndNewChannel(&gChan[i].channel, sampledSynth, 
                            initMono + initNoDrop + initNoInterp, SndDoneProc);
            if(err) {
                ErrMsgCode("\pError opening sound channel");
                ExitToShell();
            }
        }
    }
    
    /* now open the file with our sounds in it */
    prevResFile = CurResFile(); 
    gSndResFile = OpenResFile(sndFileName);
    if(gSndResFile == -1) {
        ErrMsg("\pError opening sound resource file");
    }
    
    /* remove the sound file from chain */
    UseResFile(prevResFile);
}
 
 
void SoundDisable(void)
/*
    turn sounds off
    
*/
{
    gSoundEnable = false;
}
 
void SoundEnable(void)
/*
    turn sounds on
    
*/
{
    gSoundEnable = true;
}
 
void StopAllSounds(void)
/*
    Stop all sounds from playing by dumping them
*/
{
    short   i;
    
    for(i = 0; i < kNumChan; i++) {
        if(gChan[i].priority)
            SndDisposeChannel (gChan[i].channel, true);
    }
}
 
 
Handle  GetSound(short  sndID)
/*
    Use this to get sounds from the file
    This makes the sound res file current again
    and grabs the sound.
    
    Inside Mac is ambiguous about locking down sound handles.
    
    They MUST be locked down before passed to sound play.
*/
{
    short   saveResFile;
    Handle  snd;
    
    saveResFile = CurResFile();
 
    UseResFile(gSndResFile);
    snd = Get1Resource('snd ',sndID);
    HLock(snd);
    UseResFile(saveResFile);
 
    return snd;
}
 
OSErr PlaySndAsynchChannel(short sndID, short chanNum, short priority)
/*
    This is the main sampled sound playing routine.  See comments above.
    
*/
{
    OSErr           err = noErr;
    SndCommand      cmd;
    Handle          snd;
    
    if (!gSoundEnable)  return;
    
    snd = GetSound(sndID);
    if(snd != nil) {
        if( (gChan[chanNum].priority != 0)  && (priority >= gChan[chanNum].priority) ) {
            /* if we got the sound and the channel is busy, and our priority is high enough */
            /* then we are going to play, otherwise we bail */
            if(gChan[chanNum].priority) {
                if(gSoundMgrVersion < 0x03) {
                    SndDisposeChannel (gChan[chanNum].channel, true);
                } else {
                    /* since the good sound manager is around, stop playing the sound */
                    cmd.cmd = quietCmd;
                    cmd.param1 = 0;
                    cmd.param2 = 0;
                    err = SndDoImmediate(gChan[chanNum].channel, &cmd);
                    cmd.cmd = flushCmd;
                    err = SndDoImmediate(gChan[chanNum].channel, &cmd);
                }
            }
        }
 
        if(priority >= gChan[chanNum].priority) {
            if( gSoundMgrVersion < 0x03) {
                /* old sound manager around, so create a new sound channel */
                gChan[chanNum].channel = nil;
                err = SndNewChannel(&gChan[chanNum].channel, sampledSynth, initMono + initNoDrop + initNoInterp, SndDoneProc);
            }
            if(err == noErr) {
                err = SndPlay (gChan[chanNum].channel, snd, true);
                if (err == noErr) {
                    gChan[chanNum].sndHandle = snd;
                    gChan[chanNum].priority = priority;
                    cmd.cmd = callBackCmd;
                    cmd.param2 = (long)&gChan[chanNum];
                    err = SndDoCommand (gChan[chanNum].channel, &cmd, false);
                }
            }
        }
    }
    
    return err;
}
 
 
OSErr PlaySndAsynchChannelNow(short sndID, short chanNum, short priority)
/*
    The same as above, except does not check if sounds are enabled.
    This was a hacky way to make sure that you could hear the sorry bub you loose sound.
*/
{
    OSErr           err = noErr;
    SndCommand      cmd;
    Handle          snd;
    
    snd = GetSound(sndID);
    if(snd != nil) {
        if( (gChan[chanNum].priority != 0)  && (priority >= gChan[chanNum].priority) ) {
            /* if we got the sound and the channel is busy, and our priority is high enough */
            /* then we are going to play, otherwise we bail */
            if(gChan[chanNum].priority) {
                if(gSoundMgrVersion < 0x03) {
                    SndDisposeChannel (gChan[chanNum].channel, true);
                } else {
                    /* since the good sound manager is around, stop playing the sound */
                    cmd.cmd = quietCmd;
                    cmd.param1 = 0;
                    cmd.param2 = 0;
                    err = SndDoImmediate(gChan[chanNum].channel, &cmd);
                    cmd.cmd = flushCmd;
                    err = SndDoImmediate(gChan[chanNum].channel, &cmd);
                }
            }
        }
 
        if(priority >= gChan[chanNum].priority) {
            if( gSoundMgrVersion < 0x03) {
                /* old sound manager around, so create a new sound channel */
                gChan[chanNum].channel = nil;
                err = SndNewChannel(&gChan[chanNum].channel, sampledSynth, initMono + initNoDrop + initNoInterp, SndDoneProc);
            }
            if(err == noErr) {
                err = SndPlay (gChan[chanNum].channel, snd, true);
                if (err == noErr) {
                    gChan[chanNum].sndHandle = snd;
                    gChan[chanNum].priority = priority;
                    cmd.cmd = callBackCmd;
                    cmd.param2 = (long)&gChan[chanNum];
                    err = SndDoCommand (gChan[chanNum].channel, &cmd, false);
                }
            }
        }
    }
    
    return err;
}
 
void SoundKeeper(void)
/*
    This routine must be called from your idle loop.
    It updates the sound channels, getting rid of them and setting the flags correctly.
*/
{
    short   i;
    
    for(i = 0; i < kNumChan; i++) {
        if(gChan[i].priority == -1) {
            gChan[i].priority = 0;
            if(gChan[i].sndHandle)
                HUnlock(gChan[i].sndHandle);    /* do you really want to have a big old unlocked block?*/
 
            if(gSoundMgrVersion < 0x03) {
                SndDisposeChannel (gChan[i].channel, true);
            }
            
        }
    }
}
 
pascal void SndDoneProc(SndChannelPtr channel, SndCommand *cmd)
/*
    This is the magical callback routine that flags SoundKeeper to dump this channel.
    It is magic because it does not use register a5, like most people suggest.
    I think it is lame to use register a5 to access a global from routines like this.
    It is much better to just store a pointer to the data you need!
*/
{
    SndChanInfo *sndChan;
 
    sndChan = (SndChanInfo*)cmd->param2;
    sndChan->priority = -1;
}
 
void FreeSounds(void)
/*
    Disposes of all the currently allocated sound channels
    and stops all sounds from playing.
    Also closes the sound file
*/
{
    short   i;
    
    for(i = 0; i < kNumChan; i++) {
        SndDisposeChannel (gChan[i].channel, true);
        gChan[i].priority = 0;
    }
    
    if(gSndResFile != -1) {
        CloseResFile(gSndResFile);
    }
}