AppleUSBAudioLevelControl.cpp [plain text]
#include <sys/cdefs.h>

__BEGIN_DECLS
#include <kern/thread_call.h>
__END_DECLS

#include "AppleUSBAudioLevelControl.h"
#include "AppleUSBAudioCommon.h"
#include "AppleUSBAudioDevice.h"

#include <IOKit/audio/IOAudioTypes.h>
#include <IOKit/audio/IOAudioDefines.h>
#include <IOKit/usb/USB.h>
#include <IOKit/usb/IOUSBInterface.h>
#include <IOKit/IOLib.h>
#include <IOKit/IOPlatformExpert.h>

#include <libkern/OSByteOrder.h>

#define super IOAudioLevelControl

OSDefineMetaClassAndStructors(AppleUSBAudioLevelControl, IOAudioLevelControl)

AppleUSBAudioLevelControl *AppleUSBAudioLevelControl::create (UInt8 theUnitID, UInt8 theInterfaceNumber, UInt8 theControlSelector, UInt8 theChannelNumber, Boolean shouldUpdatePRAM, USBDeviceRequest theUSBDeviceRequest, void *theCallerRefCon, UInt32 subType, UInt32 usage) {
    AppleUSBAudioLevelControl *			control;

    debug8IOLog ("+AppleUSBAudioLevelControl::create (%d, %d, %d, %d, %p, %lX, %lX)\n", theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, subType, usage);
    control = new AppleUSBAudioLevelControl;
    FailIf (NULL == control, Exit);

    if (FALSE == control->init (theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, shouldUpdatePRAM, theUSBDeviceRequest, theCallerRefCon, subType, usage)) {
        control->release ();
        control = NULL;
    }

Exit:
    debug8IOLog ("-AppleUSBAudioLevelControl::create(%d, %d, %d, %d, %p, %lX, %lX)\n", theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, subType, usage);
    return control;
}

