/****************************/
/*      AUDIO.C				*/
/* (c)2004 Pangea Software  */
/* By Brian Greenstone      */
/****************************/


/****************************/
/*    EXTERNS				*/
/****************************/

extern	FSSpec		gMyResourcesFolderFSSpec;


/****************************/
/*    PROTOTYPES            */
/****************************/

static void *MySongThread(void *in);
pascal void EndOfSongCallback(QTCallBack cb, long refCon);
static	void InitSoundManagerChannels(void);
static pascal void LoopSoundCallback(SndChannelPtr chan, SndCommand *cmd) ;
static short FindSilentChannel(void);


/****************************/
/*    CONSTANTS             */
/****************************/

typedef struct
{
	SndChannelPtr	channel;
	Boolean			isLooping;
}ChannelInfoType;

#define	MAX_CHANNELS	30			// max # sound channels to allocate
#define	MAX_EFFECTS		100			// max # of sound resources to load

/**********************/
/*     VARIABLES      */
/**********************/

static CGrafPtr		gQTDummyPort = nil;
Movie				gSongMovie = nil;
static	Boolean		gSongPlayingFlag = false;
static	Boolean		gLoopSongFlag = false;

static volatile  Boolean gInQuicktimeFunction = false;
static volatile	 gInMoviesTask = false;

static	QTCallBack		gMovieCallback;
static	QTCallBackUPP	gMovieCallbackUPP;	

static	short			gNumChannels;

static	ChannelInfoType				gChannelInfo[MAX_CHANNELS];

static	SndListHandle		gSndHandles[MAX_EFFECTS];
static	long				gSndOffsets[MAX_EFFECTS];


/***************** INIT AUDIO **********************/
//
// Initializes Quicktime, and creates a pthread for updating MoviesTask()
//

void InitAudio(void)
{
	pthread_t 			myThread;
	OSErr				iErr;
	FSSpec				spec;

		/* ACTIVATE QUICKTIME */

	EnterMovies();

	
		/* INIT MOVIES TASK THREAD */
					
	iErr = pthread_create(&myThread, NULL, MySongThread, nil);
	if (iErr != noErr)
		DoFatalAlert("\ppthread_create failed");
		
		
		/**********************/
		/* INIT SOUND MANAGER */
		/**********************/
		
		/* INIT CHANNELS */
		
	InitSoundManagerChannels();
	
	
	/* LOAD OUR SOUND RESOURCES */
		
	iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
						gMyResourcesFolderFSSpec.parID,
						"\pSounds.rsrc",
						&spec);
						
	if (iErr != noErr)
		DoFatalAlert("\pCannot find SoundEffects.rsrc");
	
	LoadSoundResources(&spec);



}

#pragma mark ____ QUICKTIME ______


/********************* MY SONG THREAD ************************/
//
// A code thread dedicated to updateing MoviesTask()
//

static void *MySongThread(void *in)
{
AbsoluteTime	expTime;
#pragma unused (in)

	while(true)						// loop inside here forever
	{
			/* CALCULATE THE TIME OF NEXT LOOP */
			//
			// Calculate current time + 100 milliseconds.
			// ThatŐs when weŐll want to loop thru again.
			//

		expTime = AddDurationToAbsolute(100 * kDurationMillisecond,
										UpTime());
		
		/* UPDATE MOVIES TASK */
		//
		// We must be very careful not to call MoviesTask()
		// while some other Quicktime call is in progress!
		//
		
		if (!gInQuicktimeFunction)
		{		
			if (gSongMovie)			
			{
				gInMoviesTask = true;
				MoviesTask(gSongMovie, 0);
				gInMoviesTask = false;
			}
		}

			/* WAIT UNTIL ITŐS TIME TO LOOP AGAIN */

		MPDelayUntil(&expTime);
	}
	
    return 0;
}





/*********************** PLAY SONG ***********************/
//
// Loads the specified sound file and starts playing it.
//
// INPUT:	songFileName = pascal string that is the audio file's filename.
//			loopFlag = true if we want the song to loop, false if only play once.
//

