/* SoundOutput.c */
/*****************************************************************************/
/*                                                                           */
/*    System Dependency Library for Building Portable Software               */
/*    Macintosh Version                                                      */
/*    Written by Thomas R. Lawrence, 1993 - 1994.                            */
/*                                                                           */
/*    This file is Public Domain; it may be used for any purpose whatsoever  */
/*    without restriction.                                                   */
/*                                                                           */
/*    This package is distributed in the hope that it will be useful,        */
/*    but WITHOUT ANY WARRANTY; without even the implied warranty of         */
/*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                   */
/*                                                                           */
/*    Thomas R. Lawrence can be reached at tomlaw@world.std.com.             */
/*                                                                           */
/*****************************************************************************/

#include "MiscInfo.h"
#include "Debug.h"
#include "Audit.h"
#include "Definitions.h"

#ifdef THINK_C
	#pragma options(pack_enums)
#endif
#include <Sound.h>
#include <Errors.h>
#ifdef THINK_C
	#pragma options(!pack_enums)
#endif

#include "SoundOutput.h"
#include "Memory.h"


#define MinimumInitialNumberOfBuffers (3)

typedef struct MyStructure
	{
		struct MyStructure*		Next;
		struct MyStructure*		Previous;
		SndCommand						MySoundCommand;
		SndCommand						MyCallbackCommand;
		volatile MyBoolean		InUseFlag; /* interrupt level flag; hence "volatile" */
		ExtSoundHeader				Header;
		char*									SampleArea;
	} MyStructure;


static MyBoolean			SoundSystemInUse = False;

static MyStructure*		NextAvailableBuffer;
static MyBoolean			IsCurrentBufferCheckedOut;

static long						MaxFramesPerBuffer;
static int						BytesPerFrame;
static int						MaxBuffersToAllocate;
static int						CurrentBufferCount;
static long						TheSamplingRate;
static MyBoolean			StereoFlag;
static MyBoolean			SixteenBitFlag;

static SndChannel*		MySoundChannel;


static void							AllocateANewBuffer(void);
static void							DisposeBuffers(void);
static pascal void			MyCallBack(SndChannel* Channel, SndCommand* Command);


/* attempt to obtain a sound channel.  returns True if the sound channel was opened */
/* or False if it couldn't be (if already in use or machine doesn't support sound) */
MyBoolean		OpenSoundChannel(long SamplingRate, SoundOutputStereo WantStereo,
							SoundOutputNumBits NumBits, long FramesPerBuffer, int MaxNumBuffers,
							int InitialNumBuffers)
	{
		OSErr							Error;
		SndCommand				Cmd;
		long							InitOptions;

		/* open the sound channel */
		if (SoundSystemInUse)
			{
				return False;
			}
		if ((SamplingRate < MINIMUMSAMPLINGRATE) || (SamplingRate > MAXIMUMSAMPLINGRATE))
			{
				return False;
			}
		InitOptions = initNoInterp /* | initNoDrop */;
		if (WantStereo == eStereo)
			{
				InitOptions |= initStereo;
			}
		MySoundChannel = NIL;
		Error = SndNewChannel(&MySoundChannel,sampledSynth,InitOptions,
			NewSndCallBackProc(&MyCallBack));
		if (Error == noErr)
			{
				/* we want maximum volume on the sound channel */
				Cmd.cmd = ampCmd;
				Cmd.param1 = 255;
				Cmd.param2 = 0;
				Error = SndDoImmediate(MySoundChannel,&Cmd);
				if (Error == noErr)
					{
						/* initialize the variables */
						TheSamplingRate = SamplingRate;
						StereoFlag = (WantStereo == eStereo);
						SixteenBitFlag = (NumBits == e16bit);
						NextAvailableBuffer = NIL;
						MaxFramesPerBuffer = FramesPerBuffer;
						BytesPerFrame = 1;
						if (StereoFlag)
							{
								BytesPerFrame *= 2; /* double the number of words per sample frame */
							}
						if (SixteenBitFlag)
							{
								BytesPerFrame *= (sizeof(short) / sizeof(char)); /* words, not bytes */
							}
						MaxBuffersToAllocate = MaxNumBuffers;
						CurrentBufferCount = 0;
						IsCurrentBufferCheckedOut = False;
						/* allocate the buffers that were requested to begin with */
						if (InitialNumBuffers < MinimumInitialNumberOfBuffers)
							{
								InitialNumBuffers = MinimumInitialNumberOfBuffers;
							}
						while (InitialNumBuffers > 0)
							{
								AllocateANewBuffer();
								InitialNumBuffers -= 1;
							}
						/* notice that we escape here */
						if (CurrentBufferCount > 1)
							{
								/* we need at least 2 buffers, otherwise the sound will skip so */
								/* there'd be no point in doing it with 1 buffer.  Other systems */
								/* may not have this restriction (e.g. UNIX) */
								SoundSystemInUse = True;
								return True;
							}
						DisposeBuffers();
					}
				SndDisposeChannel(MySoundChannel,True);
			}
		return False;
	}


