/*=============================================================================

	oalDevice.cpp
	al_osx
	
	Created by Bob Aron on Mon Oct 13 2003.
	Copyright (c) 2003 Apple Computer Inc. All rights reserved.
 
 	$Log: oalDevice.cpp,v $
 	Revision 1.35  2004/09/27 21:40:24  baron
 	Make the OALSource object thread safe by using two bufferQ lists and mutexes. Don't dereference NULL ptr. [3791105, 3791120]
 	
 	Revision 1.34  2004/09/02 20:20:07  baron
 	Add workaround for DistanceParameter problem in the 2.0 3DMixer. Allow Doppler to be turned ON. Allow OAL Distance Model to be AL_NONE.
 	
 	Revision 1.33  2004/05/11 22:01:39  baron
 	Add breaks to switch statement in GetDesiredRenderChannelCount().
 	
 	Revision 1.32  2004/04/23 16:56:39  baron
 	Correctly set the DistanceAtten property when using the v1.3 3DMixer. Rolloff curve should now be correct and this change fine tunes the change to scale distance calculations to the reference distance. Explicitly set the channel layout on the mixer and output unit.
 	
 	Revision 1.31  2004/03/20 01:59:55  baron
 	Until the  ALC_ENUMERATION_EXT extension is supported, always pass NULL for the device name when creating a new OALDevice object.
 	
 	Revision 1.30  2004/03/19 22:49:05  baron
 	Add BSD License and clean up comments before submitting to Open Source (Creative).
 	
 	Revision 1.29  2004/03/12 01:22:26  baron
 	Render to stereo if channel layout is actually 3 channels.
 	
 	Revision 1.28  2004/03/11 19:42:22  baron
 	Add extension to clamp rendering to stereo (ALC_RENDER_CHANNEL_COUNT). Merge the OAL3DGraph and OALDevice classes. General clean up.
 	
 	Revision 1.27  2004/03/04 01:13:55  baron
 	remove warnings.
 	
 	Revision 1.26  2004/02/25 22:56:41  baron
 	Should not need to explicitly set the input scope of the outputAU for channel layout for new mixer. Must explicitly set the channel layout on the output scope of older mixer before connecting the AUs.
 	
 	Revision 1.25  2004/02/25 22:05:59  baron
 	Do not render to quad, if the new 3DMixer (2.0) is not present.
 	
 	Revision 1.24  2004/02/25 21:52:53  baron
 	Do not try and set the bus count if the new 3DMixer is not present.
 	
 	Revision 1.23  2004/02/25 20:51:55  baron
 	Check version of 3DMixer component and fail if pre-Panther version. Use playback rate parameter to adjust sample rates if 3DMixer is not 2.0 or >, else use stream format property to change sample rates on the mixer busses.
 	
 	Revision 1.22  2004/02/24 01:17:27  baron
 	Throw exception from the OALGraph constructor.
 	
 	Revision 1.21  2004/02/13 23:04:25  baron
 	Add reverb support. Not yet exposed through the API however.
 	
 	Revision 1.20  2004/02/13 17:38:05  baron
 	Use the channel count obtained by getting the preferred channel layout, instead of the stream format channel count when determining the spatial setting. Fixes the case where a multi channel device is only using 2 speakers and SoundField gets used instead of a stereo panner.
 	
 	Revision 1.19  2004/02/13 02:37:12  baron
 	Check for NANs. Add 3DMixer Distance Params support. Partial Min/Max Gain changes. Everything builds and works, some commented code needs to be clean up still.
 	
 	Revision 1.18  2004/02/13 01:37:43  crogers
 	set render flags AFTER setting spatialization algorithm
 	
 	Revision 1.17  2004/02/11 18:16:52  baron
 	Refactor all the graph setup code. Fix the channel layout support. Start the graph right away and do not call AUGraphStart() after each new play call.
 	
 	Revision 1.16  2004/02/04 17:28:33  baron
 	Switch to EqualPower panner for low quality for now. Add some Mixer Bus Logging code.
 	
 	Revision 1.15  2004/01/29 22:06:19  baron
 	Fix all the renderflag, spatialization setting code. Make settable from the API set. Add support for multi channel hw.
 	
 	Revision 1.14  2004/01/23 06:11:54  baron
 	Delete graph object when the OALDevice object is destroyed.
 	
 	Revision 1.13  2004/01/20 19:42:06  baron
 	Set the stream format if necessary when getting an available bus. cleanup.
 	
 	Revision 1.12  2004/01/20 00:47:35  baron
 	Add some AUTracer code. Add build flag to check for Overload notifications.
 	
 	Revision 1.11  2004/01/09 18:35:06  baron
 	Remove calloc from Render Proc and other potential blocking code. Add several build flags and logging code.
 	
 	Revision 1.10  2004/01/06 01:59:43  baron
 	Add build flag for testing OAL with the stereo mixer.
 	
 	Revision 1.9  2003/12/23 01:42:56  baron
 	Remove commented out code and warning.
 	
 	Revision 1.8  2003/12/17 18:33:05  baron
 	Add Speaker Configuration support.
 	
 	Revision 1.7  2003/12/17 00:21:14  baron
 	Add support for a internal mix sample rate. Dependent on enhancements to the 3DMixer.
 	
 	Revision 1.6  2003/12/09 01:26:22  baron
 	more cleanup.
 	
 	Revision 1.5  2003/12/03 23:17:09  baron
 	Add some doppler support and clean up comments
 	
 	Revision 1.4  2003/12/02 18:59:50  baron
 	Add check-in comment logging.
 	
	
	$NoKeywords: $

=============================================================================*/