void PlaySong(Str255 songFileName, Boolean loopFlag)
{
	OSErr 		iErr;
	FSSpec		spec;
	short		myRefNum;
	GrafPtr		oldPort;
	TimeValue 	timeNow, duration;
	Fixed 		playRate;
	Media 		theMedia;
	
			/********************************/
			/* CREATE MOVIE FROM SOUND FILE */
			/********************************/
			
	/* CREATE FSSPEC FOR THE SOUND FILE */
	
	iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
						gMyResourcesFolderFSSpec.parID,
						songFileName,
						&spec);
	if (iErr)
		DoFatalAlert("\pSong file not found");
	
	
	/* GOT TO SET A DUMMY PORT FOR QUICKTIME */
	//
	// Even though weŐre only playing audio, we must set a
	// dummy port for Quicktime to ŇrenderÓ into.
	//
	
	if (gQTDummyPort == nil)					// create a blank graf port
		gQTDummyPort = CreateNewPort();
	
	GetPort(&oldPort);						// backup current port	 
	SetPort(gQTDummyPort);					// set dummy as active port
	
	
	/* OPEN THE SOUND FILE AS A QT MOVIE */
	
	iErr = OpenMovieFile(&spec, &myRefNum, fsRdPerm);
	if ((myRefNum == 0) || (iErr != noErr))
		DoFatalAlert("\pError opening movie file");
	
	
	/* CREATE A NEW QT MOVIE FROM THE OPEN FILE */
	
	NewMovieFromFile(&gSongMovie,
					 myRefNum,
					 0,
					 nil,
					 newMovieActive,
					 nil);
	
	CloseMovieFile(myRefNum);
	
			/******************/	
			/* SET MOVIE INFO */
			/******************/
	
	/* SET MOVIE PLAY HINTS */			
	
	SetMoviePlayHints(gSongMovie,
					  0, 						// turn these hints off	
					  hintsUseSoundInterp|hintsHighQuality);	
	
	if (loopFlag)								// turn loop hint on
		SetMoviePlayHints(gSongMovie, hintsLoop, hintsLoop);	
		
														
	/* PRE-LOAD MOVIE INTO RAM */
	
	timeNow = GetMovieTime(gSongMovie, nil);
	duration = GetMovieDuration(gSongMovie);
	playRate = GetMoviePreferredRate(gSongMovie);
	theMedia = GetTrackMedia(GetMovieIndTrack(gSongMovie, 1));		
	
	LoadMovieIntoRam(gSongMovie, timeNow, duration, keepInRam);
	LoadMediaIntoRam(theMedia, timeNow, duration, keepInRam);
	
	PrePrerollMovie(gSongMovie, timeNow, playRate, nil, nil);
	PrerollMovie(gSongMovie, timeNow, playRate); 
	
	
			/* SET LOOPING CALLBACK */
			
	if (loopFlag)
	{
		TimeBase timebase = GetMovieTimeBase(gSongMovie);	// get timebase of movie
		gMovieCallback = NewCallBack(timebase,				// create a new callback ref
							callBackAtExtremes);

		gMovieCallbackUPP = NewQTCallBackUPP(EndOfSongCallback);	// create callback UPP
			
		iErr = CallMeWhen(gMovieCallback,				// callback ref
					gMovieCallbackUPP,					// function to callback
					0,
					triggerAtStop,						// call when movie stops
					0,0);
					
		if (iErr != noErr)
			DoFatalAlert("\pcould not create callback");
	}
	
	
				/***************************/
				/* START THE MUSIC PLAYING */
				/***************************/
	
	StartMovie(gSongMovie);
	
	gSongPlayingFlag = true;
	gLoopSongFlag = loopFlag;								// remember if we want it to loop
	
	
				/* RESTORE THE PORT */
	
	SetPort(oldPort);
	
}

/***************** END OF SONG CALLBACK *********************/
//
// This gets called by MoviesTask() when the movie has ended.
// Here we rewind the song and get it playing again to make it loop.
//
// NOTE:  Since this is called from MoviesTask() and we only call
// MoviesTask() inside our thread function, we don't need to use
// our safety flags gInQuicktimeFunction or gInMoviesTask because
// these were already dealt with when MoviesTask() was called.
// Also, be careful what we call inside this function since we're
// in a thread.  Not all OS calls are thread-safe!
//