/* internal routine to add a buffer to the existing ring of buffers. */
static void				AllocateANewBuffer(void)
	{
		MyStructure*		Buffer;
		unsigned long		Mantissa;
		unsigned long		Exponent;

		Buffer = (MyStructure*)AllocPtrCanFail(sizeof(MyStructure),"SoundBufHeader");
		if (Buffer != NIL)
			{
				Buffer->SampleArea = AllocPtrCanFail(MaxFramesPerBuffer
					* BytesPerFrame,"SndBuffer");
				if (Buffer->SampleArea == NIL)
					{
						ReleasePtr((char*)Buffer);
						return;
					}
				Buffer->InUseFlag = False;
				Buffer->Header.samplePtr = Buffer->SampleArea;
				if (SixteenBitFlag)
					{
						Buffer->Header.sampleSize = 16;
					}
				 else
					{
						Buffer->Header.sampleSize = 8;
					}
				if (StereoFlag)
					{
						Buffer->Header.numChannels = 2;
					}
				 else
					{
						Buffer->Header.numChannels = 1;
					}
				Buffer->Header.sampleRate = (unsigned long)TheSamplingRate << 16;
				Buffer->Header.loopStart = 0;
				Buffer->Header.loopEnd = 0;
				Buffer->Header.encode = extSH;
				Buffer->Header.baseFrequency = 64;
				Buffer->Header.markerChunk = NULL;
				Buffer->Header.futureUse1 = 0;
				Buffer->Header.futureUse2 = 0;
				Buffer->Header.futureUse3 = 0;
				Buffer->Header.futureUse4 = 0;
				/* extended 22050 = 400D AC44000000000000 */
				/* extended 22051 = 400D AC46000000000000 */
				/* extended 44100 = 400E AC44000000000000 */
				/* extended 44101 = 400E AC45000000000000 */
				Exponent = 0x401e;
				Mantissa = TheSamplingRate;
				while ((Mantissa & 0x80000000) == 0)
					{
						Mantissa = Mantissa << 1;
						Exponent -= 1;
					}
				((char*)&(Buffer->Header.AIFFSampleRate))[0] = (Exponent >> 8) & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[1] = Exponent & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[2] = (Mantissa >> 24) & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[3] = (Mantissa >> 16) & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[4] = (Mantissa >> 8) & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[5] = Mantissa & 0xff;
				((char*)&(Buffer->Header.AIFFSampleRate))[6] = 0;
				((char*)&(Buffer->Header.AIFFSampleRate))[7] = 0;
				((char*)&(Buffer->Header.AIFFSampleRate))[8] = 0;
				((char*)&(Buffer->Header.AIFFSampleRate))[9] = 0;
				/* now, link the buffer */
				if (NextAvailableBuffer != NIL)
					{
						/* link this block in */
						Buffer->Next = NextAvailableBuffer;
						Buffer->Previous = NextAvailableBuffer->Previous;
						/* link enclosing buffers to it */
						NextAvailableBuffer->Previous->Next = Buffer;
						NextAvailableBuffer->Previous = Buffer;
						/* stick it where we can use it */
						NextAvailableBuffer = Buffer;
					}
				 else
					{
						Buffer->Next = Buffer;
						Buffer->Previous = Buffer;
						NextAvailableBuffer = Buffer;
					}
				CurrentBufferCount += 1;
			}
	}