bool AppleUSBAudioLevelControl::init (UInt8 theUnitID, UInt8 theInterfaceNumber, UInt8 theControlSelector, UInt8 theChannelNumber, Boolean shouldUpdatePRAM, USBDeviceRequest theUSBDeviceRequest, void *theCallerRefCon, UInt32 subType, UInt32 usage, OSDictionary *properties) {
	const char *				channelName = NULL;
	SInt16						currentValue;
	SInt16						deviceMin;
	SInt16						deviceMax;
	IOFixed						deviceMinDB;
	IOFixed						deviceMaxDB;
	IOFixed						resolutionDB;
	IOReturn					ret;
	Boolean						result;
	Boolean						arePRAMControl;

	debug8IOLog ("+AppleUSBAudioLevelControl[%p]::init (%d, %d, %d, %d, %p, %p)\n", this, theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, properties);
	result = FALSE;
	arePRAMControl = FALSE;

	FailIf (NULL == theUSBDeviceRequest, Exit);
	setValueThreadCall = thread_call_allocate ((thread_call_func_t)updateValueCallback, this);
	FailIf (NULL == setValueThreadCall, Exit);

	// If this control is supposed to be a pram volume control, pretend we're just a regular volume control
	// so that we can get the min and max dB from the device as we init
	if (theControlSelector == 0xff) {
		theControlSelector = VOLUME_CONTROL;
		arePRAMControl = TRUE;
	}

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) {
		// The iSub controls are on channels 1 and 2, but we want to make them look like they're on channel 0 for the HAL
		// set it to 1 here so we can query the iSub
		theChannelNumber = 1;
	}

	unitID = theUnitID;
	interfaceNumber = theInterfaceNumber;
	controlSelector = theControlSelector;
	channelNumber = theChannelNumber;
	callerRefCon = theCallerRefCon;
	usbDeviceRequest = theUSBDeviceRequest;
	fShouldUpdatePRAM = shouldUpdatePRAM;

	switch (channelNumber) {
		case kIOAudioControlChannelIDAll:
			channelName = kIOAudioControlChannelNameAll;
			break;
		case kIOAudioControlChannelIDDefaultLeft:
			channelName = kIOAudioControlChannelNameLeft;
			break;
		case kIOAudioControlChannelIDDefaultRight:
			channelName = kIOAudioControlChannelNameRight;
			break;
		case 0xff:
			debugIOLog ("++AppleUSBAudioLevelControl: Does not support channel number 0xff.\n");
			return FALSE;
		default:
			channelName = "Unknown";
			break;
	}

	currentValue = GetCurVolume (interfaceNumber, channelNumber, &ret);
	FailIf (kIOReturnSuccess != ret, Exit);
	debug3IOLog ("channelNumber %d, currentValue = 0x%X, ", channelNumber, currentValue);
	volRes = GetVolumeResolution (interfaceNumber, channelNumber, &ret);
	FailIf (kIOReturnSuccess != ret, Exit);
	debug2IOLog ("vol res = %d, ", volRes);
	deviceMin = GetMinVolume (interfaceNumber, channelNumber, &ret);
	FailIf (kIOReturnSuccess != ret, Exit);
	debug2IOLog ("deviceMin = 0x%X, ", deviceMin);
	deviceMax = GetMaxVolume (interfaceNumber, channelNumber, &ret);
	FailIf (kIOReturnSuccess != ret, Exit);
	debug2IOLog ("deviceMax = 0x%X\n", deviceMax);

	// Having the device say that it does -infinity dB messes up our math, so set the min at -127.9961dB instead.
	if ((SInt16)0x8000 == deviceMin) {
		deviceMin = (SInt16)0x8001;
		debug2IOLog ("deviceMin adjusted to = %d\n", deviceMin);
	}

	deviceMinDB = ConvertUSBVolumeTodB (deviceMin);
	deviceMaxDB = ConvertUSBVolumeTodB (deviceMax);
	resolutionDB = ConvertUSBVolumeTodB (volRes);		// The volume is incremented in units of this many dB, represented as 1/256 dB (eg 256 == 1dB of control)

	offset = -deviceMin;
	debug2IOLog ("offset = %d\n", offset);

	currentValue = (currentValue + offset) / volRes;
	deviceMax = ((deviceMin + offset) + (deviceMax + offset)) / volRes;

	// Set values needed to compute proper PRAM boot beep volume setting
	fMaxVolume = deviceMax;
	fMinVolume = deviceMin + offset;
	deviceMin = -1;

	if (arePRAMControl) {
		// If this is a 'pram' control then there is no need to call the hardware.
		UInt8 						curPRAMVol;
		IODTPlatformExpert * 		platform = NULL;

		curPRAMVol = 0;
		platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());
		if (NULL != platform) {
			platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
			curPRAMVol = (curPRAMVol & 0xF8);
		}
		currentValue = curPRAMVol;
		deviceMin = 0;
		deviceMax = 7;
		subType = kIOAudioLevelControlSubTypePRAMVolume;
		channelName = kIOAudioControlChannelNameAll;
		theChannelNumber = 0;	// force it to the master channel even though we're piggy backing off of the channel 1 control
	}

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) {
		// The iSub controls are on channels 1 and 2, but we want to make them look like they're on channel 0 for the HAL
		theChannelNumber = 0;
		channelName = kIOAudioControlChannelNameAll;
	}

//	debug4IOLog ("min = %d, max = %d, current = %d\n", (deviceMin + offset) - 1, deviceMax, currentValue);
	debug4IOLog ("min = %d, max = %d, current = %d\n", deviceMin, deviceMax, currentValue);

	FailIf (FALSE == super::init (currentValue, deviceMin, deviceMax, deviceMinDB, deviceMaxDB, theChannelNumber, channelName, 0, subType, usage), Exit);

	result = TRUE;

Exit:
	debug8IOLog ("-AppleUSBAudioLevelControl[%p]::init (%d, %d, %d, %d, %p, %p)\n", this, theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, properties);
	return result;
}

void AppleUSBAudioLevelControl::free () {
    debug2IOLog ("+AppleUSBAudioLevelControl[%p]::free ()\n", this);

    if (setValueThreadCall) {
        thread_call_free (setValueThreadCall);
        setValueThreadCall = NULL;
    }

    super::free ();
    debug2IOLog ("-AppleUSBAudioLevelControl[%p]::free ()\n", this);
}