pascal void EndOfSongCallback(QTCallBack callback, long refCon)
{
#pragma unused (callback, refCon)

	GoToBeginningOfMovie(gSongMovie);	// rewind song

	CallMeWhen(gMovieCallback,			// reinstate the callback
				gMovieCallbackUPP,
				0,
				triggerAtStop,
				0,0);
					
	StartMovie(gSongMovie);				// start playing again	
}



/*********************** FREE OUR SONG MOVIE *********************/
//
// Stops a song from playing and frees up the Quicktime movie.
//

void FreeOurSongMovie(void)
{
			/* BE SAFE */
			
	gInQuicktimeFunction = true;	// dont allow song task to start anything new
	while(gInMoviesTask);			// DONT DO ANYTHING WHILE IN THE SONG TASK!!

		
		/* DISPOSE THE LOOP CALLBACK */
		
	if (gLoopSongFlag)
	{
		DisposeCallBack(gMovieCallback);
		DisposeQTCallBackUPP(gMovieCallbackUPP);	
	}

		
		/* STOP AND DISPOSE MOVIE */
				
	gSongPlayingFlag = false;		// clear this

	StopMovie(gSongMovie);			// stop the movie
	DisposeMovie(gSongMovie);		// dispose of the movie
		
	gSongMovie = nil;
	
	gInQuicktimeFunction = false;
}


#pragma mark ____ SOUND MANAGER _____


/************** INIT SOUND MANAGER CHANNELS ****************/

static void InitSoundManagerChannels(void)
{
	OSErr			iErr;
	short			i;
	ExtSoundHeader	sndHdr;
	const double	rate = rate44khz;
	SndCommand		mySndCmd;
	SndChannelPtr	channel;
	

				/***************************/
				/* MAKE DUMMY SOUND HEADER */
				/***************************/
				//
				// This mimics the header of a sound file with no data.
				// it is needed below to issue sound commands to optimize
				// the channels.
				//

	sndHdr.samplePtr 			= nil;
    sndHdr.sampleRate			= rate44khz;  
    sndHdr.loopStart			= 0;         
    sndHdr.loopEnd				= 0;          
    sndHdr.encode				= extSH;           
    sndHdr.baseFrequency 		= 0;      
    sndHdr.numFrames			= 0;              
    sndHdr.numChannels			= 2;  
   	dtox80(&rate, &sndHdr.AIFFSampleRate);        
    sndHdr.markerChunk			= 0;          
    sndHdr.instrumentChunks		= 0;       
    sndHdr.AESRecording			= 0;
    sndHdr.sampleSize			= 16;           
    sndHdr.futureUse1			= 0;            
    sndHdr.futureUse2			= 0;            
    sndHdr.futureUse3			= 0;    
    sndHdr.futureUse4			= 0;  
    sndHdr.sampleArea[0]		= 0;  


			/*********************/
			/* ALLOCATE CHANNELS */
			/*********************/

	for (i = 0; i < MAX_CHANNELS; i++)
	{
			/* NEW SOUND CHANNEL */
				
		iErr = SndNewChannel(&channel,
							sampledSynth,
							initStereo + initNoInterp,
							NewSndCallBackUPP(LoopSoundCallback));
		if (iErr)						// if err, stop allocating channels
			break;
					
		
			/* ISSUE SOME COMMANDS TO OPTIMIZE THE CHANNEL */
			//
			// The initNoInterp command used above is ignored by
			// the Sound Manager.  To actually cause initNoInterp
			// to work, we must do it this way:
			//
	
		mySndCmd.cmd 		= soundCmd;		// install the bogus sound
		mySndCmd.param1 	= 0;
		mySndCmd.param2 	= (long)&sndHdr;
		SndDoImmediate(channel, &mySndCmd);
		
		mySndCmd.cmd 		= reInitCmd;	// re-init	the channel
		mySndCmd.param1 	= 0;
		mySndCmd.param2 	= initNoInterp | initStereo;
		SndDoImmediate(channel, &mySndCmd);


			/* INIT SOME OF OUR CHANNEL INFO */

		gChannelInfo[i].channel = channel;	// save channel into list
		gChannelInfo[i].isLooping = false;	// clear looping flag

	}	

			/* REMEMBER HOW MANY CHANNELS WE ALLOCATED SUCCESSFULLY */

	gNumChannels = i;		
}


