
//============================================================================
//----------------------------------------------------------------------------
//									Sound.c
//----------------------------------------------------------------------------
//============================================================================

// This file handles all sound routines.  It handles 2 concurrent soundÉ
// channels allowing 2 sounds to be played simultaneously.  It also handlesÉ
// a system of priorites whereby you can ensure that "important" sounds don'tÉ
// get cut off by "lesser" sounds.  In that there are 2 channels however,É
// "lesser" sounds are not discounted outright - both channels are consideredÉ
// to determine if one of the channels is not playing at all (priority = 0) orÉ
// playing a sound of an even lesser priority.  Make sense?


#include <Sound.h>
#include "Externs.h"


#define kMaxSounds				17			// Number of sounds to load.
#define	kBaseBufferSoundID		1000		// ID of first sound (assumed sequential).
#define kSoundDone				913			// Just a number I chose.
#define kSoundDone2				749			// Just a number I chose.


void PlaySound1 (short, short);
void PlaySound2 (short, short);
pascal void ExternalCallBack (SndChannelPtr, SndCommand *);
pascal void ExternalCallBack2 (SndChannelPtr, SndCommand *);
void LoadAllSounds (void);
OSErr LoadBufferSounds (void);
OSErr DumpBufferSounds (void);
OSErr OpenSoundChannel (void);
OSErr CloseSoundChannel (void);


SndCallBackUPP		externalCallBackUPP, externalCallBackUPP2;
SndChannelPtr		externalChannel, externalChannel2;
Ptr					theSoundData[kMaxSounds];
short				externalPriority, externalPriority2;
Boolean				channelOpen, soundOn;


//==============================================================  Functions
//--------------------------------------------------------------  PlaySound1
// This function takes a sound ID and a priority, and forces that sound to 
// play through channel 1 - and saves the priority globally.  As well, a 
// callback command is queues up in channel 1.

void PlaySound1 (short soundID, short priority)
{
	SndCommand	theCommand;
	OSErr		theErr;
	
	theCommand.cmd = flushCmd;			// Send 1st a flushCmd to clear the sound queue.
	theCommand.param1 = 0;
	theCommand.param2 = 0L;
	theErr = SndDoImmediate(externalChannel, &theCommand);
	
	theCommand.cmd = quietCmd;			// Send quietCmd to stop any current sound.
	theCommand.param1 = 0;
	theCommand.param2 = 0L;
	theErr = SndDoImmediate(externalChannel, &theCommand);
	
	externalPriority = priority;		// Copy priority to global variable.
	
	theCommand.cmd = bufferCmd;			// Then, send a bufferCmd to channel 1.
	theCommand.param1 = 0;				// The sound played will be soundID.
	theCommand.param2 = (long)(theSoundData[soundID]);
	theErr = SndDoImmediate(externalChannel, &theCommand);
	
	theCommand.cmd = callBackCmd;		// Lastly, queue up a callBackCmd to notify usÉ
	theCommand.param1 = kSoundDone;		// when the sound has finished playing.
	theCommand.param2 = SetCurrentA5();
	theErr = SndDoCommand(externalChannel, &theCommand, TRUE);
}

//--------------------------------------------------------------  PlaySound2
// This function is identical to the above function except that it handlesÉ
// playing sounds through channel 2.

void PlaySound2 (short soundID, short priority)
{
	SndCommand	theCommand;
	OSErr		theErr;
	
	theCommand.cmd = flushCmd;			// Send 1st a flushCmd to clear the sound queue.
	theCommand.param1 = 0;
	theCommand.param2 = 0L;
	theErr = SndDoImmediate(externalChannel2, &theCommand);
	
	theCommand.cmd = quietCmd;			// Send quietCmd to stop any current sound.
	theCommand.param1 = 0;
	theCommand.param2 = 0L;
	theErr = SndDoImmediate(externalChannel2, &theCommand);
	
	externalPriority2 = priority;		// Copy priority to global variable.
	
	theCommand.cmd = bufferCmd;			// Then, send a bufferCmd to channel 1.
	theCommand.param1 = 0;				// The sound played will be soundID.
	theCommand.param2 = (long)(theSoundData[soundID]);
	theErr = SndDoImmediate(externalChannel2, &theCommand);
	
	theCommand.cmd = callBackCmd;		// Lastly, queue up a callBackCmd to notify usÉ
	theCommand.param1 = kSoundDone2;	// when the sound has finished playing.
	theCommand.param2 = SetCurrentA5();
	theErr = SndDoCommand(externalChannel2, &theCommand, TRUE);
}