IOReturn AppleUSBAudioLevelControl::performValueChange (OSObject * newValue) {
	OSNumber *					newValueAsNumber;
	SInt32						newValueAsSInt32;

    debug3IOLog ("+AppleUSBAudioLevelControl[%p]::performValueChange (%d)\n", this, newValue); 

	newValueAsNumber = OSDynamicCast (OSNumber, newValue);
	FailIf (NULL == newValueAsNumber, Exit);
	newValueAsSInt32 = newValueAsNumber->unsigned32BitValue ();
	debug3IOLog ("++AppleUSBAudioLevelControl[%p]::performValueChange (%ld)\n", this, newValueAsSInt32);

    if (NULL != setValueThreadCall) {
        thread_call_enter1 (setValueThreadCall, (thread_call_param_t)newValueAsSInt32);
    }

    debug3IOLog ("-AppleUSBAudioLevelControl[%p]::performValueChange (%d)\n", this, newValueAsSInt32);

Exit:
    return kIOReturnSuccess;
}

void AppleUSBAudioLevelControl::updateUSBValue () {
    updateUSBValue (getIntValue ());
}

void AppleUSBAudioLevelControl::updateUSBValue (SInt32 newValue) {
//	SInt32						newiSubVolume;
    SInt16						theValue;
	SInt16						newVolume;
    IOReturn					ret;

    debug3IOLog ("+AppleUSBAudioLevelControl[%p]::updateUSBValue (%d)\n", this, newValue);

	if (newValue < 0) {
		newVolume = 0x8000;
	} else {
		newVolume = (newValue * volRes) - offset;
	}
    theValue = HostToUSBWord (newVolume);
	debug2IOLog ("setting volume to 0x%X\n", newVolume);
	ret = SetCurVolume (interfaceNumber, channelNumber, theValue);

	if (getSubType () == kIOAudioLevelControlSubTypeLFEVolume) {
		// We set the iSub's left volume (on channel 1 above), now set it on channel 2 to mimic having only a master volume control
		ret = SetCurVolume (interfaceNumber, 2, theValue);
	}

	if (TRUE == fShouldUpdatePRAM && FALSE == gExpertMode) {				// We do that only if we are on a OS 9 like UI guideline
		WritePRAMVol (newValue, newValue);
	}
#if 0
	// If this is the iSub volume control, don't bother to look for ourselves, which would probably result in an infinite loop anyway.
	if (getSubType () != kIOAudioLevelControlSubTypeLFEVolume && getUsage () == kIOAudioControlUsageOutput) {
		IOAudioLevelControl *	iSubVolume;
		IORegistryEntry *		start;
		IORegistryEntry *		parent;
		IORegistryEntry *		engine;

		iSubVolume = NULL;
		debugIOLog ("Looking for iSub volume control\n");
		start = getParentEntry (gIOServicePlane);
		FailIf (NULL == start, Exit);
		parent = start->getParentEntry (gIOServicePlane);
		FailIf (NULL == parent, Exit);
		engine = parent->childFromPath ("AppleUSBAudioEngine", gIOServicePlane);
		FailIf (NULL == engine, Exit);

		iSubVolume = (IOAudioLevelControl *)FindEntryByNameAndProperty (engine, "AppleUSBAudioLevelControl", kIOAudioControlSubTypeKey, kIOAudioLevelControlSubTypeLFEVolume);

		engine->release ();

		if (NULL != iSubVolume) {
			newiSubVolume = ((newValue * kiSubMaxVolume) / getMaxValue ()) * kiSubVolumePercent / 100;
			debugIOLog ("Setting the iSub volume control\n");
			iSubVolume->setValue (newiSubVolume);
			iSubVolume->release ();
		}
	}
#endif
//Exit:
    if (ret != kIOReturnSuccess) {
        debug4IOLog ("++AppleUSBAudioLevelContol:updateUSBValue () - set current value for %d:%d failed: 0x%X\n", controlSelector, channelNumber, ret);
    }

    debug3IOLog ("-AppleUSBAudioLevelControl[%p]::updateUSBValue (%d)\n", this, newValue);
}