/****************** LOOP SOUND CALLBACK *****************/
//
// Gets called when a sound channel reaches the end of a sound sample.
// Here we keep the sound effect looping by causing it to replay.
//

static pascal void LoopSoundCallback(SndChannelPtr chan, SndCommand *cmd) 
{
	SndCommand      theCmd;

    // Play the sound again

    theCmd.cmd 	= bufferCmd;
    theCmd.param1 = 0;
    theCmd.param2 = cmd->param2;
	SndDoCommand (chan, &theCmd, true);

    // Just reuse the callBackCmd that got us here in the first place

    SndDoCommand (chan, cmd, true);
}


/************* LOAD SOUND RESOURCES **************/

void LoadSoundResources(FSSpec *spec)
{
short			refnum, numSoundsInFile,i;

			/* OPEN THE RESOURCE FILE */
			
	refnum = FSpOpenResFile(spec, fsRdPerm);
	if (refnum == -1)
		DoFatalAlert("\pSound resource file not found!");

	UseResFile(refnum);

			/* COUNT # OF SND RESOURCES IN THE FILE */

	numSoundsInFile = Count1Resources('snd ');
	if (numSoundsInFile > MAX_EFFECTS)
		DoFatalAlert("\pToo many sounds!");

			/****************************/
			/* LOAD EACH SOUND RESOURCE */
			/****************************/

	for (i=0; i < numSoundsInFile; i++)
	{
				/* GET THE SOUND */

		gSndHandles[i] = (SndListResource **)GetResource('snd ', 10000 + i);

		if (gSndHandles[i] == nil) 
			DoFatalAlert("\pGetResource failed!");

			/* CONVERT TO A REGULAR HANDLE & LOCK IT */

		DetachResource((Handle)gSndHandles[i]);				
		HNoPurge((Handle)gSndHandles[i]);	
		HLockHi((Handle)gSndHandles[i]);
		
				/* GET OFFSET TO SOUND HEADER */
				
		GetSoundHeaderOffset(gSndHandles[i], &gSndOffsets[i]);		
	}

	CloseResFile(refnum);
}

/****************** PLAY EFFECT ******************/
//
// Plays a sound resource with the input parameters.
//
// INPUT:	sndNum = sound effect # from our list
//			leftVolume / rightVolume = left/right stereo volume balance 
//			rateMultiplier = freqeuncy to play back.
//
// OUTPUT:	channel # that effect is playing on
//