/* internal routine to dispose of all of the buffers */
/* don't call this unless they're InUseFlag is clear!!! */
static void				DisposeBuffers(void)
	{
		while (NextAvailableBuffer != NIL)
			{
				MyStructure*		Temp;

				Temp = NextAvailableBuffer;
				if (NextAvailableBuffer->Next == NextAvailableBuffer)
					{
						/* degenerate */
						NextAvailableBuffer = NIL;
					}
				 else
					{
						NextAvailableBuffer->Previous->Next = NextAvailableBuffer->Next;
						NextAvailableBuffer->Next->Previous = NextAvailableBuffer->Previous;
						NextAvailableBuffer = NextAvailableBuffer->Next;
					}
				ReleasePtr(Temp->SampleArea);
				ReleasePtr((char*)Temp);
			}
	}


#ifdef THINK_C
	#if __option(profile)
		#define Profiling (True)
	#else
		#define Profiling (False)
	#endif

	#pragma options(!profile)
#endif

/* asynchronous callback routine which marks buffers as now unused */
/* It would be very bad to profile this since profiling tampers with the */
/* stack invocation and uses A5 global variables!  (believe me, I've tried) */
static pascal void    MyCallBack(SndChannel* Channel, SndCommand* Command)
  {
    *(MyBoolean*)(Command->param2) = 0;
  }

#ifdef THINK_C
	#if Profiling
		#pragma options(profile)
	#endif
#endif


/* close the sound channel and clean up the buffers */
/* waits until all buffers have played out */
void				CloseSoundChannel(void (*Callback)(void* Refcon), void* Refcon)
	{
		MyStructure*		Scan;

		ERROR(!SoundSystemInUse,PRERR(ForceAbort,
			"CloseSoundChannel called, but sound channel isn't open"));
		SoundSystemInUse = False;
	 LoopPoint:
		Scan = NextAvailableBuffer;
		do
			{
				if (Scan->InUseFlag)
					{
						if (Callback != NIL)
							{
								(*Callback)(Refcon);
							}
						goto LoopPoint;
					}
				Scan = Scan->Next;
			} while (Scan != NextAvailableBuffer /* "Full Circle" */);
		/* dispose of the blasted thing */
		SndDisposeChannel(MySoundChannel,True/*shutupnow*/);
		/* release the memory */
		DisposeBuffers();
	}


/* obtain a pointer to one of the [nonrelocatable] sound buffers.  Data in this */
/* buffer is interpreted as such:  For 8-bit mono, the buffer is an array of */
/* signed chars.  For 16bit mono, the buffer is an array of 2-byte signed integers in */
/* the machine's native endianness.  For 8-bit stereo, the buffer is an array of */
/* 2-byte tuples; the byte lower in memory is the left channel.  For 16-bit stereo, */
/* the buffer is an array of 2-(2-byte) tuples, the left channel is lower in memory. */
/* If there are no buffers currently available (and new ones couldn't be allocated) */
/* then it returns NIL */
char*				CheckOutSoundBuffer(void)
	{
		ERROR(!SoundSystemInUse,PRERR(ForceAbort,
			"CheckOutSoundBuffer called, but sound channel isn't open"));
		ERROR(IsCurrentBufferCheckedOut,PRERR(ForceAbort,
			"CheckOutSoundBuffer called while a buffer has already been checked out"));
		if (NextAvailableBuffer->InUseFlag)
			{
				/* oops, buffer is in use, try to make another */
				if (CurrentBufferCount < MaxBuffersToAllocate)
					{
						AllocateANewBuffer();
					}
				if (NextAvailableBuffer->InUseFlag)
					{
						return NIL;
					}
			}
		IsCurrentBufferCheckedOut = True;
		return NextAvailableBuffer->SampleArea;
	}