//--------------------------------------------------------  PlayExternalSound
// This function is probably poorly named for this application.  I lifted thisÉ
// whole library from one of my games and chopped it down for purposes of Glypha.
// The original game treated "external" and "cockpit" sounds as seperate channelsÉ
// (such that cockpit sounds could only "override" other cockpit sounds andÉ
// external sounds could only override other external sounds.
// In any event, this is the primary function called from throughout Glypha.
// This function is called with a sound ID and a priority (just some number) andÉ
// the function then determines if one of the two sound channels is free to playÉ
// the sound.  It determines this by way of priorities.  If a sound channel isÉ
// idle and playing no sound, its channel priority is 0.  Since the priority ofÉ
// the sound you want to play is assumed to be greater than 0, it will, withoutÉ
// a doubt, be allowed to play on an idle channel.  If however there is alreadyÉ
// a sound playing (the channel's priority is not equal to 0), the sound with theÉ
// largest priority wins.  Mind you though that there are two channels to chooseÉ
// between.  Therefore, the function compares the priority passed in with theÉ
// sound channel with the lowest priority.

void PlayExternalSound (short soundID, short priority)
{							// A little error-checking.
	if ((soundID >= 0) && (soundID < kMaxSounds))
	{
		if (soundOn)		// More error-checking.
		{					// Find channel with lowest priority.
			if (externalPriority < externalPriority2)
			{				// Compare priority with that of channel 1.
				if (priority >= externalPriority)
					PlaySound1(soundID, priority);
			}
			else
			{				// Compare priority with that of channel 2.
				if (priority >= externalPriority2)
					PlaySound2(soundID, priority);
			}
		}
	}
}

//--------------------------------------------------------  ExternalCallBack
// Callback routine.  If this looks ugly, blame Apple's Universal Headers.
// The callback routine is called after a sound finishes playing.  TheÉ
// callback routine is extremely useful in that it enables us to know whenÉ
// to set the sound channels priority back to 0 (meaning no sound playing).
// Keep in mind (by the way) that this funciton is called at interrupt timeÉ
// and thus may not cause memory to be moved.  Also, note that also becauseÉ
// of the interupt situation, we need to handle setting A5 to point to ourÉ
// app's A5 and then set it back again.

RoutineDescriptor ExternalCallBackRD = 
		BUILD_ROUTINE_DESCRIPTOR(uppSndCallBackProcInfo, ExternalCallBack);

pascal void ExternalCallBack (SndChannelPtr theChannel, SndCommand *theCommand)
{
	long		thisA5, gameA5;
	
	if (theCommand->param1 == kSoundDone)	// See if it's OUR callback.
	{
		gameA5 = theCommand->param2;			// Extract our A5 from sound command.
		thisA5 = SetA5(gameA5);				// Point A5 to our app (save off current A5).
		
		externalPriority = 0;				// Set global to reflect no sound playing.
		
		thisA5 = SetA5(thisA5);				// Restire A5.
	}
}

//--------------------------------------------------------  ExternalCallBack2
// This function is identical to the above function but handles sound channel 2.

RoutineDescriptor ExternalCallBackRD2 = 
		BUILD_ROUTINE_DESCRIPTOR(uppSndCallBackProcInfo, ExternalCallBack2);

pascal void ExternalCallBack2 (SndChannelPtr theChannel, SndCommand *theCommand)
{
	long		thisA5, gameA5;
	
	if (theCommand->param1 == kSoundDone2)	// See if it's OUR callback.
	{
		gameA5 = theCommand->param2;			// Extract our A5 from sound command.
		thisA5 = SetA5(gameA5);				// Point A5 to our app (save off current A5).
		
		externalPriority2 = 0;				// Set global to reflect no sound playing.
		
		thisA5 = SetA5(thisA5);				// Restire A5.
	}
}

//--------------------------------------------------------  LoadBufferSounds
// This function loads up all the sounds we'll need in the game and thenÉ
// strips off their header so that we can pass them as buffer commands.
// Sounds are stored in our resource fork as 'snd ' resources.  There is aÉ
// 20 byte header that we need to remove in order to use bufferCmd's.
// This function is called only once, when the game loads up.

OSErr LoadBufferSounds (void)
{
	Handle		theSound;
	long		soundDataSize;
	OSErr		theErr;
	short		i;
	
	theErr = noErr;						// Assume no errors.
	
	for (i = 0; i < kMaxSounds; i++)	// Walk through all sounds.
	{									// Load 'snd ' from resource.
		theSound = GetResource('snd ', i + kBaseBufferSoundID);
		if (theSound == 0L)				// Make sure it loaded okay.
			return (ResError());		// Return reason it failed (if it did).
		
		HLock(theSound);				// If we got this far, lock sound down.
										// Calculate size of sound minus header.
		soundDataSize = GetHandleSize(theSound) - 20L;
		HUnlock(theSound);				// Okay, unlock.
										// Create pointer the size calculated above.
		theSoundData[i] = NewPtr(soundDataSize);
		if (theSoundData[i] == 0L)		// See if we created it okay.
			return (MemError());		// If failed, return the reason why.
		HLock(theSound);				// Okay, lock the sound handle again.
										// Copy sound data (minus header) to our pointer.
		BlockMove((Ptr)(*theSound + 20L), theSoundData[i], soundDataSize);
		HUnlock(theSound);				// Unlock sound handle again.
		ReleaseResource(theSound);		// And toss it from memory.
	}
	
	return (theErr);
}

