/*
**	Apple Macintosh Developer Technical Support
**
**	Routines for dealing with virtual sound hardware from a 'sdev' component.
**
**	by Mark Cookson, Apple Developer Technical Support
**	--based on code by Kip Olson
**
**	File:	Hardware.c
**
**	Copyright ©1993-1996 Apple Computer, Inc.
**	All rights reserved.
**
**	You may incorporate this sample code into your applications without
**	restriction, though the sample code has been provided "AS IS" and the
**	responsibility for its operation is 100% yours.  However, what you are
**	not permitted to do is to redistribute the source as "Apple Sample
**	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 Code, but that you've made changes.
*/

#include <AIFF.h>
#include <Files.h>
#include <Devices.h>
#include <Timer.h>
#include <Sound.h>
#include <FixMath.h>
#include <Script.h>

#include "AIFFWriter.h"


#ifdef FakeInterrupts
extern SoundOutputGlobalsPtr					gGlobals;
#endif


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Initialize the hardware.

OSErr SetupHardware (SoundOutputGlobalsPtr globals)
{
	IOBufferPtr			ioBuffer;
	ParmBlkPtr			iopb;
	unsigned long		samples;
	short				i;
	OSErr				result;

	// setup our output data which we'll request from the mixer and
	// setup the output file as well
	
	globals->outputData.flags = 0;
	if ((**globals->prefs).sampleSize == 8)
		globals->outputData.format = k8BitOffsetBinaryFormat;
	else
		globals->outputData.format = k16BitBigEndianFormat;
	globals->outputData.numChannels = (**globals->prefs).numChannels;
	globals->outputData.sampleSize = (**globals->prefs).sampleSize;
	globals->outputData.sampleRate = (**globals->prefs).sampleRate;
	globals->outputData.sampleCount = (**globals->prefs).sampleCount;
	globals->outputData.buffer = nil;
	globals->outputData.reserved = 0;

	// Setup hardware here. This example just calculates the interrupt interval
	// for the time manager task interrupt based on the current sample rate.

	globals->interruptInterval = UnsignedFixedMulDiv(((long)kHardwareSampleCount) << 16,
													1000000, globals->outputData.sampleRate);

	samples = UnsignedFixedMulDiv(globals->outputData.sampleRate, kSecondsInIOBuffer, fixed1);
	samples *= (globals->outputData.sampleSize >> 3) * globals->outputData.numChannels;
	globals->ioBufferSize = (samples + kHardwareSampleCount - 1) & (~(kHardwareSampleCount - 1));

	result = SetupOutputFile(globals);							// initialize output file
	FailIf(result != noErr, Exit);

	for (i = 0; i < 2; ++i)										// loop over both parameter blocks
	{
		ioBuffer = &globals->ioBuffers[i];

		// get some memory for i/o buffers
		ioBuffer->buffer = NewPtrClear(globals->ioBufferSize);
		FailIf(ioBuffer->buffer == nil, NewPtrFailed);
		iopb = &ioBuffer->iopb;									// get pointer to param block

		/* fill up the param block with the fields that never change after this. Note that
		   fields that are set to zero are commented out since we just zeroed things above. */

		iopb->ioParam.ioRefNum		= globals->fRefNum;			// refnum of file to write to
//		iopb->ioParam.ioCompletion	= nil;						// no completion routine
//		iopb->ioParam.ioReqCount	= 0;						// no request yet
		iopb->ioParam.ioBuffer		= ioBuffer->buffer;			// buffer to write to
		iopb->ioParam.ioPosMode		= (1 << noCacheBit);		// write w/o cache
//		iopb->ioParam.ioPosOffset	= 0;						// no offset yet
	}
	return (noErr);


NewPtrFailed:
	if (globals->ioBuffers[0].buffer != nil)
		DisposePtr(globals->ioBuffers[0].buffer);			// dispose of buffers
	if (globals->ioBuffers[1].buffer != nil)
		DisposePtr(globals->ioBuffers[1].buffer);			// dispose of buffers
Exit:
	return (result);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Release the hardware.
// BUG ALERT!  If we sample MicroSeconds at just the wrong time, we could be here for a long time!

void ReleaseHardware (SoundOutputGlobalsPtr globals)
{
	unsigned long		timeLimit;
	unsigned long		startTime;

	startTime = MicroSeconds();
	timeLimit = globals->interruptInterval << 1;				// wait for 2 interrupt periods
	while ((globals->hardwareOn) && ((MicroSeconds() - startTime) < timeLimit))
	{
		;	// wait until interrupts turn off, or we time out
	}

	StopHardware(globals);										// make sure hardware is off

	if (globals->fRefNum != 0)
	{
		CloseOutputFile(globals);								// close output file
		globals->fRefNum = 0;
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Turn on hardware interrupts.

void StartHardware(SoundOutputGlobalsPtr globals)
{
	if (!globals->hardwareOn)
	{
		globals->hardwareOn = true;							// the hardware will soon be on

		// Turn hardware on here. The example uses the Time Manager for interrupts

#ifndef FakeInterrupts
		// start the time manager task going

		globals->tmTask.task.tmAddr = NewTimerProc(TMInterrupt);
		globals->tmTask.task.tmCount = 0;
		globals->tmTask.task.tmWakeUp = 0;
		globals->tmTask.task.tmReserved = 0;
		globals->tmTask.globals = globals;

		InsXTime((QElemPtr) &globals->tmTask);
		PrimeTime((QElemPtr) &globals->tmTask, -globals->interruptInterval);
#endif
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Turn off hardware interrupts.

void StopHardware(SoundOutputGlobalsPtr globals)
{
	if (globals->hardwareOn)
	{
		// Turn hardware off here. The example removes the Time Manager task

#ifndef FakeInterrupts
		RmvTime((QElemPtr) &globals->tmTask);
#endif
		globals->hardwareOn = false;						// the hardware is now off
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Resume hardware interrupts after they were suspended.

void ResumeHardware(SoundOutputGlobalsPtr globals)
{
	if (globals->hardwareOn)
	{
		// Resume hardware interrupts here. The example queues another Time Manager interrupt

#ifndef FakeInterrupts
		PrimeTime((QElemPtr) &globals->tmTask, -globals->interruptInterval);
#endif
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Copy samples to the hardware buffer. In our case, write to a file.

#define SamplesToBytes(samples, shift)	(samples << shift)
#define BytesToSamples(bytes, shift)	(bytes >> shift)

void CopySamplesToHardware(SoundOutputGlobalsPtr globals, SoundComponentDataPtr siftPtr)
{
	long			samplesToCopy;
	long			bytesToCopy;
	long			bytesLeft;
	IOBufferPtr		currentBuffer;
	short			sourceShift;
	short			destShift;

	sourceShift = siftPtr->sampleSize / 16;
	if (siftPtr->numChannels == 2)
		sourceShift++;

	destShift = globals->outputData.sampleSize / 16;				// add 1 more for 16 bit, 2 for 32
	if (globals->outputData.numChannels == 2)
		destShift++;

	samplesToCopy = siftPtr->sampleCount;							// don't copy more than hardware buffer has
	if (samplesToCopy > kHardwareSampleCount)
		samplesToCopy = kHardwareSampleCount;

	currentBuffer = &globals->ioBuffers[globals->currentIndex];		// get current i/o buffer
	if (currentBuffer->iopb.ioParam.ioResult != 0)
	{																// oh, oh, write is still in progress
		FailMessage(currentBuffer->iopb.ioParam.ioResult != 0);
		siftPtr->buffer += SamplesToBytes(samplesToCopy, sourceShift);	// update source pointer
		siftPtr->sampleCount -= samplesToCopy;						// subtract amount copied from source and throw these bytes away!
		return;														// throw these bytes away!
	}

	bytesToCopy = SamplesToBytes(samplesToCopy, destShift);		// turn samples into bytes
	bytesLeft = globals->ioBufferSize - currentBuffer->byteCount;		// calc how much is left in i/o buffer

	if (bytesToCopy > bytesLeft)								// limit to amount we have left in buffer
		bytesToCopy = bytesLeft;
	samplesToCopy = BytesToSamples(bytesToCopy, destShift);		// turn back into samples

	if (globals->fRefNum) 										// only write out if file is open
		OutputToFile(siftPtr, currentBuffer->buffer + currentBuffer->byteCount, bytesToCopy);

	currentBuffer->byteCount += bytesToCopy;					// update dest pointer
	siftPtr->buffer += SamplesToBytes(samplesToCopy, sourceShift);	// update source pointer
	siftPtr->sampleCount -= samplesToCopy;						// subtract amount copied from source

	if (currentBuffer->byteCount == (long)globals->ioBufferSize)// current buffer is full - write it out
	{
		if (globals->fRefNum)									// only write out if file is open
		{
			currentBuffer->iopb.ioParam.ioReqCount = currentBuffer->byteCount;	// write this many bytes out
			currentBuffer->iopb.ioParam.ioPosOffset = 0;		// offset from current position

#ifdef FakeInterrupts
			PBWriteSync(&currentBuffer->iopb);					// write out the data synchronously
#else
			PBWriteAsync(&currentBuffer->iopb);					// start the asynch write
#endif
		}

		currentBuffer->byteCount = 0;							// buffer will be empty next time we use it
		globals->currentIndex ^= 1;								// switch to other buffer
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Slam the file.

void OutputToFile(SoundComponentDataPtr siftPtr, void *dest, long bytesToCopy)
{
	long	i;
	Byte	*sp;
	Byte	*dp;

	if (siftPtr->format == k8BitOffsetBinaryFormat)				// 8 bit offset source
	{
		// convert the data from offset binary to two's complement
		
		sp = (Byte *)siftPtr->buffer;
		dp = (Byte *)dest;
		for (i = bytesToCopy - 1; i >= 0; --i)
			*dp++ = *sp++ ^ 0x80;
	}
	else														// copy other formats
		BlockMove(siftPtr->buffer, dest, bytesToCopy);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Fake interrupt routine used when running the debugging application.

#ifdef FakeInterrupts
Boolean FakeInterrupt(void)
{
	SoundOutputGlobalsPtr	globals = gGlobals;					// get our globals

	if (globals->hardwareOn)
	{
		if (MicroSeconds() >= globals->nextTime)
		{
			globals->nextTime = MicroSeconds();
			InterruptRoutine(globals);
			globals->nextTime += globals->interruptInterval;
		}
	}
	return (globals->hardwareOn);
}
#endif

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Entry point for the Time Manager task interrupt routine

//#if __MWERKS__ && TARGET_CPU_68K

//void TMInterrupt (myTMTaskPtr taskPtr:__a1)
//{
//	InterruptRoutine(taskPtr->globals);						// call interrupt routine with globals
//}

//#else

void TMInterrupt (myTMTaskPtr taskPtr)
{
#if TARGET_CPU_68K
	taskPtr = (myTMTaskPtr)GetRegisterA1();
#endif
	InterruptRoutine(taskPtr->globals);						// call interrupt routine with globals
}
//#endif

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This routine is called every hardware interrupt to fill the hardware
// with audio data. First it should suspend interrupts so it will not
// be interrupted again (this example just queues up the next interrupt).
// Then it should get more data from the source mixer, and copy the data
// to the hardware. In the way out, if all data was copied, try to get
// some more so it will be available next time.

void InterruptRoutine(SoundOutputGlobalsPtr globals)
{
	SoundComponentDataPtr		siftPtr;

	siftPtr = GetMoreSource(globals);						// get source from mixer
	if (siftPtr == nil)										// no more source
		StopHardware(globals);								// turn hardware off
	else
	{
		CopySamplesToHardware(globals, siftPtr);			// fullfill hardware request

		if (siftPtr->sampleCount == 0)						// exhausted the source
			siftPtr = GetMoreSource(globals);				// get more for next time
	}
	ResumeHardware(globals);								// queue next interrupt while we are processing
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This routine returns the component data pointer to your mixer source. If there
// is no source or it is empty, it will call down the chain to fill it up.

SoundComponentDataPtr GetMoreSource(SoundOutputGlobalsPtr globals)
{
	ComponentResult			result;
	SoundComponentDataPtr	siftPtr;

	siftPtr = globals->sourceDataPtr;
	if ((siftPtr == nil) || (siftPtr->sampleCount == 0))	// no data - better get some
	{
		result = SoundComponentGetSourceData(globals->sourceComponent, &globals->sourceDataPtr);
		FailIf(result != noErr, Failure);

		siftPtr = globals->sourceDataPtr;
		if ((siftPtr == nil) || (siftPtr->sampleCount == 0)) // source has no more data
			return (nil);									// stop the presses
	}
	return (siftPtr);										// return pointer to source

Failure:
	return (nil);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This routine initializes the output file and sets up the i/o param block
// used to write the data to disk.

OSErr SetupOutputFile (SoundOutputGlobalsPtr globals)
{
	FSSpec			fspec;
	OSErr			err;

	err = FSMakeFSSpec(0, 0, kSndFileName, &fspec);			// make the FS spec
	if (err == fnfErr)										// file not found is OK
		err = noErr;
	else
		FailIf(FSpDelete(&fspec) != noErr, Failure);
	FailIf(err != noErr, Failure);

	err = FSpCreate(&fspec, kAIFFCreator, AIFFID, smSystemScript);	// create file
	FailIf(err != noErr, FSpCreateFailed);

	err = FSpOpenDF(&fspec, fsRdWrPerm, &globals->fRefNum);		// open file
	FailIf(err != noErr, FSpOpenDFFailed);

	err = SetupAIFFHeader(globals->fRefNum, globals->outputData.numChannels, globals->outputData.sampleRate,
						  globals->outputData.sampleSize, globals->outputData.format, 0, 0);
	FailIf(err != noErr, SetupAIFFFailed);

	err = GetFPos(globals->fRefNum, &globals->headerLen);		// get length of AIFF header
	FailIf(err != noErr, SetupAIFFFailed);

	return (noErr);

SetupAIFFFailed:
	if (globals->fRefNum != 0)
		FSClose(globals->fRefNum);

FSpOpenDFFailed:
FSpCreateFailed:
Failure:
	return (err);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This routine makes sure all asynchronous i/o is complete, writes out any
// remaining data, updates the AIFF header and closes the file.

OSErr CloseOutputFile(SoundOutputGlobalsPtr globals)
{
	long			filePos, bytesWritten;
	IOBufferPtr		currentBuffer;
	OSErr			err;

	while  ((globals->ioBuffers[globals->currentIndex].iopb.ioParam.ioResult > 0) ||	// wait for PBWrite to complete
			(globals->ioBuffers[globals->currentIndex^1].iopb.ioParam.ioResult > 0))	// for both buffers
		;

	currentBuffer = &globals->ioBuffers[globals->currentIndex];	// get current i/o buffer

	if (currentBuffer->byteCount != 0)					// still some data to write out
	{
		currentBuffer->iopb.ioParam.ioReqCount = currentBuffer->byteCount;	// write this many bytes out
		currentBuffer->iopb.ioParam.ioPosOffset = 0;	// offset from current position

		err = PBWriteSync(&currentBuffer->iopb);		// write this buffer synchronously
		FailIf(err != noErr, PBWriteSyncFailed);
	}

	err = GetFPos(globals->fRefNum, &filePos);			// get current file position
	FailIf(err != noErr, GetFPosFailed);

	bytesWritten = filePos - globals->headerLen;		// calc no. bytes written to file
	filePos = ++filePos & ~1;							// make sure file length is a word-aligned

	err = SetEOF(globals->fRefNum, filePos);			// set current file position to EOF
	FailIf(err != noErr, SetEOFFailed);

	err = SetFPos(globals->fRefNum, fsFromStart, 0);	// rewind file to beginning
	FailIf(err != noErr, SetFPosFailed);

	err = SetupAIFFHeader(globals->fRefNum, globals->outputData.numChannels, globals->outputData.sampleRate,
						  globals->outputData.sampleSize, globals->outputData.format, bytesWritten, 0);
	FailIf(err != noErr, SetupAIFFHeaderFailed);
	// all non-error conditions can fall through to the clean up code

SetupAIFFHeaderFailed:
SetFPosFailed:
SetEOFFailed:
GetFPosFailed:
PBWriteSyncFailed:
	FSClose(globals->fRefNum);							// close output file
	globals->fRefNum = 0;

	return (err);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OSErr GetPreferences (SoundOutputGlobalsPtr globals)
{
	HardwarePrefsHandle		prefs;
	HardwarePrefsHandle		oldPrefs;
	OSErr					result;

	prefs = (HardwarePrefsHandle)GetComponentRefcon((Component)globals->self);
	if (prefs == nil)
	{	
		// no in memory preferences found, then create them
		prefs = (HardwarePrefsHandle)NewHandleSysClear(sizeof(HardwarePreferences));
		FailWithAction(prefs == nil, result = MemError(), NewPrefsFailed);

		// set our preferences to default values and put them in our refCon
		(**prefs).size = sizeof(HardwarePreferences);
		(**prefs).version = kSoundComponentVersion;
		(**prefs).volume = 0x00100100;
		(**prefs).sampleRate = rate22050hz;				// default sample rate
		(**prefs).sampleSize = 16;						// default sample size
		(**prefs).numChannels = 2;						// default num channels
		(**prefs).sampleCount = kHardwareSampleCount;	// default buffer count
		SetComponentRefcon((Component)globals->self, (unsigned long)prefs);
		globals->prefs = prefs;											// set global

		oldPrefs = (HardwarePrefsHandle)NewHandleSysClear(sizeof(HardwarePreferences));
		FailWithAction(oldPrefs == nil, result = MemError(), NewOldPrefsFailed);

		result = GetSoundPreference(kAIFFWriterSubType, kAIFFWriterName, (Handle)oldPrefs);
		if (result == noErr)
		{
			// we found our old preferences, check version and size of old preferences
			(**prefs).volume = (**oldPrefs).volume;
			(**prefs).sampleRate = (**oldPrefs).sampleRate;
			(**prefs).sampleSize = (**oldPrefs).sampleSize;
			(**prefs).numChannels = (**oldPrefs).numChannels;
			(**prefs).sampleCount = (**oldPrefs).sampleCount;
		}
		DisposeHandle((Handle)oldPrefs);
		
		result = InitFromPreferences(globals);
		FailIf(result != noErr, InitHWFailed);
	}
	return (noErr);
	
InitHWFailed:
NewOldPrefsFailed:
NewPrefsFailed:
	return (result);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Setup the hardware according to the user's preferences.
// Configure as much of the hardware as possible at this time, and use the
// InitOutputDevice() method call for the configuration that needs to be done
// everytime the component is being opened to play audio. If altering a setting
// would cause a click, then it would be better to do that here instead of everytime
// the user started a new sound.

OSErr InitFromPreferences(SoundOutputGlobalsPtr globals)
{
	globals; // suppress "unused variable" warning for all compilers
	return (noErr);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#if !TARGET_CPU_68K
// for 68K, the macro we are using only returns a long, so the PPC version
// will just return the lo part of the number to be consistent
unsigned long MicroSeconds(void)
{
	UnsignedWide			microTickCount;

	Microseconds(&microTickCount);
	return (microTickCount.lo);
}
#endif