SInt16 AppleUSBAudioLevelControl::GetCurVolume (UInt8 interfaceNumber, UInt8 channelNumber, IOReturn * error) {
    IOUSBDevRequest				devReq;
    SInt16						theVolume;

    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_CUR;
    devReq.wValue = (controlSelector << 8) | channelNumber;
    devReq.wIndex = (unitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	*error = usbDeviceRequest (&devReq, callerRefCon);
    FailIf (kIOReturnSuccess != *error, Error);

Exit:
    return USBToHostWord (theVolume);
Error:
	theVolume = 0;
	goto Exit;
}

SInt16 AppleUSBAudioLevelControl::GetMaxVolume (UInt8 interfaceNumber, UInt8 channelNumber, IOReturn * error) {
    IOUSBDevRequest				devReq;
    SInt16						theVolume;

    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_MAX;
    devReq.wValue = (controlSelector << 8) | channelNumber;
    devReq.wIndex = (unitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	*error = usbDeviceRequest (&devReq, callerRefCon);
    FailIf (kIOReturnSuccess != *error, Error);

Exit:
    return USBToHostWord (theVolume);
Error:
	theVolume = 0;
	goto Exit;
}

SInt16 AppleUSBAudioLevelControl::GetMinVolume (UInt8 interfaceNumber, UInt8 channelNumber, IOReturn * error) {
    IOUSBDevRequest				devReq;
    SInt16						theVolume;

    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_MIN;
    devReq.wValue = (controlSelector << 8) | channelNumber;
    devReq.wIndex = (unitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	*error = usbDeviceRequest (&devReq, callerRefCon);
    FailIf (kIOReturnSuccess != *error, Error);

Exit:
    return USBToHostWord (theVolume);
Error:
	theVolume = 0;
	goto Exit;
}

UInt16 AppleUSBAudioLevelControl::GetVolumeResolution (UInt8 interfaceNumber, UInt8 channelNumber, IOReturn * error) {
    IOUSBDevRequest				devReq;
    UInt16						theResolution;

    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_RES;
    devReq.wValue = (controlSelector << 8) | channelNumber;
    devReq.wIndex = (unitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theResolution;

	*error = usbDeviceRequest (&devReq, callerRefCon);
    FailIf (kIOReturnSuccess != *error, Error);

Exit:
    return USBToHostWord (theResolution);
Error:
	theResolution = 0;
	goto Exit;
}

IOReturn AppleUSBAudioLevelControl::SetCurVolume (UInt8 interfaceNumber, UInt8 channelNumber, SInt16 volume) {
    IOUSBDevRequest				devReq;
	IOReturn					error;

    devReq.bmRequestType = USBmakebmRequestType (kUSBOut, kUSBClass, kUSBInterface);
    devReq.bRequest = SET_CUR;
    devReq.wValue = (controlSelector << 8) | channelNumber;
    devReq.wIndex = (unitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &volume;

	FailIf ((TRUE == isInactive()), DeviceInactive);  	// In case we've been unplugged during sleep
	error = usbDeviceRequest (&devReq, callerRefCon);
    FailIf (kIOReturnSuccess != error, Exit);

Exit:
    return error;
	
DeviceInactive:
	debugIOLog("AppleUSBAudioLevelControl::SetCurVolume ERROR attempt to send a device request to and inactive device\n");
	error = kIOReturnError;
	goto Exit;
}

void AppleUSBAudioLevelControl::updateValueCallback (void *arg1, void *arg2) {
    AppleUSBAudioLevelControl 		*levelControl;
    SInt32							value;
	UInt32							subType;

    debug3IOLog ("+AppleUSBAudioLevelControl::updateValueCallback (%p, %p)\n", (UInt32*)arg1, (UInt32*)arg2);
    levelControl = OSDynamicCast (AppleUSBAudioLevelControl, (OSObject*)arg1);
    value = (SInt32)arg2;

    if (levelControl) {
		subType = levelControl->getSubType ();
		
		if (kIOAudioLevelControlSubTypePRAMVolume == subType) {
			UInt8 						curPRAMVol;
			IODTPlatformExpert * 		platform = NULL;

			platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());
			if (NULL != platform) {
				platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
				curPRAMVol = (curPRAMVol & 0xF8) | value;
				platform->writeXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
			}
		} else {
			levelControl->updateUSBValue (value);
		}
    }

    debug3IOLog ("-AppleUSBAudioLevelControl::updateValueCallback (%p, %p)\n", (UInt32*)arg1, (UInt32*)arg2);
}

// This is how the thing is defined in the USB Audio spec (section 5.2.2.4.3.2 for the curious).
// The volume setting of a device is described in 1/256 dB increments using a number that goes from
// a max of 0x7fff (127.9961 dB) down to 0x8001 (-127.9961 dB) using standard signed math, but 0x8000
// is actually negative infinity (not -128 dB), so I have to special case it.
IOFixed AppleUSBAudioLevelControl::ConvertUSBVolumeTodB (SInt16 volume) {
	IOFixed							dBVolumeFixed;

	if (volume == (SInt16)0x8000) {
		dBVolumeFixed = ((SInt16)0x8000 * 256) << 8;	// really is negative infinity
	} else {
		dBVolumeFixed = volume * 256;
	}

	debug3IOLog ("volume = %d, dBVolumeFixed = 0x%X\n", volume, dBVolumeFixed);

	return dBVolumeFixed;
}
#if 0
IORegistryEntry * AppleUSBAudioLevelControl::FindEntryByNameAndProperty (const IORegistryEntry * start, const char * name, const char * key, UInt32 value) {
	OSIterator				*iterator;
	IORegistryEntry			*theEntry;
	IORegistryEntry			*tmpReg;
	OSNumber				*tmpNumber;

	theEntry = NULL;
	iterator = NULL;
	FailIf (NULL == start, Exit);

	iterator = start->getChildIterator (gIOServicePlane);
	FailIf (NULL == iterator, Exit);

	while (NULL == theEntry && (tmpReg = OSDynamicCast (IORegistryEntry, iterator->getNextObject ())) != NULL) {
		if (strcmp (tmpReg->getName (), name) == 0) {
			tmpNumber = OSDynamicCast (OSNumber, tmpReg->getProperty (key));
			if (NULL != tmpNumber && tmpNumber->unsigned32BitValue () == value) {
				theEntry = tmpReg;
				theEntry->retain ();
			}
		}
	}

Exit:
	if (NULL != iterator) {
		iterator->release ();
	}
	return theEntry;
}
#endif
#pragma mark +PRAM VOLUME
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Calculates the PRAM volume value for stereo volume.
UInt8 AppleUSBAudioLevelControl::VolumeToPRAMValue (SInt32 leftVol, SInt32 rightVol) {
	UInt32			pramVolume;						// Volume level to store in PRAM
	UInt32 			averageVolume;					// summed volume
	const UInt32 	volumeRange = (fMaxVolume - fMinVolume + 1);
	UInt32 			volumeSteps;

	if (leftVol < 0)
		leftVol = 0;
	if (rightVol < 0)
		rightVol = 0;

	averageVolume = (leftVol + rightVol) >>  1;		// sum the channel volumes and get an average
	volumeSteps = volumeRange / kMaximumPRAMVolume;	// divide the range by the range of the pramVolume
	pramVolume = averageVolume / volumeSteps;    

	// Since the volume level in PRAM is only worth three bits,
	// we round small values up to 1. This avoids SysBeep from
	// flashing the menu bar when it thinks sound is off and
	// should really just be very quiet.

	if ((pramVolume == 0) && (leftVol != 0 || rightVol !=0 )) {
		pramVolume = 1;
	}

	return (pramVolume & 0x07);
}

void AppleUSBAudioLevelControl::WritePRAMVol (SInt32 leftVol, SInt32 rightVol) {
	UInt8						pramVolume;
	UInt8 						curPRAMVol;
	IODTPlatformExpert * 		platform = NULL;

	platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());

    debug3IOLog ("AppleUSBTrinityAudioDevice::WritePRAMVol leftVol=%lu, rightVol=%lu\n",leftVol,  rightVol);

    if (NULL != platform) {
		pramVolume = VolumeToPRAMValue (leftVol, rightVol);

		// get the old value to compare it with
		platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);

		// Update only if there is a change
		if (pramVolume != (curPRAMVol & 0x07)) {
			// clear bottom 3 bits of volume control byte from PRAM low memory image
			curPRAMVol = (curPRAMVol & 0xF8) | pramVolume;
            debug2IOLog ("AppleUSBAudioLevelControl::WritePRAMVol curPRAMVol=0x%x\n", curPRAMVol);

			// write out the volume control byte to PRAM
			platform->writeXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount) 1);
		}
	}
}

Generated by GNU enscript 1.6.4.