/**********************************************************************************************************************************
*
*   OpenAL cross platform audio library
*   Copyright © 2004, Apple Computer, Inc. All rights reserved.
*
*   Redistribution and use in source and binary forms, with or without modification, are permitted provided 
*   that the following conditions are met:
*
*   1.  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
*   2.  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
*       disclaimer in the documentation and/or other materials provided with the distribution. 
*   3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of its contributors may be used to endorse or promote 
*       products derived from this software without specific prior written permission. 
*
*   THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
*   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS 
*   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
*   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
*   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 
*   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
**********************************************************************************************************************************/

#include "oalDevice.h"

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#define LOG_BUS_CONNECTIONS  	0
#define PROFILE_IO_USAGE  		0

#if PROFILE_IO_USAGE
static int debugCounter         = -1;
static int numCyclesToPrint     = 1000;

static UInt64 lastHostTime;
static UInt64 totalHostTime;
static UInt64 minUsage;
static UInt64 maxUsage;
static UInt64 totalUsage;

#define PROFILE_IO_CYCLE 0
#if PROFILE_IO_CYCLE
static UInt64 maxHT;
static UInt64 minHT;
#endif

#include <CoreAudio/HostTime.h>
#endif

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
#if GET_OVERLOAD_NOTIFICATIONS
OSStatus	PrintTheOverloadMessage(	AudioDeviceID			inDevice,
										UInt32					inChannel,
										Boolean					isInput,
										AudioDevicePropertyID	inPropertyID,
										void*					inClientData)
{
	DebugMessage("OVERLOAD OCCURRED");
}
#endif

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// OALDevices
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#pragma mark ***** OALDevices *****

/*
	If caller wants a specific HAL device (instead of the default output device), a NULL terminated 
	C-String representation of the CFStringRef returned from the HAL APIs for the 
	kAudioDevicePropertyDeviceUID property
*/
OALDevice::OALDevice (const char* 	 inDeviceName, UInt32   inSelfToken, UInt32  &inBusCount)
	: 	mSelfToken (inSelfToken),
        mHALDevice (0),
        mPreferred3DMixerExists(true),
        mDistanceScalingRequired(false),
        mDefaultReferenceDistance(1.0),
        mDefaultMaxDistance(100000.0),
        mAUGraph(0),
        mBusCount(inBusCount),
		mOutputNode(0), 
		mOutputUnit(0),
		mMixerNode(0), 
		mMixerUnit (0),
		mMixerOutputRate(kDefaultMixerRate),
		mCurrentMixerChannelCount(0),
        mRenderChannelCount(ALC_RENDER_CHANNEL_COUNT_MULTICHANNEL),
		mRenderQuality(ALC_SPATIAL_RENDERING_QUALITY_LOW),
		mSpatialSetting(0),
        mReverbSetting(0)
#if LOG_BUS_CONNECTIONS
		, mMonoSourcesConnected(0),
		mStereoSourcesConnected(0)
