1
1
mirror of https://github.com/bitgapp/eqMac.git synced 2024-11-30 03:35:04 +03:00
eqMac/native/driver/Source/EQM_Device.cpp
2020-10-11 16:30:15 +03:00

2040 lines
81 KiB
C++

//
// EQM_Device.cpp
// EQMDriver
//
// Copyright © 2017 Andrew Tonner
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Based largely on SA_Device.cpp from Apple's SimpleAudioDriver Plug-In sample code. Also uses a few sections from Apple's
// NullAudio.c sample code (found in the same sample project).
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
//
// Self Include
#include "EQM_Device.h"
// Local Includes
#include "EQM_PlugIn.h"
#include "EQM_XPCHelper.h"
#include "EQM_Utils.h"
// PublicUtility Includes
#include "CADispatchQueue.h"
#include "CAException.h"
#include "CACFArray.h"
#include "CACFString.h"
#include "CADebugMacros.h"
#include "CAHostTimeBase.h"
// STL Includes
#include <stdexcept>
// System Includes
#include <mach/mach_time.h>
#include <CoreAudio/AudioHardwareBase.h>
#pragma mark Construction/Destruction
pthread_once_t EQM_Device::sStaticInitializer = PTHREAD_ONCE_INIT;
EQM_Device* EQM_Device::sInstance = nullptr;
EQM_Device* EQM_Device::sUISoundsInstance = nullptr;
EQM_Device& EQM_Device::GetInstance()
{
pthread_once(&sStaticInitializer, StaticInitializer);
return *sInstance;
}
EQM_Device& EQM_Device::GetUISoundsInstance()
{
pthread_once(&sStaticInitializer, StaticInitializer);
return *sUISoundsInstance;
}
void EQM_Device::StaticInitializer()
{
try
{
// The main instance, usually referred to in the code as "EQMDevice". This is the device
// that appears in System Preferences as "Background Music".
sInstance = new EQM_Device(kObjectID_Device,
CFSTR(kDeviceName),
CFSTR(kEQMDeviceUID),
CFSTR(kEQMDeviceModelUID),
kObjectID_Stream_Input,
kObjectID_Stream_Output,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Output_Master);
sInstance->Activate();
// if (
// !EQM_Utils::process_at_path_running("/Applications/eqMac.app/Contents/MacOS/eqMac")
// && !EQM_Utils::process_at_path_running("/Applications/Xcode.app/Contents/MacOS/Xcode")
// ) {
// // Activate the Driver only if eqMac or Xcode are running from /Application folder
// sInstance->Deactivate();
// } else {
// }
// The instance for system (UI) sounds.
sUISoundsInstance = new EQM_Device(kObjectID_Device_UI_Sounds,
CFSTR(kDeviceName_UISounds),
CFSTR(kEQMDeviceUID_UISounds),
CFSTR(kEQMDeviceModelUID_UISounds),
kObjectID_Stream_Input_UI_Sounds,
kObjectID_Stream_Output_UI_Sounds,
kObjectID_Volume_Output_Master_UI_Sounds,
kAudioObjectUnknown); // No mute control.
// Set up the UI sounds device's volume control.
EQM_VolumeControl& theUISoundsVolumeControl = sUISoundsInstance->mVolumeControl;
// Default to full volume.
theUISoundsVolumeControl.SetVolumeScalar(1.0f);
// Make the volume curve a bit steeper than the default.
theUISoundsVolumeControl.GetVolumeCurve().SetTransferFunction(CAVolumeCurve::kPow4Over1Curve);
// Apply the volume to the device's output stream. The main instance of EQM_Device doesn't
// apply volume to its audio because EQMApp changes the real output device's volume directly
// instead.
theUISoundsVolumeControl.SetWillApplyVolumeToAudio(true);
// sUISoundsInstance->Activate();
}
catch(...)
{
DebugMsg("EQM_Device::StaticInitializer: failed to create the devices");
delete sInstance;
sInstance = nullptr;
delete sUISoundsInstance;
sUISoundsInstance = nullptr;
}
}
EQM_Device::EQM_Device(AudioObjectID inObjectID,
const CFStringRef __nonnull inDeviceName,
const CFStringRef __nonnull inDeviceUID,
const CFStringRef __nonnull inDeviceModelUID,
AudioObjectID inInputStreamID,
AudioObjectID inOutputStreamID,
AudioObjectID inOutputVolumeControlID,
AudioObjectID inOutputMuteControlID)
:
EQM_AbstractDevice(inObjectID, kAudioObjectPlugInObject),
mStateMutex("Device State"),
mIOMutex("Device IO"),
mDeviceName(inDeviceName),
mDeviceUID(inDeviceUID),
mDeviceModelUID(inDeviceModelUID),
mWrappedAudioEngine(nullptr),
mClients(inObjectID, &mTaskQueue),
mInputStream(inInputStreamID, inObjectID, false, kSampleRateDefault),
mOutputStream(inOutputStreamID, inObjectID, false, kSampleRateDefault),
mAudibleState(),
mVolumeControl(inOutputVolumeControlID, GetObjectID(), kAudioObjectPropertyScopeOutput),
mMuteControl(inOutputMuteControlID, GetObjectID(), kAudioObjectPropertyScopeOutput)
{
// Initialises the loopback clock with the default sample rate and, if there is one, sets the wrapped device to the same sample rate
SetSampleRate(kSampleRateDefault);
SetSafetyOffset(kSafetyOffsetDefault);
SetLatency(kLatencyDefault);
SetShown(kShownDefault);
}
EQM_Device::~EQM_Device()
{
}
void EQM_Device::Activate()
{
CAMutex::Locker theStateLocker(mStateMutex);
// Open the connection to the driver and initialize things.
//_HW_Open();
mInputStream.Activate();
mOutputStream.Activate();
if(mVolumeControl.GetObjectID() != kAudioObjectUnknown)
{
mVolumeControl.Activate();
}
if(mMuteControl.GetObjectID() != kAudioObjectUnknown)
{
mMuteControl.Activate();
}
// Call the super-class, which just marks the object as active
EQM_AbstractDevice::Activate();
}
void EQM_Device::Deactivate()
{
// When this method is called, the object is basically dead, but we still need to be thread
// safe. In this case, we also need to be safe vs. any IO threads, so we need to take both
// locks.
CAMutex::Locker theStateLocker(mStateMutex);
CAMutex::Locker theIOLocker(mIOMutex);
// Mark the device's sub-objects inactive.
mInputStream.Deactivate();
mOutputStream.Deactivate();
mVolumeControl.Deactivate();
mMuteControl.Deactivate();
// mark the object inactive by calling the super-class
EQM_AbstractDevice::Deactivate();
// close the connection to the driver
//_HW_Close();
}
void EQM_Device::InitLoopback()
{
// Calculate the number of host clock ticks per frame for our loopback clock.
mLoopbackTime.hostTicksPerFrame = CAHostTimeBase::GetFrequency() / mLoopbackSampleRate;
// Zero-out the loopback buffer
// 2 channels * 32-bit float = bytes in each frame
memset(mLoopbackRingBuffer, 0, sizeof(Float32) * 2 * kLoopbackRingBufferFrameSize);
}
#pragma mark Property Operations
bool EQM_Device::HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const
{
// This object owns several API-level objects. So the first thing to do is to figure out
// which object this request is really for. Note that mObjectID is an invariant as this
// driver's structure does not change dynamically. It will always have the parts it has.
bool theAnswer = false;
if(inObjectID == mObjectID)
{
theAnswer = Device_HasProperty(inObjectID, inClientPID, inAddress);
}
else
{
theAnswer = GetOwnedObjectByID(inObjectID).HasProperty(inObjectID, inClientPID, inAddress);
}
return theAnswer;
}
bool EQM_Device::IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const
{
bool theAnswer = false;
if(inObjectID == mObjectID)
{
theAnswer = Device_IsPropertySettable(inObjectID, inClientPID, inAddress);
}
else
{
theAnswer = GetOwnedObjectByID(inObjectID).IsPropertySettable(inObjectID, inClientPID, inAddress);
}
return theAnswer;
}
UInt32 EQM_Device::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const
{
UInt32 theAnswer = 0;
if(inObjectID == mObjectID)
{
theAnswer = Device_GetPropertyDataSize(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData);
}
else
{
theAnswer = GetOwnedObjectByID(inObjectID).GetPropertyDataSize(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData);
}
return theAnswer;
}
void EQM_Device::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* outData) const
{
ThrowIfNULL(outData, std::runtime_error("!outData"), "EQM_Device::GetPropertyData: !outData");
if(inObjectID == mObjectID)
{
Device_GetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, outDataSize, outData);
}
else
{
GetOwnedObjectByID(inObjectID).GetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, outDataSize, outData);
}
}
void EQM_Device::SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData)
{
ThrowIfNULL(inData, std::runtime_error("no data"), "EQM_Device::SetPropertyData: no data");
if(inObjectID == mObjectID)
{
Device_SetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, inData);
}
else
{
GetOwnedObjectByID(inObjectID).SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
if(IsStreamID(inObjectID))
{
// When one of the stream's sample rate changes, set the new sample rate for both
// streams and the device. The streams check the new format before this point but don't
// change until the device tells them to, as it has to get the host to pause IO first.
if(inAddress.mSelector == kAudioStreamPropertyVirtualFormat ||
inAddress.mSelector == kAudioStreamPropertyPhysicalFormat)
{
const AudioStreamBasicDescription* theNewFormat =
reinterpret_cast<const AudioStreamBasicDescription*>(inData);
RequestSampleRate(theNewFormat->mSampleRate);
}
}
}
}
#pragma mark Device Property Operations
bool EQM_Device::Device_HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// Device_GetPropertyData() method.
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioDevicePropertyStreams:
case kAudioDevicePropertyIcon:
case kAudioObjectPropertyCustomPropertyInfoList:
case kAudioDeviceCustomPropertyDeviceAudibleState:
case kAudioDeviceCustomPropertyMusicPlayerProcessID:
case kAudioDeviceCustomPropertyMusicPlayerBundleID:
case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp:
case kAudioDeviceCustomPropertyAppVolumes:
case kAudioDeviceCustomPropertyEnabledOutputControls:
case kAudioDevicePropertySafetyOffset:
case kAudioDeviceCustomPropertyLatency:
case kAudioDeviceCustomPropertySafetyOffset:
case kAudioDeviceCustomPropertyShown:
case kAudioDeviceCustomPropertyVersion:
theAnswer = true;
break;
case kAudioDevicePropertyPreferredChannelsForStereo:
case kAudioDevicePropertyPreferredChannelLayout:
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
theAnswer = (inAddress.mScope == kAudioObjectPropertyScopeInput) || (inAddress.mScope == kAudioObjectPropertyScopeOutput);
break;
default:
theAnswer = EQM_AbstractDevice::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool EQM_Device::Device_IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// Device_GetPropertyData() method.
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioDevicePropertyStreams:
case kAudioDevicePropertyPreferredChannelsForStereo:
case kAudioDevicePropertyPreferredChannelLayout:
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
case kAudioDevicePropertyIcon:
case kAudioDevicePropertyLatency:
case kAudioDevicePropertySafetyOffset:
case kAudioObjectPropertyCustomPropertyInfoList:
case kAudioDeviceCustomPropertyDeviceAudibleState:
case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp:
case kAudioDeviceCustomPropertyVersion:
theAnswer = false;
break;
case kAudioDevicePropertyNominalSampleRate:
case kAudioDeviceCustomPropertyMusicPlayerProcessID:
case kAudioDeviceCustomPropertyMusicPlayerBundleID:
case kAudioDeviceCustomPropertyAppVolumes:
case kAudioDeviceCustomPropertyEnabledOutputControls:
case kAudioDeviceCustomPropertyLatency:
case kAudioDeviceCustomPropertySafetyOffset:
case kAudioDeviceCustomPropertyShown:
theAnswer = true;
break;
default:
theAnswer = EQM_AbstractDevice::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 EQM_Device::Device_GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// Device_GetPropertyData() method.
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyOwnedObjects:
{
switch(inAddress.mScope)
{
case kAudioObjectPropertyScopeGlobal:
theAnswer = GetNumberOfSubObjects() * sizeof(AudioObjectID);
break;
case kAudioObjectPropertyScopeInput:
theAnswer = kNumberOfInputSubObjects * sizeof(AudioObjectID);
break;
case kAudioObjectPropertyScopeOutput:
theAnswer = kNumberOfOutputStreams * sizeof(AudioObjectID);
theAnswer += GetNumberOfOutputControls() * sizeof(AudioObjectID);
break;
default:
break;
};
}
break;
case kAudioDevicePropertyStreams:
{
switch(inAddress.mScope)
{
case kAudioObjectPropertyScopeGlobal:
theAnswer = kNumberOfStreams * sizeof(AudioObjectID);
break;
case kAudioObjectPropertyScopeInput:
theAnswer = kNumberOfInputStreams * sizeof(AudioObjectID);
break;
case kAudioObjectPropertyScopeOutput:
theAnswer = kNumberOfOutputStreams * sizeof(AudioObjectID);
break;
default:
break;
};
}
break;
case kAudioObjectPropertyControlList:
theAnswer = GetNumberOfOutputControls() * sizeof(AudioObjectID);
break;
case kAudioDevicePropertyAvailableNominalSampleRates:
theAnswer = 1 * sizeof(AudioValueRange);
break;
case kAudioDevicePropertyPreferredChannelsForStereo:
theAnswer = 2 * sizeof(UInt32);
break;
case kAudioDevicePropertyPreferredChannelLayout:
theAnswer = offsetof(AudioChannelLayout, mChannelDescriptions) + (2 * sizeof(AudioChannelDescription));
break;
case kAudioDevicePropertyIcon:
theAnswer = sizeof(CFURLRef);
break;
case kAudioObjectPropertyCustomPropertyInfoList:
theAnswer = sizeof(AudioServerPlugInCustomPropertyInfo) * 10;
break;
case kAudioDeviceCustomPropertyDeviceAudibleState:
theAnswer = sizeof(CFNumberRef);
break;
case kAudioDeviceCustomPropertyMusicPlayerProcessID:
theAnswer = sizeof(CFPropertyListRef);
break;
case kAudioDeviceCustomPropertyMusicPlayerBundleID:
theAnswer = sizeof(CFStringRef);
break;
case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp:
theAnswer = sizeof(CFBooleanRef);
break;
case kAudioDeviceCustomPropertyAppVolumes:
theAnswer = sizeof(CFPropertyListRef);
break;
case kAudioDeviceCustomPropertyEnabledOutputControls:
theAnswer = sizeof(CFArrayRef);
break;
case kAudioDeviceCustomPropertyLatency:
theAnswer = sizeof(CFNumberRef);
break;
case kAudioDeviceCustomPropertySafetyOffset:
theAnswer = sizeof(CFNumberRef);
break;
case kAudioDeviceCustomPropertyShown:
theAnswer = sizeof(CFBooleanRef);
break;
case kAudioDeviceCustomPropertyVersion:
theAnswer = sizeof(CFStringRef);
break;
default:
theAnswer = EQM_AbstractDevice::GetPropertyDataSize(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData);
break;
};
return theAnswer;
}
void EQM_Device::Device_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* outData) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required.
// Also, since most of the data that will get returned is static, there are few instances where
// it is necessary to lock the state mutex.
UInt32 theNumberItemsToFetch;
UInt32 theItemIndex;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
// This is the human readable name of the device. Note that in this case we return a
// value that is a key into the localizable strings in this bundle. This allows us to
// return a localized name for the device.
ThrowIf(inDataSize < sizeof(AudioObjectID), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioObjectPropertyName for the device");
*reinterpret_cast<CFStringRef*>(outData) = mDeviceName;
outDataSize = sizeof(CFStringRef);
break;
case kAudioObjectPropertyManufacturer:
// This is the human readable name of the maker of the plug-in. Note that in this case
// we return a value that is a key into the localizable strings in this bundle. This
// allows us to return a localized name for the manufacturer.
ThrowIf(inDataSize < sizeof(AudioObjectID), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioObjectPropertyManufacturer for the device");
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kDeviceManufacturerName);
outDataSize = sizeof(CFStringRef);
break;
case kAudioObjectPropertyOwnedObjects:
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
theNumberItemsToFetch = inDataSize / sizeof(AudioObjectID);
// The device owns its streams and controls. Note that what is returned here
// depends on the scope requested.
switch(inAddress.mScope)
{
case kAudioObjectPropertyScopeGlobal:
// global scope means return all objects
{
CAMutex::Locker theStateLocker(mStateMutex);
if(theNumberItemsToFetch > GetNumberOfSubObjects())
{
theNumberItemsToFetch = GetNumberOfSubObjects();
}
// fill out the list with as many objects as requested, which is everything
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mInputStream.GetObjectID();
}
if(theNumberItemsToFetch > 1)
{
reinterpret_cast<AudioObjectID*>(outData)[1] = mOutputStream.GetObjectID();
}
// If at least one of the controls is enabled, and there's room, return one.
if(theNumberItemsToFetch > 2)
{
if(mVolumeControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[2] = mVolumeControl.GetObjectID();
}
else if(mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[2] = mMuteControl.GetObjectID();
}
}
// If both controls are enabled, and there's room, return the mute control as well.
if(theNumberItemsToFetch > 3 && mVolumeControl.IsActive() && mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[3] = mMuteControl.GetObjectID();
}
}
break;
case kAudioObjectPropertyScopeInput:
// input scope means just the objects on the input side
if(theNumberItemsToFetch > kNumberOfInputSubObjects)
{
theNumberItemsToFetch = kNumberOfInputSubObjects;
}
// fill out the list with the right objects
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mInputStream.GetObjectID();
}
break;
case kAudioObjectPropertyScopeOutput:
// output scope means just the objects on the output side
{
CAMutex::Locker theStateLocker(mStateMutex);
if(theNumberItemsToFetch > GetNumberOfOutputControls())
{
theNumberItemsToFetch = GetNumberOfOutputControls();
}
// fill out the list with the right objects
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mOutputStream.GetObjectID();
}
// If at least one of the controls is enabled, and there's room, return one.
if(theNumberItemsToFetch > 1)
{
if(mVolumeControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[1] = mVolumeControl.GetObjectID();
}
else if(mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[1] = mMuteControl.GetObjectID();
}
}
// If both controls are enabled, and there's room, return the mute control as well.
if(theNumberItemsToFetch > 2 && mVolumeControl.IsActive() && mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[2] = mMuteControl.GetObjectID();
}
}
break;
};
// report how much we wrote
outDataSize = theNumberItemsToFetch * sizeof(AudioObjectID);
break;
case kAudioDevicePropertyDeviceUID:
// This is a CFString that is a persistent token that can identify the same
// audio device across boot sessions. Note that two instances of the same
// device must have different values for this property.
ThrowIf(inDataSize < sizeof(AudioObjectID), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyDeviceUID for the device");
*reinterpret_cast<CFStringRef*>(outData) = mDeviceUID;
outDataSize = sizeof(CFStringRef);
break;
case kAudioDevicePropertyModelUID:
// This is a CFString that is a persistent token that can identify audio
// devices that are the same kind of device. Note that two instances of the
// save device must have the same value for this property.
ThrowIf(inDataSize < sizeof(AudioObjectID), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyModelUID for the device");
*reinterpret_cast<CFStringRef*>(outData) = mDeviceModelUID;
outDataSize = sizeof(CFStringRef);
break;
case kAudioDevicePropertyDeviceIsRunning:
// This property returns whether or not IO is running for the device.
ThrowIf(inDataSize < sizeof(UInt32), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyDeviceIsRunning for the device");
*reinterpret_cast<UInt32*>(outData) = mClients.ClientsRunningIO() ? 1 : 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
// See EQM_AbstractDevice::GetPropertyData.
//
// We don't allow the UI Sounds instance of EQM_Device to be set as the default device
// so that it doesn't appear in the list of devices, which would just be confusing to
// users. (And it wouldn't make sense to set it as the default device anyway.)
//
// Instead, EQMApp sets the UI Sounds device as the "system default" (see
// kAudioDevicePropertyDeviceCanBeDefaultSystemDevice) so apps will use it for
// UI-related sounds.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceCanBeDefaultDevice for the device");
// TODO: Add a field for this and set it in EQM_Device::StaticInitializer so we don't
// have to handle a specific instance differently here.
*reinterpret_cast<UInt32*>(outData) = mShown ? 1 : 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyStreams:
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
theNumberItemsToFetch = inDataSize / sizeof(AudioObjectID);
// Note that what is returned here depends on the scope requested.
switch(inAddress.mScope)
{
case kAudioObjectPropertyScopeGlobal:
// global scope means return all streams
if(theNumberItemsToFetch > kNumberOfStreams)
{
theNumberItemsToFetch = kNumberOfStreams;
}
// fill out the list with as many objects as requested
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mInputStream.GetObjectID();
}
if(theNumberItemsToFetch > 1)
{
reinterpret_cast<AudioObjectID*>(outData)[1] = mOutputStream.GetObjectID();
}
break;
case kAudioObjectPropertyScopeInput:
// input scope means just the objects on the input side
if(theNumberItemsToFetch > kNumberOfInputStreams)
{
theNumberItemsToFetch = kNumberOfInputStreams;
}
// fill out the list with as many objects as requested
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mInputStream.GetObjectID();
}
break;
case kAudioObjectPropertyScopeOutput:
// output scope means just the objects on the output side
if(theNumberItemsToFetch > kNumberOfOutputStreams)
{
theNumberItemsToFetch = kNumberOfOutputStreams;
}
// fill out the list with as many objects as requested
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mOutputStream.GetObjectID();
}
break;
};
// report how much we wrote
outDataSize = theNumberItemsToFetch * sizeof(AudioObjectID);
break;
case kAudioObjectPropertyControlList:
{
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list, in which
// case only that many items will be returned.
theNumberItemsToFetch = inDataSize / sizeof(AudioObjectID);
if(theNumberItemsToFetch > 2)
{
theNumberItemsToFetch = 2;
}
UInt32 theNumberOfItemsFetched = 0;
CAMutex::Locker theStateLocker(mStateMutex);
// fill out the list with as many objects as requested
if(theNumberItemsToFetch > 0)
{
if(mVolumeControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mVolumeControl.GetObjectID();
theNumberOfItemsFetched++;
}
else if(mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[0] = mMuteControl.GetObjectID();
theNumberOfItemsFetched++;
}
}
if(theNumberItemsToFetch > 1 && mVolumeControl.IsActive() && mMuteControl.IsActive())
{
reinterpret_cast<AudioObjectID*>(outData)[1] = mMuteControl.GetObjectID();
theNumberOfItemsFetched++;
}
// report how much we wrote
outDataSize = theNumberOfItemsFetched * sizeof(AudioObjectID);
}
break;
case kAudioDevicePropertyLatency:
// This property returns the latency of the device.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyLatency for the device");
*reinterpret_cast<UInt32*>(outData) = GetLatency();
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertySafetyOffset:
// This property returns the safety offset of the device.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertySafetyOffset for the device");
*reinterpret_cast<UInt32*>(outData) = GetSafetyOffset();
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyNominalSampleRate:
// This property returns the nominal sample rate of the device.
ThrowIf(inDataSize < sizeof(Float64),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyNominalSampleRate for the device");
*reinterpret_cast<Float64*>(outData) = GetSampleRate();
outDataSize = sizeof(Float64);
break;
case kAudioDevicePropertyAvailableNominalSampleRates:
// This returns all nominal sample rates the device supports as an array of
// AudioValueRangeStructs. Note that for discrete sampler rates, the range
// will have the minimum value equal to the maximum value.
//
// EQMDevice supports any sample rate so it can be set to match the output
// device when in loopback mode.
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
theNumberItemsToFetch = inDataSize / sizeof(AudioValueRange);
// clamp it to the number of items we have
if(theNumberItemsToFetch > 1)
{
theNumberItemsToFetch = 1;
}
// fill out the return array
if(theNumberItemsToFetch > 0)
{
// 0 would cause divide-by-zero errors in other EQM_Device functions (and
// wouldn't make sense anyway).
((AudioValueRange*)outData)[0].mMinimum = 1.0;
// Just in case DBL_MAX would cause problems in a client for some reason,
// use an arbitrary very large number instead. (It wouldn't make sense to
// actually set the sample rate this high, but I don't know what a
// reasonable maximum would be.)
((AudioValueRange*)outData)[0].mMaximum = 1000000000.0;
}
// report how much we wrote
outDataSize = theNumberItemsToFetch * sizeof(AudioValueRange);
break;
case kAudioDevicePropertyPreferredChannelsForStereo:
// This property returns which two channels to use as left/right for stereo
// data by default. Note that the channel numbers are 1-based.
ThrowIf(inDataSize < (2 * sizeof(UInt32)), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyPreferredChannelsForStereo for the device");
((UInt32*)outData)[0] = 1;
((UInt32*)outData)[1] = 2;
outDataSize = 2 * sizeof(UInt32);
break;
case kAudioDevicePropertyPreferredChannelLayout:
// This property returns the default AudioChannelLayout to use for the device
// by default. For this device, we return a stereo ACL.
{
UInt32 theACLSize = offsetof(AudioChannelLayout, mChannelDescriptions) + (2 * sizeof(AudioChannelDescription));
ThrowIf(inDataSize < theACLSize, CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyPreferredChannelLayout for the device");
((AudioChannelLayout*)outData)->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
((AudioChannelLayout*)outData)->mChannelBitmap = 0;
((AudioChannelLayout*)outData)->mNumberChannelDescriptions = 2;
for(theItemIndex = 0; theItemIndex < 2; ++theItemIndex)
{
((AudioChannelLayout*)outData)->mChannelDescriptions[theItemIndex].mChannelLabel = kAudioChannelLabel_Left + theItemIndex;
((AudioChannelLayout*)outData)->mChannelDescriptions[theItemIndex].mChannelFlags = 0;
((AudioChannelLayout*)outData)->mChannelDescriptions[theItemIndex].mCoordinates[0] = 0;
((AudioChannelLayout*)outData)->mChannelDescriptions[theItemIndex].mCoordinates[1] = 0;
((AudioChannelLayout*)outData)->mChannelDescriptions[theItemIndex].mCoordinates[2] = 0;
}
outDataSize = theACLSize;
}
break;
case kAudioDevicePropertyZeroTimeStampPeriod:
// This property returns how many frames the HAL should expect to see between
// successive sample times in the zero time stamps this device provides.
ThrowIf(inDataSize < sizeof(UInt32), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyZeroTimeStampPeriod for the device");
*reinterpret_cast<UInt32*>(outData) = kLoopbackRingBufferFrameSize;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyIcon:
{
// This property is a CFURL that points to the device's icon in the plugin's resource bundle
ThrowIf(inDataSize < sizeof(CFURLRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDevicePropertyIcon for the device");
CFBundleRef theBundle = CFBundleGetBundleWithIdentifier(EQM_PlugIn::GetInstance().GetBundleID());
ThrowIf(theBundle == NULL, CAException(kAudioHardwareUnspecifiedError), "EQM_Device::Device_GetPropertyData: could not get the plugin bundle for kAudioDevicePropertyIcon");
CFURLRef theURL = CFBundleCopyResourceURL(theBundle, CFSTR("icon.icns"), NULL, NULL);
ThrowIf(theURL == NULL, CAException(kAudioHardwareUnspecifiedError), "EQM_Device::Device_GetPropertyData: could not get the URL for kAudioDevicePropertyIcon");
*reinterpret_cast<CFURLRef*>(outData) = theURL;
outDataSize = sizeof(CFURLRef);
}
break;
case kAudioObjectPropertyCustomPropertyInfoList:
theNumberItemsToFetch = inDataSize / sizeof(AudioServerPlugInCustomPropertyInfo);
// clamp it to the number of items we have
if(theNumberItemsToFetch > 10)
{
theNumberItemsToFetch = 10;
}
if(theNumberItemsToFetch > 0)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[0].mSelector = kAudioDeviceCustomPropertyAppVolumes;
((AudioServerPlugInCustomPropertyInfo*)outData)[0].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[0].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 1)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[1].mSelector = kAudioDeviceCustomPropertyMusicPlayerProcessID;
((AudioServerPlugInCustomPropertyInfo*)outData)[1].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[1].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 2)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[2].mSelector = kAudioDeviceCustomPropertyMusicPlayerBundleID;
((AudioServerPlugInCustomPropertyInfo*)outData)[2].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFString;
((AudioServerPlugInCustomPropertyInfo*)outData)[2].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 3)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[3].mSelector = kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp;
((AudioServerPlugInCustomPropertyInfo*)outData)[3].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[3].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 4)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[4].mSelector = kAudioDeviceCustomPropertyDeviceAudibleState;
((AudioServerPlugInCustomPropertyInfo*)outData)[4].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[4].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 5)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mSelector = kAudioDeviceCustomPropertyEnabledOutputControls;
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 6)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mSelector = kAudioDeviceCustomPropertyLatency;
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[5].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 7)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mSelector = kAudioDeviceCustomPropertySafetyOffset;
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 8)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mSelector = kAudioDeviceCustomPropertyShown;
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
((AudioServerPlugInCustomPropertyInfo*)outData)[6].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
if(theNumberItemsToFetch > 9)
{
((AudioServerPlugInCustomPropertyInfo*)outData)[7].mSelector = kAudioDeviceCustomPropertyVersion;
((AudioServerPlugInCustomPropertyInfo*)outData)[7].mPropertyDataType = kAudioServerPlugInCustomPropertyDataTypeCFString;
((AudioServerPlugInCustomPropertyInfo*)outData)[7].mQualifierDataType = kAudioServerPlugInCustomPropertyDataTypeNone;
}
outDataSize = theNumberItemsToFetch * sizeof(AudioServerPlugInCustomPropertyInfo);
break;
case kAudioDeviceCustomPropertyDeviceAudibleState:
{
ThrowIf(inDataSize < sizeof(CFNumberRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyDeviceAudibleState for the device");
// The audible state is read without locking to avoid priority inversions on the IO threads.
EQMDeviceAudibleState theAudibleState = mAudibleState.GetState();
*reinterpret_cast<CFNumberRef*>(outData) =
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &theAudibleState);
outDataSize = sizeof(CFNumberRef);
}
break;
case kAudioDeviceCustomPropertyMusicPlayerProcessID:
{
ThrowIf(inDataSize < sizeof(CFNumberRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyMusicPlayerProcessID for the device");
CAMutex::Locker theStateLocker(mStateMutex);
pid_t pid = mClients.GetMusicPlayerProcessIDProperty();
*reinterpret_cast<CFNumberRef*>(outData) = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
outDataSize = sizeof(CFNumberRef);
}
break;
case kAudioDeviceCustomPropertyMusicPlayerBundleID:
{
ThrowIf(inDataSize < sizeof(CFStringRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyMusicPlayerBundleID for the device");
CAMutex::Locker theStateLocker(mStateMutex);
*reinterpret_cast<CFStringRef*>(outData) = mClients.CopyMusicPlayerBundleIDProperty();
outDataSize = sizeof(CFStringRef);
}
break;
case kAudioDeviceCustomPropertyVersion:
{
ThrowIf(inDataSize < sizeof(CFStringRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyVersion for the device");
CAMutex::Locker theStateLocker(mStateMutex);
CFStringRef version = CFCopyDescription(
CFBundleGetValueForInfoDictionaryKey(
CFBundleGetBundleWithIdentifier(CFSTR(kEQMDriverBundleID)),
kCFBundleVersionKey
)
);
// DebugMsg("EQM_LOG: %s", CFStringGetCStringPtr(CFCopyDescription(CFBundleGetMainBundle()), kCFStringEncodingMacRoman));
*reinterpret_cast<CFStringRef*>(outData) = version;
outDataSize = sizeof(CFStringRef);
}
break;
case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp:
ThrowIf(inDataSize < sizeof(CFBooleanRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp for the device");
*reinterpret_cast<CFBooleanRef*>(outData) = mClients.ClientsOtherThanEQMAppRunningIO() ? kCFBooleanTrue : kCFBooleanFalse;
outDataSize = sizeof(CFBooleanRef);
break;
case kAudioDeviceCustomPropertyAppVolumes:
{
ThrowIf(inDataSize < sizeof(CFArrayRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyAppVolumes for the device");
CAMutex::Locker theStateLocker(mStateMutex);
*reinterpret_cast<CFArrayRef*>(outData) = mClients.CopyClientRelativeVolumesAsAppVolumes().GetCFArray();
outDataSize = sizeof(CFArrayRef);
}
break;
case kAudioDeviceCustomPropertyEnabledOutputControls:
{
ThrowIf(inDataSize < sizeof(CFArrayRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_GetPropertyData: not enough space for the return value of kAudioDeviceCustomPropertyEnabledOutputControls for the device");
CACFArray theEnabledControls(2, true);
{
CAMutex::Locker theStateLocker(mStateMutex);
theEnabledControls.AppendCFType(mVolumeControl.IsActive() ? kCFBooleanTrue : kCFBooleanFalse);
theEnabledControls.AppendCFType(mMuteControl.IsActive() ? kCFBooleanTrue : kCFBooleanFalse);
}
*reinterpret_cast<CFArrayRef*>(outData) = theEnabledControls.CopyCFArray();
outDataSize = sizeof(CFArrayRef);
}
break;
default:
EQM_AbstractDevice::GetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, outDataSize, outData);
break;
};
}
void EQM_Device::Device_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData)
{
switch(inAddress.mSelector)
{
case kAudioDeviceCustomPropertyLatency:
{
ThrowIf(inDataSize < sizeof(CFNumberRef),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertyLatency");
CFNumberRef latencyRef = *reinterpret_cast<const CFNumberRef*>(inData);
uint32_t latency = 0;
CFNumberGetValue(latencyRef, kCFNumberSInt32Type, &latency);
ThrowIf(latency < 0,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: kAudioDeviceCustomPropertyLatency cannot be lower than 0");
SetLatency(latency);
break;
}
case kAudioDeviceCustomPropertySafetyOffset:
{
ThrowIf(inDataSize < sizeof(CFNumberRef),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertySafetyOffset");
CFNumberRef safetyOffsetRef = *reinterpret_cast<const CFNumberRef*>(inData);
uint32_t safetyOffset = 0;
CFNumberGetValue(safetyOffsetRef, kCFNumberSInt32Type, &safetyOffset);
ThrowIf(safetyOffset < 0,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: kAudioDeviceCustomPropertySafetyOffset cannot be lower than 0");
SetSafetyOffset(safetyOffset);
break;
}
case kAudioDeviceCustomPropertyShown:
{
ThrowIf(inDataSize < sizeof(CFBooleanRef),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertyShown");
CFBooleanRef shownRef = *reinterpret_cast<const CFBooleanRef*>(inData);
bool shown = CFBooleanGetValue(shownRef);
SetShown(shown);
break;
}
case kAudioDevicePropertyNominalSampleRate: {
ThrowIf(inDataSize < sizeof(Float64),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDevicePropertyNominalSampleRate");
RequestSampleRate(*reinterpret_cast<const Float64*>(inData));
break;
}
case kAudioDeviceCustomPropertyMusicPlayerProcessID:
{
ThrowIf(inDataSize < sizeof(CFNumberRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertyMusicPlayerProcessID");
CFNumberRef pidRef = *reinterpret_cast<const CFNumberRef*>(inData);
ThrowIf(pidRef == NULL, CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: null reference given for kAudioDeviceCustomPropertyMusicPlayerProcessID");
ThrowIf(CFGetTypeID(pidRef) != CFNumberGetTypeID(), CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: CFType given for kAudioDeviceCustomPropertyMusicPlayerProcessID was not a CFNumber");
// Get the pid out of the CFNumber we received
// (Not using CACFNumber::GetSInt32 here because it would return 0 if CFNumberGetValue didn't write to our
// pid variable, and we want that to be an error.)
pid_t pid = INT_MIN;
// CFNumberGetValue docs: "If the conversion is lossy, or the value is out of range, false is returned."
Boolean success = CFNumberGetValue(pidRef, kCFNumberIntType, &pid);
ThrowIf(!success, CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: probable error from CFNumberGetValue when reading pid for kAudioDeviceCustomPropertyMusicPlayerProcessID");
CAMutex::Locker theStateLocker(mStateMutex);
bool propertyWasChanged = false;
try
{
propertyWasChanged = mClients.SetMusicPlayer(pid);
}
catch(EQM_InvalidClientPIDException)
{
Throw(CAException(kAudioHardwareIllegalOperationError));
}
if(propertyWasChanged)
{
// Send notification
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[] = { kEQMMusicPlayerProcessIDAddress, kEQMMusicPlayerBundleIDAddress };
EQM_PlugIn::Host_PropertiesChanged(inObjectID, 2, theChangedProperties);
});
}
}
break;
case kAudioDeviceCustomPropertyMusicPlayerBundleID:
{
ThrowIf(inDataSize < sizeof(CFStringRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertyMusicPlayerBundleID");
CFStringRef theBundleIDRef = *reinterpret_cast<const CFStringRef*>(inData);
ThrowIfNULL(theBundleIDRef, CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: kAudioDeviceCustomPropertyMusicPlayerBundleID cannot be set to NULL");
ThrowIf(CFGetTypeID(theBundleIDRef) != CFStringGetTypeID(), CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: CFType given for kAudioDeviceCustomPropertyMusicPlayerBundleID was not a CFString");
CAMutex::Locker theStateLocker(mStateMutex);
CFRetain(theBundleIDRef);
CACFString bundleID(theBundleIDRef);
bool propertyWasChanged = mClients.SetMusicPlayer(bundleID);
if(propertyWasChanged)
{
// Send notification
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[] = { kEQMMusicPlayerBundleIDAddress, kEQMMusicPlayerProcessIDAddress };
EQM_PlugIn::Host_PropertiesChanged(inObjectID, 2, theChangedProperties);
});
}
}
break;
case kAudioDeviceCustomPropertyAppVolumes:
{
ThrowIf(inDataSize < sizeof(CFArrayRef), CAException(kAudioHardwareBadPropertySizeError), "EQM_Device::Device_SetPropertyData: wrong size for the data for kAudioDeviceCustomPropertyAppVolumes");
CFArrayRef arrayRef = *reinterpret_cast<const CFArrayRef*>(inData);
ThrowIfNULL(arrayRef, CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: kAudioDeviceCustomPropertyAppVolumes cannot be set to NULL");
ThrowIf(CFGetTypeID(arrayRef) != CFArrayGetTypeID(), CAException(kAudioHardwareIllegalOperationError), "EQM_Device::Device_SetPropertyData: CFType given for kAudioDeviceCustomPropertyAppVolumes was not a CFArray");
CACFArray array(arrayRef, false);
bool propertyWasChanged = false;
CAMutex::Locker theStateLocker(mStateMutex);
try
{
propertyWasChanged = mClients.SetClientsRelativeVolumes(array);
}
catch(EQM_InvalidClientRelativeVolumeException)
{
Throw(CAException(kAudioHardwareIllegalOperationError));
}
if(propertyWasChanged)
{
// Send notification
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[] = { kEQMAppVolumesAddress };
EQM_PlugIn::Host_PropertiesChanged(inObjectID, 1, theChangedProperties);
});
}
}
break;
case kAudioDeviceCustomPropertyEnabledOutputControls:
{
ThrowIf(inDataSize < sizeof(CFArrayRef),
CAException(kAudioHardwareBadPropertySizeError),
"EQM_Device::Device_SetPropertyData: wrong size for the data for "
"kAudioDeviceCustomPropertyEnabledOutputControls");
CFArrayRef theEnabledControlsRef = *reinterpret_cast<const CFArrayRef*>(inData);
ThrowIfNULL(theEnabledControlsRef,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: null reference given for "
"kAudioDeviceCustomPropertyEnabledOutputControls");
ThrowIf(CFGetTypeID(theEnabledControlsRef) != CFArrayGetTypeID(),
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: CFType given for "
"kAudioDeviceCustomPropertyEnabledOutputControls was not a CFArray");
CACFArray theEnabledControls(theEnabledControlsRef, false);
ThrowIf(theEnabledControls.GetNumberItems() != 2,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: Expected the CFArray given for "
"kAudioDeviceCustomPropertyEnabledOutputControls to have exactly 2 elements");
bool theVolumeControlEnabled;
bool didGetBool = theEnabledControls.GetBool(kEQMEnabledOutputControlsIndex_Volume,
theVolumeControlEnabled);
ThrowIf(!didGetBool,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: Expected CFBoolean for volume elem of "
"kAudioDeviceCustomPropertyEnabledOutputControls");
bool theMuteControlEnabled;
didGetBool = theEnabledControls.GetBool(kEQMEnabledOutputControlsIndex_Mute,
theMuteControlEnabled);
ThrowIf(!didGetBool,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::Device_SetPropertyData: Expected CFBoolean for mute elem of "
"kAudioDeviceCustomPropertyEnabledOutputControls");
RequestEnabledControls(theVolumeControlEnabled, theMuteControlEnabled);
}
break;
default:
EQM_AbstractDevice::SetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, inData);
break;
};
}
#pragma mark IO Operations
void EQM_Device::StartIO(UInt32 inClientID)
{
bool clientIsEQMApp, EQMAppHasClientRegistered;
{
CAMutex::Locker theStateLocker(mStateMutex);
// An overview of the process this function is part of:
// - A client starts IO.
// - The plugin host (the HAL) calls the StartIO function in EQM_PlugInInterface, which calls this function.
// - EQMDriver sends a message to EQMApp telling it to start the (real) audio hardware.
// - EQMApp starts the hardware and, after the hardware is ready, replies to EQMDriver's message.
// - EQMDriver lets the host know that it's ready to do IO by returning from StartIO.
// Update our client data.
//
// We add the work to the task queue, rather than doing it here, because BeginIOOperation and EndIOOperation
// also add this task to the queue and the updates should be done in order.
bool didStartIO = mTaskQueue.QueueSync_StartClientIO(&mClients, inClientID);
// We only tell the hardware to start if this is the first time IO has been started.
if(didStartIO)
{
kern_return_t theError = _HW_StartIO();
ThrowIfKernelError(theError,
CAException(theError),
"EQM_Device::StartIO: Failed to start because of an error calling down to the driver.");
}
clientIsEQMApp = mClients.IsEQMApp(inClientID);
EQMAppHasClientRegistered = mClients.EQMAppHasClientRegistered();
}
// We only return from StartIO after EQMApp is ready to pass the audio through to the output device. That way
// the HAL doesn't start sending us data before EQMApp can play it, which would mean we'd have to either drop
// frames or increase latency.
if(!clientIsEQMApp && EQMAppHasClientRegistered)
{
UInt64 theXPCError = StartEQMAppPlayThroughSync(GetObjectID() == kObjectID_Device_UI_Sounds);
switch(theXPCError)
{
case kEQMXPC_Success:
DebugMsg("EQM_Device::StartIO: Ready for IO.");
break;
case kEQMXPC_MessageFailure:
// This most likely means EQMXPCHelper isn't installed or has crashed. IO will probably still work,
// but we may drop frames while the audio hardware starts up.
LogError("EQM_Device::StartIO: Couldn't reach EQMApp via XPC. Attempting to start IO anyway.");
break;
case kEQMXPC_ReturningEarlyError:
// This can (and might always) happen when the user changes output device in EQMApp while IO is running.
// See EQMAudioDeviceManager::startPlayThroughSync and EQMPlayThrough::WaitForOutputDeviceToStart.
LogWarning("EQM_Device::StartIO: EQMApp was busy, so EQMDriver has to return from StartIO early.");
break;
default:
LogError("EQM_Device::StartIO: EQMApp failed to start the output device. theXPCError=%llu", theXPCError);
Throw(CAException(kAudioHardwareNotRunningError));
}
}
}
void EQM_Device::StopIO(UInt32 inClientID)
{
CAMutex::Locker theStateLocker(mStateMutex);
// Update our client data.
//
// We add the work to the task queue, rather than doing it here, because BeginIOOperation and EndIOOperation also
// add this task to the queue and the updates should be done in order.
bool didStopIO = mTaskQueue.QueueSync_StopClientIO(&mClients, inClientID);
// we tell the hardware to stop if this is the last stop call
if(didStopIO)
{
_HW_StopIO();
}
}
void EQM_Device::GetZeroTimeStamp(Float64& outSampleTime, UInt64& outHostTime, UInt64& outSeed)
{
// accessing the buffers requires holding the IO mutex
CAMutex::Locker theIOLocker(mIOMutex);
if(mWrappedAudioEngine != NULL)
{
}
else
{
// Without a wrapped device, we base our timing on the host. This is mostly from Apple's NullAudio.c sample code
UInt64 theCurrentHostTime;
Float64 theHostTicksPerRingBuffer;
Float64 theHostTickOffset;
UInt64 theNextHostTime;
// get the current host time
theCurrentHostTime = mach_absolute_time();
// calculate the next host time
theHostTicksPerRingBuffer = mLoopbackTime.hostTicksPerFrame * kLoopbackRingBufferFrameSize;
theHostTickOffset = static_cast<Float64>(mLoopbackTime.numberTimeStamps + 1) * theHostTicksPerRingBuffer;
theNextHostTime = mLoopbackTime.anchorHostTime + static_cast<UInt64>(theHostTickOffset);
// go to the next time if the next host time is less than the current time
if(theNextHostTime <= theCurrentHostTime)
{
mLoopbackTime.numberTimeStamps++;
}
// set the return values
outSampleTime = mLoopbackTime.numberTimeStamps * kLoopbackRingBufferFrameSize;
outHostTime = static_cast<UInt64>(mLoopbackTime.anchorHostTime + (static_cast<Float64>(mLoopbackTime.numberTimeStamps) * theHostTicksPerRingBuffer));
// TODO: I think we should increment outSeed whenever this device switches to/from having a wrapped engine
outSeed = 1;
}
}
void EQM_Device::WillDoIOOperation(UInt32 inOperationID, bool& outWillDo, bool& outWillDoInPlace) const
{
switch(inOperationID)
{
case kAudioServerPlugInIOOperationThread:
case kAudioServerPlugInIOOperationReadInput:
case kAudioServerPlugInIOOperationProcessOutput:
case kAudioServerPlugInIOOperationWriteMix:
outWillDo = true;
outWillDoInPlace = true;
break;
case kAudioServerPlugInIOOperationProcessMix:
outWillDo = mVolumeControl.WillApplyVolumeToAudioRT();
outWillDoInPlace = true;
break;
case kAudioServerPlugInIOOperationCycle:
case kAudioServerPlugInIOOperationConvertInput:
case kAudioServerPlugInIOOperationProcessInput:
case kAudioServerPlugInIOOperationMixOutput:
case kAudioServerPlugInIOOperationConvertMix:
default:
outWillDo = false;
outWillDoInPlace = true;
break;
};
}
void EQM_Device::BeginIOOperation(UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo& inIOCycleInfo, UInt32 inClientID)
{
#pragma unused(inIOBufferFrameSize, inIOCycleInfo)
if(inOperationID == kAudioServerPlugInIOOperationThread)
{
// Update this client's IO state and send notifications if that changes the value of
// kAudioDeviceCustomPropertyDeviceIsRunning or
// kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanEQMApp. We have to do this here
// as well as in StartIO because the HAL only calls StartIO/StopIO with the first/last clients.
//
// We perform the update async because it isn't real-time safe, but we can't just dispatch it with
// dispatch_async because that isn't real-time safe either. (Apparently even constructing a block
// isn't.)
//
// We don't have to hold the IO mutex here because mTaskQueue and mClients don't change and
// adding a task to mTaskQueue is thread safe.
mTaskQueue.QueueAsync_StartClientIO(&mClients, inClientID);
}
}
void EQM_Device::DoIOOperation(AudioObjectID inStreamObjectID, UInt32 inClientID, UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo& inIOCycleInfo, void* ioMainBuffer, void* ioSecondaryBuffer)
{
#pragma unused(inStreamObjectID, ioSecondaryBuffer)
switch(inOperationID)
{
case kAudioServerPlugInIOOperationReadInput:
{
CAMutex::Locker theIOLocker(mIOMutex);
ReadInputData(inIOBufferFrameSize, inIOCycleInfo.mInputTime.mSampleTime, ioMainBuffer);
}
break;
case kAudioServerPlugInIOOperationProcessOutput:
{
bool theClientIsMusicPlayer = mClients.IsMusicPlayerRT(inClientID);
CAMutex::Locker theIOLocker(mIOMutex);
// Called in this IO operation so we can get the music player client's data separately
mAudibleState.UpdateWithClientIO(theClientIsMusicPlayer,
inIOBufferFrameSize,
inIOCycleInfo.mOutputTime.mSampleTime,
reinterpret_cast<const Float32*>(ioMainBuffer));
}
ApplyClientRelativeVolume(inClientID, inIOBufferFrameSize, ioMainBuffer);
break;
case kAudioServerPlugInIOOperationProcessMix:
{
// Check the arguments.
ThrowIfNULL(ioMainBuffer,
CAException(kAudioHardwareIllegalOperationError),
"EQM_Device::DoIOOperation: Buffer for "
"kAudioServerPlugInIOOperationProcessMix must not be null");
CAMutex::Locker theIOLocker(mIOMutex);
// We ask to do this IO operation so this device can apply its own volume to the
// stream. Currently, only the UI sounds device does.
mVolumeControl.ApplyVolumeToAudioRT(reinterpret_cast<Float32*>(ioMainBuffer),
inIOBufferFrameSize);
}
break;
case kAudioServerPlugInIOOperationWriteMix:
{
CAMutex::Locker theIOLocker(mIOMutex);
bool didChangeState =
mAudibleState.UpdateWithMixedIO(inIOBufferFrameSize,
inIOCycleInfo.mOutputTime.mSampleTime,
reinterpret_cast<const Float32*>(ioMainBuffer));
if(didChangeState)
{
// Send notifications. I'm pretty sure we don't have to use
// RequestDeviceConfigurationChange for this property, but the docs seemed a bit
// unclear to me.
mTaskQueue.QueueAsync_SendPropertyNotification(
kAudioDeviceCustomPropertyDeviceAudibleState, GetObjectID());
}
WriteOutputData(inIOBufferFrameSize, inIOCycleInfo.mOutputTime.mSampleTime, ioMainBuffer);
}
break;
default:
// Note that this will only log the error in debug builds.
DebugMsg("EQM_Device::DoIOOperation: Unexpected IO operation: %u", inOperationID);
break;
};
}
void EQM_Device::EndIOOperation(UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo& inIOCycleInfo, UInt32 inClientID)
{
#pragma unused(inIOBufferFrameSize, inIOCycleInfo)
if(inOperationID == kAudioServerPlugInIOOperationThread)
{
// Tell EQM_Clients that this client has stopped IO. Queued async because we have to be real-time safe here.
//
// We don't have to hold the IO mutex here because mTaskQueue and mClients don't change and adding a task to
// mTaskQueue is thread safe.
mTaskQueue.QueueAsync_StopClientIO(&mClients, inClientID);
}
}
void EQM_Device::ReadInputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, void* outBuffer)
{
// figure out where we are starting
UInt64 theSampleTime = static_cast<UInt64>(inSampleTime);
UInt32 theStartFrameOffset = theSampleTime % kLoopbackRingBufferFrameSize;
// figure out how many frames we need to copy
UInt32 theNumberFramesToCopy1 = inIOBufferFrameSize;
UInt32 theNumberFramesToCopy2 = 0;
if((theStartFrameOffset + theNumberFramesToCopy1) > kLoopbackRingBufferFrameSize)
{
theNumberFramesToCopy1 = kLoopbackRingBufferFrameSize - theStartFrameOffset;
theNumberFramesToCopy2 = inIOBufferFrameSize - theNumberFramesToCopy1;
}
// do the copying (the byte sizes here assume a 32 bit stereo sample format)
Float32* theDestination = reinterpret_cast<Float32*>(outBuffer);
memcpy(theDestination, mLoopbackRingBuffer + (theStartFrameOffset * 2), theNumberFramesToCopy1 * 8);
if(theNumberFramesToCopy2 > 0)
{
memcpy(theDestination + (theNumberFramesToCopy1 * 2), mLoopbackRingBuffer, theNumberFramesToCopy2 * 8);
}
//DebugMsg("EQM_Device::ReadInputData: Reading. theSampleTime=%llu theStartFrameOffset=%u theNumberFramesToCopy1=%u theNumberFramesToCopy2=%u", theSampleTime, theStartFrameOffset, theNumberFramesToCopy1, theNumberFramesToCopy2);
}
void EQM_Device::WriteOutputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, const void* inBuffer)
{
// figure out where we are starting
UInt64 theSampleTime = static_cast<UInt64>(inSampleTime);
UInt32 theStartFrameOffset = theSampleTime % kLoopbackRingBufferFrameSize;
// figure out how many frames we need to copy
UInt32 theNumberFramesToCopy1 = inIOBufferFrameSize;
UInt32 theNumberFramesToCopy2 = 0;
if((theStartFrameOffset + theNumberFramesToCopy1) > kLoopbackRingBufferFrameSize)
{
theNumberFramesToCopy1 = kLoopbackRingBufferFrameSize - theStartFrameOffset;
theNumberFramesToCopy2 = inIOBufferFrameSize - theNumberFramesToCopy1;
}
// do the copying (the byte sizes here assume a 32 bit stereo sample format)
const Float32* theSource = reinterpret_cast<const Float32*>(inBuffer);
memcpy(mLoopbackRingBuffer + (theStartFrameOffset * 2), theSource, theNumberFramesToCopy1 * 8);
if(theNumberFramesToCopy2 > 0)
{
memcpy(mLoopbackRingBuffer, theSource + (theNumberFramesToCopy1 * 2), theNumberFramesToCopy2 * 8);
}
//DebugMsg("EQM_Device::WriteOutputData: Writing. theSampleTime=%llu theStartFrameOffset=%u theNumberFramesToCopy1=%u theNumberFramesToCopy2=%u", theSampleTime, theStartFrameOffset, theNumberFramesToCopy1, theNumberFramesToCopy2);
}
void EQM_Device::ApplyClientRelativeVolume(UInt32 inClientID, UInt32 inIOBufferFrameSize, void* ioBuffer) const
{
Float32* theBuffer = reinterpret_cast<Float32*>(ioBuffer);
Float32 theRelativeVolume = mClients.GetClientRelativeVolumeRT(inClientID);
auto thePanPositionInt = mClients.GetClientPanPositionRT(inClientID);
Float32 thePanPosition = static_cast<Float32>(thePanPositionInt) / 100.0f;
// TODO When we get around to supporting devices with more than two channels it would be worth looking into
// kAudioFormatProperty_PanningMatrix and kAudioFormatProperty_BalanceFade in AudioFormat.h.
// TODO precompute matrix coefficients w/ volume and do everything in one pass
// Apply balance w/ crossfeed to the frames in the buffer.
// Expect samples interleaved, starting with left
if (thePanPosition > 0.0f) {
for (UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2) {
auto L = i;
auto R = i + 1;
theBuffer[R] = theBuffer[R] + theBuffer[L] * thePanPosition;
theBuffer[L] = theBuffer[L] * (1 - thePanPosition);
}
} else if (thePanPosition < 0.0f) {
for (UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2) {
auto L = i;
auto R = i + 1;
theBuffer[L] = theBuffer[L] + theBuffer[R] * (-thePanPosition);
theBuffer[R] = theBuffer[R] * (1 + thePanPosition);
}
}
if(theRelativeVolume != 1.0f)
{
for(UInt32 i = 0; i < inIOBufferFrameSize * 2; i++)
{
Float32 theAdjustedSample = theBuffer[i] * theRelativeVolume;
// Clamp to [-1, 1].
// (This way is roughly 6 times faster than using std::min and std::max because the compiler can vectorize the loop.)
const Float32 theAdjustedSampleClippedBelow = theAdjustedSample < -1.0f ? -1.0f : theAdjustedSample;
theBuffer[i] = theAdjustedSampleClippedBelow > 1.0f ? 1.0f : theAdjustedSampleClippedBelow;
}
}
}
#pragma mark Accessors
void EQM_Device::RequestEnabledControls(bool inVolumeEnabled, bool inMuteEnabled)
{
CAMutex::Locker theStateLocker(mStateMutex);
bool changeVolume = (mVolumeControl.IsActive() != inVolumeEnabled);
bool changeMute = (mMuteControl.IsActive() != inMuteEnabled);
if(changeVolume)
{
DebugMsg("EQM_Device::RequestEnabledControls: %s volume control",
(inVolumeEnabled ? "Enabling" : "Disabling"));
mPendingOutputVolumeControlEnabled = inVolumeEnabled;
}
if(changeMute)
{
DebugMsg("EQM_Device::RequestEnabledControls: %s mute control",
(inMuteEnabled ? "Enabling" : "Disabling"));
mPendingOutputMuteControlEnabled = inMuteEnabled;
}
if(changeVolume || changeMute)
{
// Ask the host to stop IO (and whatever else) so we can safely update the device's list of
// controls. See RequestDeviceConfigurationChange in AudioServerPlugIn.h.
AudioObjectID theDeviceObjectID = GetObjectID();
UInt64 action = static_cast<UInt64>(ChangeAction::SetEnabledControls);
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
EQM_PlugIn::Host_RequestDeviceConfigurationChange(theDeviceObjectID, action, nullptr);
});
}
}
Float64 EQM_Device::GetSampleRate() const
{
// The sample rate is guarded by the state lock. Note that we don't need to take the IO lock.
CAMutex::Locker theStateLocker(mStateMutex);
Float64 theSampleRate;
// Report the sample rate from the wrapped device if we have one. Note that _HW_GetSampleRate
// the device's nominal sample rate, not one calculated from its timestamps.
if(mWrappedAudioEngine == nullptr)
{
theSampleRate = mLoopbackSampleRate;
}
else
{
theSampleRate = _HW_GetSampleRate();
}
return theSampleRate;
}
UInt32 EQM_Device::GetSafetyOffset() const
{
// The safety offset is guarded by the state lock. Note that we don't need to take the IO lock.
CAMutex::Locker theStateLocker(mStateMutex);
return mSafetyOffset;
}
UInt32 EQM_Device::GetLatency() const
{
// The safety offset is guarded by the state lock. Note that we don't need to take the IO lock.
CAMutex::Locker theStateLocker(mStateMutex);
return mLatency;
}
void EQM_Device::RequestSampleRate(Float64 inRequestedSampleRate)
{
// Changing the sample rate needs to be handled via the RequestConfigChange/PerformConfigChange
// machinery. See RequestDeviceConfigurationChange in AudioServerPlugIn.h.
// We try to support any sample rate a real output device might.
ThrowIf(inRequestedSampleRate < 1.0,
CAException(kAudioDeviceUnsupportedFormatError),
"EQM_Device::RequestSampleRate: unsupported sample rate");
DebugMsg("EQM_Device::RequestSampleRate: Sample rate change requested: %f",
inRequestedSampleRate);
CAMutex::Locker theStateLocker(mStateMutex);
if(inRequestedSampleRate != GetSampleRate()) // Check the sample rate will actually be changed.
{
mPendingSampleRate = inRequestedSampleRate;
// Dispatch this so the change can happen asynchronously.
auto requestSampleRate = ^{
UInt64 action = static_cast<UInt64>(ChangeAction::SetSampleRate);
EQM_PlugIn::Host_RequestDeviceConfigurationChange(GetObjectID(), action, nullptr);
};
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, requestSampleRate);
}
}
EQM_Object& EQM_Device::GetOwnedObjectByID(AudioObjectID inObjectID)
{
// C++ is weird. See "Avoid Duplication in const and Non-const Member Functions" in Item 3 of Effective C++.
return const_cast<EQM_Object&>(static_cast<const EQM_Device&>(*this).GetOwnedObjectByID(inObjectID));
}
const EQM_Object& EQM_Device::GetOwnedObjectByID(AudioObjectID inObjectID) const
{
if(inObjectID == mInputStream.GetObjectID())
{
return mInputStream;
}
else if(inObjectID == mOutputStream.GetObjectID())
{
return mOutputStream;
}
else if(inObjectID == mVolumeControl.GetObjectID())
{
return mVolumeControl;
}
else if(inObjectID == mMuteControl.GetObjectID())
{
return mMuteControl;
}
else
{
LogError("EQM_Device::GetOwnedObjectByID: Unknown object ID. inObjectID = %u", inObjectID);
Throw(CAException(kAudioHardwareBadObjectError));
}
}
UInt32 EQM_Device::GetNumberOfSubObjects() const
{
return kNumberOfInputSubObjects + GetNumberOfOutputSubObjects();
}
UInt32 EQM_Device::GetNumberOfOutputSubObjects() const
{
return kNumberOfOutputStreams + GetNumberOfOutputControls();
}
UInt32 EQM_Device::GetNumberOfOutputControls() const
{
CAMutex::Locker theStateLocker(mStateMutex);
UInt32 theAnswer = 0;
if(mVolumeControl.IsActive())
{
theAnswer++;
}
if(mMuteControl.IsActive())
{
theAnswer++;
}
return theAnswer;
}
void EQM_Device::SetEnabledControls(bool inVolumeEnabled, bool inMuteEnabled)
{
CAMutex::Locker theStateLocker(mStateMutex);
if(mVolumeControl.IsActive() != inVolumeEnabled)
{
DebugMsg("EQM_Device::SetEnabledControls: %s the volume control",
inVolumeEnabled ? "Enabling" : "Disabling");
if(inVolumeEnabled)
{
mVolumeControl.Activate();
}
else
{
mVolumeControl.Deactivate();
}
}
if(mMuteControl.IsActive() != inMuteEnabled)
{
DebugMsg("EQM_Device::SetEnabledControls: %s the mute control",
inMuteEnabled ? "Enabling" : "Disabling");
if(inMuteEnabled)
{
mMuteControl.Activate();
}
else
{
mMuteControl.Deactivate();
}
}
}
void EQM_Device::SetSampleRate(Float64 inSampleRate, bool force)
{
// We try to support any sample rate a real output device might.
ThrowIf(inSampleRate < 1.0,
CAException(kAudioDeviceUnsupportedFormatError),
"EQM_Device::SetSampleRate: unsupported sample rate");
CAMutex::Locker theStateLocker(mStateMutex);
Float64 theCurrentSampleRate = GetSampleRate();
if((inSampleRate != theCurrentSampleRate) || force) // Check whether we need to change it.
{
DebugMsg("EQM_Device::SetSampleRate: Changing the sample rate from %f to %f",
theCurrentSampleRate,
inSampleRate);
// Update the sample rate on the wrapped device if we have one.
if(mWrappedAudioEngine != nullptr)
{
kern_return_t theError = _HW_SetSampleRate(inSampleRate);
ThrowIfKernelError(theError,
CAException(kAudioHardwareUnspecifiedError),
"EQM_Device::SetSampleRate: Error setting the sample rate on the "
"wrapped audio device.");
}
// Update the sample rate for loopback.
mLoopbackSampleRate = inSampleRate;
InitLoopback();
// Update the streams.
mInputStream.SetSampleRate(inSampleRate);
mOutputStream.SetSampleRate(inSampleRate);
}
else
{
DebugMsg("EQM_Device::SetSampleRate: The sample rate is already set to %f", inSampleRate);
}
}
void EQM_Device::SetSafetyOffset(UInt32 inSafetyOffset)
{
ThrowIf(inSafetyOffset < 0,
CAException(kAudioDeviceUnsupportedFormatError),
"EQM_Device::SetSafetyOffset: unsupported safety offset");
CAMutex::Locker theStateLocker(mStateMutex);
mSafetyOffset = inSafetyOffset;
}
void EQM_Device::SetLatency(UInt32 inLatency)
{
ThrowIf(inLatency < 0,
CAException(kAudioDeviceUnsupportedFormatError),
"EQM_Device::SetLatency: unsupported latency");
CAMutex::Locker theStateLocker(mStateMutex);
mLatency = inLatency;
}
void EQM_Device::SetShown(bool shown)
{
CAMutex::Locker theStateLocker(mStateMutex);
mShown = shown;
}
bool EQM_Device::IsStreamID(AudioObjectID inObjectID) const noexcept
{
return (inObjectID == mInputStream.GetObjectID()) || (inObjectID == mOutputStream.GetObjectID());
}
#pragma mark Hardware Accessors
// TODO: Out of laziness, some of these hardware functions do more than their names suggest
void EQM_Device::_HW_Open()
{
}
void EQM_Device::_HW_Close()
{
}
kern_return_t EQM_Device::_HW_StartIO()
{
EQMAssert(mStateMutex.IsOwnedByCurrentThread(),
"EQM_Device::_HW_StartIO: Called without taking the state mutex");
if(mWrappedAudioEngine != nullptr)
{
}
// Reset the loopback timing values
mLoopbackTime.numberTimeStamps = 0;
mLoopbackTime.anchorHostTime = mach_absolute_time();
// ...and the most-recent audible/silent sample times. mAudibleState is usually guarded by the
// IO mutex, but we haven't started IO yet (and this function can only be called by one thread
// at a time).
EQMAssert(mIOMutex.IsFree(), "EQM_Device::_HW_StartIO: IO mutex taken before starting IO");
mAudibleState.Reset();
return KERN_SUCCESS;
}
void EQM_Device::_HW_StopIO()
{
if(mWrappedAudioEngine != NULL)
{
}
}
Float64 EQM_Device::_HW_GetSampleRate() const
{
// This function should only be called when wrapping a device.
ThrowIf(mWrappedAudioEngine == nullptr,
CAException(kAudioHardwareUnspecifiedError),
"EQM_Device::_HW_GetSampleRate: No wrapped audio device");
return mWrappedAudioEngine->GetSampleRate();
}
kern_return_t EQM_Device::_HW_SetSampleRate(Float64 inNewSampleRate)
{
// This function should only be called when wrapping a device.
ThrowIf(mWrappedAudioEngine == nullptr,
CAException(kAudioHardwareUnspecifiedError),
"EQM_Device::_HW_SetSampleRate: No wrapped audio device");
return mWrappedAudioEngine->SetSampleRate(inNewSampleRate);
}
UInt32 EQM_Device::_HW_GetRingBufferFrameSize() const
{
return (mWrappedAudioEngine != NULL) ? mWrappedAudioEngine->GetSampleBufferFrameSize() : 0;
}
#pragma mark Implementation
void EQM_Device::AddClient(const AudioServerPlugInClientInfo* inClientInfo)
{
DebugMsg("EQM_Device::AddClient: Adding client %u (%s)",
inClientInfo->mClientID,
(inClientInfo->mBundleID == NULL ?
"no bundle ID" :
CFStringGetCStringPtr(inClientInfo->mBundleID, kCFStringEncodingUTF8)));
CAMutex::Locker theStateLocker(mStateMutex);
mClients.AddClient(inClientInfo);
}
void EQM_Device::RemoveClient(const AudioServerPlugInClientInfo* inClientInfo)
{
DebugMsg("EQM_Device::RemoveClient: Removing client %u (%s)",
inClientInfo->mClientID,
CFStringGetCStringPtr(inClientInfo->mBundleID, kCFStringEncodingUTF8));
CAMutex::Locker theStateLocker(mStateMutex);
// If we're removing EQMApp, reenable all of EQMDevice's controls.
if(mClients.IsEQMApp(inClientInfo->mClientID))
{
RequestEnabledControls(true, true);
}
mClients.RemoveClient(inClientInfo->mClientID);
}
void EQM_Device::PerformConfigChange(UInt64 inChangeAction, void* inChangeInfo)
{
#pragma unused(inChangeInfo)
DebugMsg("EQM_Device::PerformConfigChange: inChangeAction = %llu", inChangeAction);
// Apply a change requested with EQM_PlugIn::Host_RequestDeviceConfigurationChange. See
// PerformDeviceConfigurationChange in AudioServerPlugIn.h.
switch(static_cast<ChangeAction>(inChangeAction))
{
case ChangeAction::SetSampleRate:
SetSampleRate(mPendingSampleRate);
break;
case ChangeAction::SetEnabledControls:
SetEnabledControls(mPendingOutputVolumeControlEnabled,
mPendingOutputMuteControlEnabled);
break;
}
}
void EQM_Device::AbortConfigChange(UInt64 inChangeAction, void* inChangeInfo)
{
#pragma unused(inChangeAction, inChangeInfo)
// this device doesn't need to do anything special if a change request gets aborted
}