short  PlayEffect(short sndNum, u_long leftVolume, u_long rightVolume, u_long rateMultiplier)
{
	SndCommand 		mySndCmd;
	SndChannelPtr	chanPtr;
	short			chanNum;
	u_long			loopStart, loopEnd;
	SoundHeaderPtr   sndPtr;

			/* GET POINTER TO THE SOUND HEADER */
			
	sndPtr = (SoundHeaderPtr)(((long)*gSndHandles[sndNum]) + gSndOffsets[sndNum]);


			/* GET A FREE CHANNEL */
			
	chanNum = FindSilentChannel();
	if (chanNum == -1)					// no free channels
		return(-1);


			/*********************************/
			/* ISSUE COMMANDS TO THE CHANNEL */
			/*********************************/

	chanPtr = gChannelInfo[chanNum].channel;
	
		/* FLUSH ANY OLD COMMANDS OUT OF THE CHANNEL */

	mySndCmd.cmd = flushCmd;	
	mySndCmd.param1 = 0;
	mySndCmd.param2 = 0;
	SndDoImmediate(chanPtr, &mySndCmd);

	/* MAKE SURE NO SOUND IS PLAYING ON THIS CHANNEL */
 
	mySndCmd.cmd = quietCmd;
	mySndCmd.param1 = 0;
	mySndCmd.param2 = 0;
	SndDoImmediate(chanPtr, &mySndCmd);
	
			/* SET LEFT & RIGHT VOLUME FOR CHANNEL */
			//
			// param2 = 32-bit word containing the right
			//		volume in the upper 16 bits, and the
			//		left volume in the lower 16 bits.
			//

	mySndCmd.cmd = volumeCmd;			
	mySndCmd.param1 = 0;
	mySndCmd.param2 = (rightVolume  << 16) | leftVolume;
	SndDoCommand(chanPtr, &mySndCmd, true);

			/* START PLAYING THE CHANNEL */
			//
			// param2 points to the soundŐs header
			//

	mySndCmd.cmd = bufferCmd;	
	mySndCmd.param1 = 0;
	mySndCmd.param2 = (int)sndPtr;
    SndDoCommand(chanPtr, &mySndCmd, true);

			/* SET THE PLAYBACK FREQUENCY */
			//
			// This has to be done after bufferCmd above
			//

	mySndCmd.cmd 		= rateMultiplierCmd;							
	mySndCmd.param1 	= 0;
	mySndCmd.param2 	= rateMultiplier;	
	SndDoImmediate(chanPtr, &mySndCmd);

    
		/***********************************/
    	/* SEE IF THIS IS A LOOPING EFFECT */    		
		/***********************************/
		//
		// We look in the soundŐs header to see if it loops.
		// 
		
    
	loopStart = sndPtr->loopStart;
    loopEnd = sndPtr->loopEnd;
    if ((loopStart + 1) < loopEnd)
    {
			/* SET THE CALLBACK COMMAND */

    	mySndCmd.cmd 		= callBackCmd;			
    	mySndCmd.param1 	= 0;
    	mySndCmd.param2 	= (int)sndPtr;
		SndDoCommand(chanPtr, &mySndCmd, true);
    	
    	gChannelInfo[chanNum].isLooping = true;
	}
	else
		gChannelInfo[chanNum].isLooping = false;
		

			/* SET MY INFO */
			
	return(chanNum);							// return channel #	
}


/***************** FIND SILENT CHANNEL ******************/
//
// Returns the channel # of a channel that isn't playing sound
//

static short FindSilentChannel(void)
{
	short		c;
	OSErr		iErr;
	SCStatus	theStatus;

	for (c = 0; c < gNumChannels; c++)
	{
			/* LOOPING CHANNELS ARE OBVIOUSLY NOT AVAILABLE */

		if (gChannelInfo[c].isLooping)				
			continue;
			
				/* GET CHANNEL STATUS */

		iErr = SndChannelStatus(gChannelInfo[c].channel, sizeof(SCStatus), &theStatus);	
		if (iErr != noErr)
			continue;

				/* SEE IF CHANNEL IS BUSY */

		if (theStatus.scChannelBusy)					
			continue;
	
		return(c);
	}

	
			/* NO FREE CHANNELS */
	
	return(-1);										
}

/******************* CHANGE CHANNEL VOLUME ******************/
//
// Changes the volume of a playing channel
//

void ChangeChannelVolume(short channel, u_long leftVol, u_long rightVol)
{
	SndCommand 	mySndCmd;
	SndChannelPtr	chanPtr;

	chanPtr = gChannelInfo[channel].channel;	// get the channel ptr				

			/* SEND A VOLUME COMMAND TO THE CHANNEL */

	mySndCmd.cmd = volumeCmd;		
	mySndCmd.param1 = 0;
	mySndCmd.param2 = (rightVol << 16) | leftVol;	
	SndDoImmediate(chanPtr, &mySndCmd);
}

/****************** CHANGE CHANNEL RATE ******************/

void ChangeChannelRate(short channel, long rateMult)
{
	SndCommand 		mySndCmd;
	SndChannelPtr		chanPtr;

	chanPtr = gChannelInfo[channel].channel;	// get the channel ptr	

			/* SEND A RATE MULTIPLIER COMMAND */

	mySndCmd.cmd 		= rateMultiplierCmd;
	mySndCmd.param1 	= 0;
	mySndCmd.param2 	= rateMult;	
	SndDoImmediate(chanPtr, &mySndCmd);
}