#endif
{
	OSStatus	result = noErr;
	UInt32		size = 0;
	CFStringRef	cfString = NULL;
    char        *useThisDevice = (char *) inDeviceName;
	
	try {
        // until the ALC_ENUMERATION_EXT extension is supported only use the default output device
        useThisDevice = NULL;
        
		// first, get the requested HAL device's ID
		if (useThisDevice)
		{
			// turn the inDeviceName into a CFString
			cfString = CFStringCreateWithCString(NULL, useThisDevice, kCFStringEncodingUTF8);
			if (cfString)
			{
				AudioValueTranslation	translation;
				
				translation.mInputData = &cfString;
				translation.mInputDataSize = sizeof(cfString);
				translation.mOutputData = &mHALDevice;
				translation.mOutputDataSize = sizeof(mHALDevice);
				
				size = sizeof(AudioValueTranslation);
				result = AudioHardwareGetProperty(kAudioHardwarePropertyDeviceForUID, &size, &translation);
                CFRelease (cfString);
			}
			else
				result = -1; // couldn't get string ref
				
			THROW_RESULT
		}
		else
		{
			size = sizeof(AudioDeviceID);
			result = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &size, &mHALDevice);
				THROW_RESULT
		}
		
		InitializeGraph(useThisDevice);
		
#if PROFILE_IO_USAGE
		debugCounter = -1;
#endif

	}
	catch (...) {
        throw;
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OALDevice::~OALDevice()
{		
	TeardownGraph();
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void	OALDevice::SetEnvironment(OALEnvironmentInfo	&inEnvironment)
{
    // currently unused, will be useful when applications actually switch between different OAL Contexts
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::InitializeGraph (const char* 		inDeviceName)
{	
	if (mAUGraph)
		throw static_cast<OSStatus>('init');
	
    OSStatus result = noErr;

    // ~~~~~~~~~~~~~~~~~~~ GET 3DMIXER VERSION

	ComponentDescription	mixerCD;
	mixerCD.componentFlags = 0;        
	mixerCD.componentFlagsMask = 0;     
    mixerCD.componentType = kAudioUnitType_Mixer;          
	mixerCD.componentSubType = kAudioUnitSubType_3DMixer;       
	mixerCD.componentManufacturer = kAudioUnitManufacturer_Apple;  

    ComponentInstance   mixerInstance = OpenComponent(FindNextComponent(0, &mixerCD));
    long  version = GetComponentVersion(mixerInstance);
    CloseComponent(mixerInstance);
    
    if (version < kMinimumMixerVersion)
        throw -1;                           // we do not have a current enough 3DMixer to use, so set an error and throw
    else if (version < 0x20000)
        mPreferred3DMixerExists = false;    // we do not have version 2.0 or greater of the 3DMixer
    else if (version == 0x20000)
        mDistanceScalingRequired = true;        
    
    // ~~~~~~~~~~~~~~~~~~~~ CREATE GRAPH

	result = NewAUGraph(&mAUGraph);
		THROW_RESULT
	 
    // ~~~~~~~~~~~~~~~~~~~~ SET UP BASIC NODES

	ComponentDescription	cd;
	cd.componentFlags = 0;        
	cd.componentFlagsMask = 0;     

	// At this time, only allow the default output device to be used and ignore the inDeviceName parameter
	cd.componentType = kAudioUnitType_Output;          
	cd.componentSubType = kAudioUnitSubType_DefaultOutput;       	
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;  
	result = AUGraphNewNode (mAUGraph, &cd, 0, NULL, &mOutputNode);
		THROW_RESULT
		        
    result = AUGraphNewNode (mAUGraph, &mixerCD, 0, NULL, &mMixerNode);
		THROW_RESULT
		
	// ~~~~~~~~~~~~~~~~~~~~ OPEN AND INITIALIZE GRAPH
	
	result = AUGraphOpen (mAUGraph);
		THROW_RESULT
	
	result = AUGraphGetNodeInfo (mAUGraph, mMixerNode, 0, 0, 0, &mMixerUnit);
		THROW_RESULT   
	
	result = AUGraphGetNodeInfo (mAUGraph, mOutputNode, 0, 0, 0, &mOutputUnit);
		THROW_RESULT   

#if	USE_AU_TRACER
	result = mAUTracer.Establish (mOutputUnit, "/tmp/OALOverloadTraces");
	printf ("\nProfiling Overload Latencies to log files: /tmp/OALOverloadTraces.txt\n");
#endif
	
 	result = AudioUnitInitialize (mOutputUnit);
		THROW_RESULT

	SetupGraph();

    // store some default distance settings
    if (mPreferred3DMixerExists)
    {
        MixerDistanceParams		distanceParams;
        UInt32                  outSize;
        result = AudioUnitGetProperty(GetMixerUnit(), kAudioUnitProperty_3DMixerDistanceParams, kAudioUnitScope_Input, 1, &distanceParams, &outSize);
        if (result == noErr)
        {
            mDefaultReferenceDistance = distanceParams.mReferenceDistance;
            mDefaultMaxDistance = distanceParams.mMaxDistance;
        }
    }
	 	
	mCanScheduleEvents = true;
	Print();

	AUGraphStart(mAUGraph);
		
#if GET_OVERLOAD_NOTIFICATIONS
	AudioDeviceID	device = 0;
	UInt32			size = sizeof(device);
	
	AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &size, &device);
	if (device != 0)
	{
		DebugMessage("********** Adding Overload Notification ***********");
		AudioDeviceAddPropertyListener(	device, 0, false, kAudioDeviceProcessorOverload, PrintTheOverloadMessage, NULL);	
	}
#endif	
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::TeardownGraph()
{
#if GET_OVERLOAD_NOTIFICATIONS
	AudioDeviceID	device = 0;
	UInt32			size = sizeof(device);
	
	AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &size, &device);
	if (device != 0)
	{
		DebugMessage("********** Removing Overload Notification ***********");
		AudioDeviceRemovePropertyListener(	device, 0, false, kAudioDeviceProcessorOverload, PrintTheOverloadMessage);	
	}
#endif

	if (mAUGraph) 
	{
		AUGraphStop (mAUGraph);

		DisposeAUGraph (mAUGraph);
		mAUGraph = 0;
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*  
    Determine the correct number of channels for the mixer to render to, by discovering what 
    channel layout has been set for the output unit. Account for differences in the 
    1.3/2.0 versions of the 3DMixer, and user setting of for the ALC_RENDER_CHANNEL_COUNT
    OAL extension. This method should only return: 2 (stereo) ,4 (quad) or 5 (5.0).
*/
UInt32 OALDevice::GetDesiredRenderChannelCount ()
{
	UInt32			returnValue = 2;	// return stereo by default
	
    // observe the mRenderChannelCount flag and clamp to stereo if necessary
    // This allows the user to request the libary to render to stereo in the case where only 2 speakers
    // are connected to multichannel hardware
    if (mRenderChannelCount == ALC_RENDER_CHANNEL_COUNT_STEREO)
        return (returnValue);

	// get the HAL device id form the output AU
	AudioDeviceID	deviceID;
	UInt32			outSize =  sizeof(deviceID);
	OSStatus	result = AudioUnitGetProperty(mOutputUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Output, 1, &deviceID, &outSize);
		THROW_RESULT
	
	// get the channel layout set by the user in AMS		
	result = AudioDeviceGetPropertyInfo(deviceID, 0, false, kAudioDevicePropertyPreferredChannelLayout, &outSize, NULL);
    if (result != noErr)
        return (returnValue);   // default to stereo since channel layout could not be obtained

	AudioChannelLayout	*layout = NULL;
	layout = (AudioChannelLayout *) calloc(1, outSize);
	if (layout != NULL)
	{
		result = AudioDeviceGetProperty(deviceID, 0, false, kAudioDevicePropertyPreferredChannelLayout, &outSize, layout);
		if (layout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions)
		{
			// no channel layout tag is returned, so walk through the channel descriptions and count 
            // the channels that are associated with a speaker
			if (layout->mNumberChannelDescriptions == 2)
			{	
                returnValue = 2;        // there is no channel info for stereo
			}
			else
			{
				returnValue = 0;
				for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; i++)
				{
					if (layout->mChannelDescriptions[i].mChannelLabel != kAudioChannelLabel_Unknown)
						returnValue++;
				}
			}
		}
		else
		{
			switch (layout->mChannelLayoutTag)
			{
				case kAudioChannelLayoutTag_AudioUnit_5_0:
				case kAudioChannelLayoutTag_AudioUnit_5_1:
				case kAudioChannelLayoutTag_AudioUnit_6:
					returnValue = 5;
					break;
				case kAudioChannelLayoutTag_AudioUnit_4:
					returnValue = 4;
					break;
				default:
					returnValue = 2;
					break;
			}
		}
	
		free(layout);
	}
    
    if ((!mPreferred3DMixerExists) && (returnValue == 4))
    {
        // quad did not work properly before version 2.0 of the 3DMixer, so just render to stereo
        returnValue = 2;
    }
    else if (returnValue < 4)
    {
        // guard against the possibility of multi channel hw that has never been given a preferred channel layout
        // Or, that a 3 channel layout was returned (which is unsupported by the 3DMixer)
        returnValue = 2; 
    } 
    else if (returnValue > 5)
    {
        // 3DMixer currently does not render to more than 5 channels
        returnValue = 5;    
    }
        
	return (returnValue);
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
    This method should ONLY be called just before the mixer and output AUs are connected. 
    By explicitly setting the mixer's channel layout, the input scope of the output AU will
    get configured correctly at connection time.
*/
void	OALDevice::ConfigureGraphForChannelLayout()
{
	OSStatus	result = noErr;

	// get the channel count that should be set for the mixer's output stream format
	mCurrentMixerChannelCount = GetDesiredRenderChannelCount();

	// set the stream format
	CAStreamBasicDescription	format;
	UInt32                      outSize = sizeof(format);
	result = AudioUnitGetProperty(mOutputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &format, &outSize);
		THROW_RESULT

	format.SetCanonical (mCurrentMixerChannelCount, false);     // not interleaved
	format.mSampleRate = mMixerOutputRate;
	outSize = sizeof(format);
	result = AudioUnitSetProperty (mOutputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, outSize);
		THROW_RESULT

	result = AudioUnitSetProperty (mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &format, outSize);
		THROW_RESULT

    // explicitly set the channel layout, pre 2.0 mixer is just 5.0 and stereo
    // the output AU will then be configured correctly when the mixer is connected to it later
    AudioChannelLayout		layout;
     
    if (mCurrentMixerChannelCount == 5)
        layout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_0;
    else if (mCurrentMixerChannelCount == 4)
        layout.mChannelLayoutTag = (mPreferred3DMixerExists == true) ? kAudioChannelLayoutTag_AudioUnit_4 : kAudioChannelLayoutTag_Stereo;
    else
        layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
    
    layout.mChannelBitmap = 0;			
    layout.mNumberChannelDescriptions = 0;

    // it isn't currently necessary to explicitly set the mixer's output channel layout but it might in the future if there were more
    // than one layout usable by the current channel count. It doesn't hurt to set this property.
    outSize = sizeof(layout);
    result = AudioUnitSetProperty (mMixerUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, 0, &layout, outSize);

	result = AudioUnitSetProperty (mOutputUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Input, 0, &layout, outSize);
		THROW_RESULT
    
	return;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::InitRenderQualityOnBusses(UInt32	&inRenderQuality)
{
	mRenderQuality = inRenderQuality;

	if (GetDesiredRenderChannelCount() > 2)
	{
        // at this time, there is only one spatial quality being used for multi channel hw
        DebugMessage("********** InitRenderQualityOnBusses:kDefaultMultiChannelQuality ***********");
		mSpatialSetting = kDefaultMultiChannelQuality;
	}
	else if (mRenderQuality == ALC_SPATIAL_RENDERING_QUALITY_LOW)
	{
		// this is the default case for stereo
        DebugMessage("********** InitRenderQualityOnBusses:kDefaultLowQuality ***********");
		mSpatialSetting =  kDefaultLowQuality;
	}
	else
	{
		DebugMessage("********** InitRenderQualityOnBusses:kDefaultHighQuality ***********");
		mSpatialSetting = kDefaultHighQuality;
	}
	
	UInt32 		render_flags_3d = k3DMixerRenderingFlags_DistanceAttenuation;
	if (mRenderQuality == ALC_SPATIAL_RENDERING_QUALITY_HIGH)
	{
    	 // off by default, on if the user sets High Quality rendering, as HRTF requires InterAuralDelay to be on
         render_flags_3d += k3DMixerRenderingFlags_InterAuralDelay;     
	}
    
    if (mReverbSetting > 0)
	{
    	 // off by default, on if the user turns on Reverb, as it requires DistanceDiffusion to be on
    	render_flags_3d += k3DMixerRenderingFlags_DistanceDiffusion;
    }
    
	OSStatus                    result = noErr;
    UInt32                      outSize;
    CAStreamBasicDescription	format;
	for (UInt32	i = 0; i < mBusCount; i++)
	{
		outSize = sizeof(format);
		result = AudioUnitGetProperty (	mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &format, &outSize);
		
		// only reset the mono channels, stereo channels are always set to stereo pass thru regardless of render quality setting
        if ((result == noErr) && (format.NumberChannels() == 1)) 
		{
			// Spatialization
			result = AudioUnitSetProperty(	mMixerUnit, kAudioUnitProperty_SpatializationAlgorithm, kAudioUnitScope_Input, 
											i, &mSpatialSetting, sizeof(mSpatialSetting));

			// Render Flags                
            result = AudioUnitSetProperty(	mMixerUnit, kAudioUnitProperty_3DMixerRenderingFlags, kAudioUnitScope_Input, 
											i, &render_flags_3d, sizeof(render_flags_3d));
			
			// Doppler - This must be done AFTER the spatialization setting, because some algorithms explicitly turn doppler on
			UInt32		doppler = kDopplerDefault;
			result = AudioUnitSetProperty(mMixerUnit, kAudioUnitProperty_DopplerShift, kAudioUnitScope_Input, i, &doppler, sizeof(doppler));
		}
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void	OALDevice::SetupGraph()
{
    ConfigureGraphForChannelLayout();	
	
	// connect mixer to output unit
	OSStatus	result = AUGraphConnectNodeInput (mAUGraph, mMixerNode, 0, mOutputNode, 0);
		THROW_RESULT

	// reverb off by default
    result = AudioUnitSetProperty(mMixerUnit, kAudioUnitProperty_UsesInternalReverb, kAudioUnitScope_Global, 0, &mReverbSetting, sizeof(mReverbSetting));
	// ignore result
		
	result = AUGraphInitialize (mAUGraph);
		THROW_RESULT

	// update the graph
	for (;;) {
		result = AUGraphUpdate (mAUGraph, NULL);
		if (result == noErr)
			break;
	}
		
	result = AudioUnitAddPropertyListener (mOutputUnit, kAudioUnitProperty_StreamFormat, GraphFormatPropertyListener, this);
		THROW_RESULT

    // ~~~~~~~~~~~~~~~~~~~~ BUS SETUP

    UInt32	outSize;
    if (!mPreferred3DMixerExists)
    {
        mBusCount = kDefaultMaximumMixerBusCount; // 1.3 version of the mixer did not allow a change in the bus count
    }
    else
    {
        // set the bus count on the mixer if necessary	
        UInt32  currentBusCount;
        outSize = sizeof(currentBusCount);
        result = AudioUnitGetProperty (	mMixerUnit, kAudioUnitProperty_BusCount, kAudioUnitScope_Input, 0, &currentBusCount, &outSize);
        if ((result == noErr) && (mBusCount != currentBusCount))
        {
            result = AudioUnitSetProperty (	mMixerUnit, kAudioUnitProperty_BusCount, kAudioUnitScope_Input, 0, &mBusCount, outSize);
            if (result != noErr)
            {
                // couldn't set the bus count so make sure we know just how many busses there are
                outSize = sizeof(mBusCount);
                AudioUnitGetProperty (	mMixerUnit, kAudioUnitProperty_BusCount, kAudioUnitScope_Input, 0, &mBusCount, &outSize);
            }
        }
    }
    mBusInfo = (BusInfo *) calloc (1, sizeof(BusInfo) * mBusCount);

	// INITIALIZE THE BUSSES - set doppler & distance attenuation, make all busses mono, since this is the more common case
	CAStreamBasicDescription	format;
	for (UInt32	i = 0; i < mBusCount; i++)
	{				
		// Stream Format: kAudioUnitProperty_StreamFormat: mono
		outSize = sizeof(format);
		AudioUnitGetProperty (	mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &format, &outSize);
		if (format.NumberChannels() != 1)
		{
			format.mChannelsPerFrame = 1;
			AudioUnitSetProperty (	mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &format, outSize);
		}

		// Distance Attenuation: for pre v2.0 mixer
        SetDistanceAttenuation (i, kDefaultReferenceDistance, kDefaultMaximumDistance, kDefaultRolloff);			

		mBusInfo[i].mIsAvailable = true;
		mBusInfo[i].mNumberChannels = 1;
		mBusInfo[i].mReverbSetting = mReverbSetting;
	}

	// this call sets the kAudioUnitProperty_SpatializationAlgorithm & kAudioUnitProperty_3DMixerRenderingFlags for all mono busses
	InitRenderQualityOnBusses(mRenderQuality); 	
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
UInt32		OALDevice::GetAvailableMonoBus ()
{
	// look for a bus already set for mono
	for (UInt32 i = 0; i < mBusCount; i++)
	{
		if (mBusInfo[i].mIsAvailable == true && mBusInfo[i].mNumberChannels == 1) 
		{
			mBusInfo[i].mIsAvailable = false;
#if LOG_BUS_CONNECTIONS
			mMonoSourcesConnected++;
			DebugMessageN2("GetAvailableMonoBus1: Sources Connected, Mono =  %ld, Stereo = %ld\n", mMonoSourcesConnected, mStereoSourcesConnected);
			DebugMessageN1("GetAvailableMonoBus1: BUS_NUMBER = %ld\n", i);
#endif
			return (i);
		}
	}

	// couldn't find a mono bus, so find any available channel and make it mono
	for (UInt32 i = 0; i < mBusCount; i++)
	{
		if (mBusInfo[i].mIsAvailable == true) 
		{
#if LOG_BUS_CONNECTIONS
			mMonoSourcesConnected++;
			DebugMessageN2("GetAvailableMonoBus2: Sources Connected, Mono =  %ld, Stereo = %ld\n", mMonoSourcesConnected, mStereoSourcesConnected);
#endif
			CAStreamBasicDescription 	theOutFormat;
			theOutFormat.mChannelsPerFrame = 1;
			theOutFormat.mSampleRate = GetMixerRate();          // as a default, set the bus to the mixer's output rate, it should get reset if necessary later on
			theOutFormat.mFormatID = kAudioFormatLinearPCM;
			theOutFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
			theOutFormat.mBytesPerPacket = sizeof (Float32);	
			theOutFormat.mFramesPerPacket = 1;	
			theOutFormat.mBytesPerFrame = sizeof (Float32);
			theOutFormat.mBitsPerChannel = sizeof (Float32) * 8;	
			OSStatus	result = AudioUnitSetProperty (	mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 
														i, &theOutFormat, sizeof(CAStreamBasicDescription));
				THROW_RESULT

			mBusInfo[i].mIsAvailable = false;
			mBusInfo[i].mNumberChannels = 1; 			
			AudioUnitSetProperty(	mMixerUnit, kAudioUnitProperty_SpatializationAlgorithm, kAudioUnitScope_Input, 
									i, &mSpatialSetting, sizeof(mSpatialSetting));

			UInt32 		render_flags_3d = k3DMixerRenderingFlags_DistanceAttenuation;
			if (mRenderQuality == ALC_SPATIAL_RENDERING_QUALITY_HIGH)
				render_flags_3d += k3DMixerRenderingFlags_InterAuralDelay; // off by default, on if the user sets High Quality rendering

			// Render Flags
			result = AudioUnitSetProperty(	mMixerUnit, kAudioUnitProperty_3DMixerRenderingFlags, kAudioUnitScope_Input, 
											i, &render_flags_3d, sizeof(render_flags_3d));
			
			return (i);
		}
	}
	
	DebugMessage("ERROR: GetAvailableMonoBus: COULD NOT GET A MONO BUS");
	throw (-1); // no inputs available
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
UInt32		OALDevice::GetAvailableStereoBus ()
{
	for (UInt32 i = 0; i < mBusCount; i++)
	{
		if (mBusInfo[i].mIsAvailable == true && mBusInfo[i].mNumberChannels == 2) 
		{
			mBusInfo[i].mIsAvailable = false;
#if LOG_BUS_CONNECTIONS
			mStereoSourcesConnected++;
			DebugMessageN2("GetAvailableStereoBus1: Sources Connected, Mono =  %ld, Stereo = %ld\n", mMonoSourcesConnected, mStereoSourcesConnected);
			DebugMessageN1("GetAvailableStereoBus1: BUS_NUMBER = %ld\n", i);
#endif
			return (i);
		}
	}

	// couldn't find one, so look for a mono channel, make it stereo and set to kSpatializationAlgorithm_StereoPassThrough
	for (UInt32 i = 0; i < mBusCount; i++)
	{
		if (mBusInfo[i].mIsAvailable == true) 
		{

#if LOG_BUS_CONNECTIONS
			mStereoSourcesConnected++;
			DebugMessageN2("GetAvailableStereoBus2: Sources Connected, Mono =  %ld, Stereo = %ld\n", mMonoSourcesConnected, mStereoSourcesConnected);
			DebugMessageN1("GetAvailableStereoBus2: BUS_NUMBER = %ld\n", i);
#endif
			CAStreamBasicDescription 	theOutFormat;
			theOutFormat.mChannelsPerFrame = 2;
			theOutFormat.mSampleRate = GetMixerRate();
			theOutFormat.mFormatID = kAudioFormatLinearPCM;
			theOutFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
			theOutFormat.mBytesPerPacket = sizeof (Float32);	
			theOutFormat.mFramesPerPacket = 1;	
			theOutFormat.mBytesPerFrame = sizeof (Float32);
			theOutFormat.mBitsPerChannel = sizeof (Float32) * 8;	
			OSStatus	result = AudioUnitSetProperty (	mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 
														i, &theOutFormat, sizeof(CAStreamBasicDescription));
				THROW_RESULT

			mBusInfo[i].mIsAvailable = false;
			mBusInfo[i].mNumberChannels = 2; 

			UInt32		spatAlgo = kSpatializationAlgorithm_StereoPassThrough;
			AudioUnitSetProperty(	mMixerUnit, kAudioUnitProperty_SpatializationAlgorithm, kAudioUnitScope_Input, 
									i, &spatAlgo, sizeof(spatAlgo));

			return (i);
		}
	}
	
	DebugMessage("ERROR: GetAvailableStereoBus: COULD NOT GET A STEREO BUS");
	throw (-1); // no inputs available
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void	OALDevice::SetBusAsAvailable (UInt32 inBusIndex) const
{	
	mBusInfo[inBusIndex].mIsAvailable = true;

#if LOG_BUS_CONNECTIONS
	if (mBusInfo[inBusIndex].mNumberChannels == 1)
		mMonoSourcesConnected--;
	else
		mStereoSourcesConnected--;

		DebugMessageN2("SetBusAsAvailable: Sources Connected, Mono =  %ld, Stereo = %ld\n", mMonoSourcesConnected, mStereoSourcesConnected);
#endif
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void	OALDevice::ResetChannelLayout()
{
	// verify that the channel count has actually changed before doing all this work...
	UInt32	channelCount = GetDesiredRenderChannelCount();

	if (mCurrentMixerChannelCount == channelCount)
		return; // only reset the graph if the channel count has changed

	mCanScheduleEvents = false;

	AUGraphStop (mAUGraph);
	Boolean flag;
	do {
		AUGraphIsRunning (mAUGraph, &flag);
	} while (flag);

	// disconnect the mixer from the  output au
	OSStatus	result = AUGraphDisconnectNodeInput (mAUGraph, mOutputNode, 0);
	// update the graph
	for (;;) {
		result = AUGraphUpdate (mAUGraph, NULL);
		if (result == noErr)
			break;
	}
	
	ConfigureGraphForChannelLayout();
	
	// connect mixer to output unit
	result = AUGraphConnectNodeInput (mAUGraph, mMixerNode, 0, mOutputNode, 0);
		THROW_RESULT
		
	// update the graph
	for (;;) {
		result = AUGraphUpdate (mAUGraph, NULL);
		if (result == noErr)
			break;
	}

	InitRenderQualityOnBusses (mRenderQuality);

	AUGraphStart(mAUGraph);
	
	mCanScheduleEvents = true;

	return;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::GraphFormatPropertyListener (	void *inRefCon, 
													AudioUnit ci, 
													AudioUnitPropertyID inID, 
													AudioUnitScope inScope, 
													AudioUnitElement inElement)
{
	try {
		if (inScope == kAudioUnitScope_Output)
		{
			((OALDevice*)inRefCon)->ResetChannelLayout ();
		}		
	} 
	catch (...) {
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::SetMixerRate (Float64	inSampleRate)
{
	DebugMessage("OALDevice::SetMixerRate called");
	
	if (mMixerOutputRate == inSampleRate)
		return;		// nothing to do	
	
	mCanScheduleEvents = false;
	
	AUGraphDisconnectNodeInput (mAUGraph, mOutputNode, 0);
	OSStatus result;
	for (;;) 
    {
		result = AUGraphUpdate (mAUGraph, NULL);
		if (result == noErr)
			break;
	}

	AudioUnitUninitialize (mMixerUnit);
	
	// reconfigure the graph's mixer...
	CAStreamBasicDescription outFormat;
	UInt32 outSize = sizeof(outFormat);
	result = AudioUnitGetProperty (mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outFormat, &outSize);
		THROW_RESULT
		
	outFormat.mSampleRate = inSampleRate;
	result = AudioUnitSetProperty (mMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outFormat, sizeof(outFormat));
		THROW_RESULT
		
    // lets just do the output unit format
	result = AudioUnitSetProperty (mOutputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outFormat, sizeof(outFormat));
		THROW_RESULT

	AudioUnitInitialize(mMixerUnit);

	result = AUGraphConnectNodeInput (mAUGraph, mMixerNode, 0, mOutputNode, 0);
		THROW_RESULT

	for (;;) 
    {
		result = AUGraphUpdate (mAUGraph, NULL);
		if (result == noErr)
			break;
	}

	mMixerOutputRate = inSampleRate;
	mCanScheduleEvents = true;
}


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::SetRenderChannelCount (UInt32 inRenderChannelCount)
{
    if ((inRenderChannelCount != ALC_RENDER_CHANNEL_COUNT_MULTICHANNEL) && (inRenderChannelCount != ALC_RENDER_CHANNEL_COUNT_STEREO))
		throw (OSStatus) AL_INVALID_VALUE;
    
    if (inRenderChannelCount == mRenderChannelCount)
        return; //nothing to do
        
    mRenderChannelCount = inRenderChannelCount;
    
    if (inRenderChannelCount == ALC_RENDER_CHANNEL_COUNT_STEREO)
    {
        // clamping to stereo
        if (mCurrentMixerChannelCount == 2)
            return; // already rendering to stereo, so there's nothing to do
    }
    else
    {
        // allowing multi channel now
        if (mCurrentMixerChannelCount > 2)
            return; // already rendering to mc, so there's nothing to do
    }    

    // work to be done now, it is necessary to change the channel layout and stream format from multi channel to stereo
    // this requires the graph to be stopped and reconfigured
    ResetChannelLayout ();
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::SetRenderQuality (UInt32 inRenderQuality)
{
	if (mRenderQuality == inRenderQuality)
		return;	// nothing to do;

	// make sure a valid quality setting is requested
	if (!IsValidRenderQuality(inRenderQuality))
		throw (OSStatus) AL_INVALID_VALUE;
			
	// change the spatialization for all mono busses on the mixer
	InitRenderQualityOnBusses(inRenderQuality); 
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void OALDevice::SetReverb(UInt32  inReverbSetting)
{
	if (mReverbSetting == inReverbSetting)
		return;	// nothing to do;

    mReverbSetting = inReverbSetting;
    if (mReverbSetting > 1)
        mReverbSetting = 1;
        
    OSStatus    result = AudioUnitSetProperty(mMixerUnit, kAudioUnitProperty_UsesInternalReverb, kAudioUnitScope_Global, 0, &mReverbSetting, sizeof(mReverbSetting));			
	if (result == noErr)
        InitRenderQualityOnBusses(mRenderQuality); // distance diffusion needs to be reset on the busses now
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bool OALDevice::IsValidRenderQuality (UInt32 inRenderQuality)
{	
	switch (inRenderQuality)
	{
		case ALC_SPATIAL_RENDERING_QUALITY_HIGH:
		case ALC_SPATIAL_RENDERING_QUALITY_LOW:
			return (true);
			break;
			
		default:
			return (false);
			break;
	}

	return (false);
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void    OALDevice::SetDistanceAttenuation(UInt32    inBusIndex, Float64 inRefDist, Float64 inMaxDist, Float64 inRolloff)
{
    if (IsPreferredMixerAvailable())
        return;     // unnecessary with v2.0 mixer
        
    Float64     maxattenuationDB = 20 * log10(inRefDist / (inRefDist + (inRolloff * (inMaxDist - inRefDist))));
    Float64     maxattenuation = pow(10, (maxattenuationDB/20));                    
    Float64     distAttenuation = (log(1/maxattenuation))/(log(inMaxDist)) - 1.0;

#if 0
    DebugMessageN1("SetDistanceAttenuation:Reference Distance =  %f", inRefDist);
    DebugMessageN1("SetDistanceAttenuation:Maximum Distance =  %f", inMaxDist);
    DebugMessageN1("SetDistanceAttenuation:Rolloff =  %f", inRolloff);
    DebugMessageN1("SetDistanceAttenuation:Max Attenuation DB =  %f", maxattenuationDB);
    DebugMessageN1("SetDistanceAttenuation:Max Attenuation Scalar =  %f", maxattenuation);
    DebugMessageN1("SetDistanceAttenuation:distAttenuation =  %f", distAttenuation);

#endif
    
    AudioUnitSetProperty(mMixerUnit, kAudioUnitProperty_3DMixerDistanceAtten, kAudioUnitScope_Input, inBusIndex, &distAttenuation, sizeof(distAttenuation));
    return;
}