/* submit a buffer to be queued to the system's sound channel.  The number of frames */
/* in the buffer actually used is specified to allow less than the full buffer to */
/* be used. */
void				SubmitBuffer(char* Buffer, long NumUsedFrames,
							void (*Callback)(void* Refcon), void* Refcon)
	{
		OSErr			Error;
		long			Scan;

		ERROR(!SoundSystemInUse,PRERR(ForceAbort,
			"SubmitBuffer called, but sound channel isn't open"));
		ERROR(!IsCurrentBufferCheckedOut,PRERR(ForceAbort,
			"SubmitBuffer called but no buffer has been checked out"));
		ERROR(Buffer != NextAvailableBuffer->SampleArea,
			PRERR(ForceAbort,"SubmitBuffer:  Wrong buffer was submitted"));
		ERROR((NumUsedFrames < 0) || (NumUsedFrames > MaxFramesPerBuffer),
			PRERR(ForceAbort,"SubmitBuffer:  Number of used frames exceeds buffer size!"));
		IsCurrentBufferCheckedOut = False;
		ERROR(NextAvailableBuffer->InUseFlag,PRERR(ForceAbort,
			"SubmitBuffer:  Internal error -- InUseFlag is set but shouldn't be"));
		/* adjust the signed samples to be unsigned */
		/* due to Apple's silliness, this only needs to be done for 8-bit samples */
		if (StereoFlag)
			{
				if (!SixteenBitFlag)
					{
						/* stereo, 8-bit */
						for (Scan = (2 * NumUsedFrames) - 1; Scan >= 0; Scan -= 1)
							{
								((char*)Buffer)[Scan] += 0x80;
							}
					}
			}
		 else
			{
				if (!SixteenBitFlag)
					{
						/* mono, 8-bit */
						for (Scan = NumUsedFrames - 1; Scan >= 0; Scan -= 1)
							{
								((char*)Buffer)[Scan] += 0x80;
							}
					}
			}
		/* submit the command to actually play the buffer */
	 TryAgainPoint1:
		NextAvailableBuffer->InUseFlag = True;
		NextAvailableBuffer->MySoundCommand.cmd = bufferCmd;
		NextAvailableBuffer->MySoundCommand.param1 = 0;
		NextAvailableBuffer->MySoundCommand.param2 = (long)&(NextAvailableBuffer->Header);
		NextAvailableBuffer->Header.numFrames = NumUsedFrames;
		Error = SndDoCommand(MySoundChannel,&(NextAvailableBuffer->MySoundCommand),True);
		if (queueFull == Error)
			{
				/* oops, we filled up the OS queue; wait a little and try again */
				if (Callback != NIL)
					{
						(*Callback)(Refcon);
					}
				goto TryAgainPoint1;
			}
		/* submit the callback routine request.  the callback is an interrupt level */
		/* thing that clears a buffer so we can use it again. */
	 TryAgainPoint2:
		NextAvailableBuffer->MyCallbackCommand.cmd = callBackCmd;
		/* say where to store the "0" */
		NextAvailableBuffer->MyCallbackCommand.param2
			= (long)&(NextAvailableBuffer->InUseFlag);
		Error = SndDoCommand(MySoundChannel,&(NextAvailableBuffer->MyCallbackCommand),True);
		if (queueFull == Error)
			{
				if (Callback != NIL)
					{
						(*Callback)(Refcon);
					}
				goto TryAgainPoint2;
			}
		/* advance to the next buffer */
		NextAvailableBuffer = NextAvailableBuffer->Next;
	}


/* discard all queued data on the sound channel and close it immediately */
void				KillSoundChannel(void)
	{
		ERROR(!SoundSystemInUse,PRERR(ForceAbort,
			"KillSoundChannel:  but sound channel isn't open"));

		/* dispose of the sound channel */
		SndDisposeChannel(MySoundChannel,True/*shutupnow*/);

		/* release buffers */
		DisposeBuffers();

		/* mark sound subsystem as available */
		SoundSystemInUse = False;
	}