//--------------------------------------------------------  DumpBufferSounds
// This function is called when Glypha exits (quits).  All those nasty pointersÉ
// we created in the above function are reclaimed.

OSErr DumpBufferSounds (void)
{
	OSErr		theErr;
	short		i;
	
	theErr = noErr;
	
	for (i = 0; i < kMaxSounds; i++)		// Go through all sound pointers.
	{
		if (theSoundData[i] != 0L)			// Make sure it exists.
			DisposPtr(theSoundData[i]);		// Dispose of it.
		theSoundData[i] = 0L;				// Make sure it reflects its "nonexistence".
	}
	
	return (theErr);
}

//--------------------------------------------------------  OpenSoundChannel
// This should perhaps be called OpenSoundChannels() since it opens two.
// It is called once (at initialization) to set up the two sound channelsÉ
// we will use throughout Glypha.  For purposes of speed, 8-bit sound channelsÉ
// with no interpolation and monophonic are opened.  They'll use the sampledÉ
// synthesizer (digitized sound) and be assigned their respective callbackÉ
// routines.

OSErr OpenSoundChannel (void)
{
	OSErr		theErr;
	
	#if USESROUTINEDESCRIPTORS
		externalCallBackUPP = &ExternalCallBackRD;	// Handle Universal Header ugliness.
		externalCallBackUPP2 = &ExternalCallBackRD2;
	#else
		externalCallBackUPP = (SndCallBackUPP) &ExternalCallBack;
		externalCallBackUPP2 = (SndCallBackUPP) &ExternalCallBack2;
	#endif
	
	theErr = noErr;									// Assume no errors.
	
	if (channelOpen)								// Error checking.
		return (theErr);
	
	externalChannel = 0L;
	theErr = SndNewChannel(&externalChannel, 		// Open channel 1.
			sampledSynth, initNoInterp + initMono, 
			(SndCallBackUPP)externalCallBackUPP);
	if (theErr == noErr)							// See if it worked.
		channelOpen = TRUE;
	
	externalChannel2 = 0L;
	theErr = SndNewChannel(&externalChannel2, 		// Open channel 2.
			sampledSynth, initNoInterp + initMono, 
			(SndCallBackUPP)externalCallBackUPP2);
	if (theErr == noErr)							// See if it worked.
		channelOpen = TRUE;
	
	return (theErr);
}

//--------------------------------------------------------  CloseSoundChannel
// This function is called only upon quitting Glypha.  Both sound channelsÉ
// we created above are closed down.

OSErr CloseSoundChannel (void)
{
	OSErr		theErr;
	
	theErr = noErr;
	
	if (!channelOpen)			// Error checking.
		return (theErr);
	
	if (externalChannel != 0L)	// Dispose of channel 1 (if open).
		theErr = SndDisposeChannel(externalChannel, TRUE);
	externalChannel = 0L;		// Flag it closed.
	
	if (externalChannel2 != 0L)	// Dispose of channel 2 (if open).
		theErr = SndDisposeChannel(externalChannel2, TRUE);
	externalChannel2 = 0L;		// Flag it closed.
	
	if (theErr == noErr)
		channelOpen = FALSE;
	
	return (theErr);
}

//--------------------------------------------------------  InitSound
// All the above initialization routines are handled by this one function.
// This single function is the only one that needs to be called - it handlesÉ
// calling the functions that load the sounds and create the sound channels.
// It is called from main() when Glypha is loading up and going through itsÉ
// initialization phase.

void InitSound (void)
{
	OSErr		theErr;
	
	soundOn = TRUE;			// Note that initialization of sounds has occurredÉ
							// (or rather is just about to this instant!).
	externalChannel = 0L;	// Flag channels as nonexistant.
	externalChannel2 = 0L;
	externalPriority = 0;	// Set priorities to 0 (no sound playing).
	externalPriority2 = 0;
							// Load up all sounds (see above function).
	theErr = LoadBufferSounds();
	if (theErr != noErr)	// If it fails, we'll quit Glypha.
		RedAlert("\pFailed Loading Sounds");
							// Open up the two sound channels.
	theErr = OpenSoundChannel();
	if (theErr != noErr)	// If that fails we'll quit Glypha as well.
		RedAlert("\pFailed To Open Sound Channels");
}

//--------------------------------------------------------  KillSound
// Complementary to the above function, this one is called only when GlyphaÉ
// quits and it handles all the "shut-down" routines.  It also is called fromÉ
// main(), but it is called last - just as Glypha is quitting.

void KillSound (void)
{
	OSErr		theErr;
	
	theErr = DumpBufferSounds();	// Kill all sound pointers.
	theErr = CloseSoundChannel();	// Close down the sound channels.
}

