Files
VolumeControl/app/VCAudioDevice.cpp
2026-03-15 06:02:56 +09:00

388 lines
14 KiB
C++

// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// VCAudioDevice.cpp
// VCApp
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#include "VCAudioDevice.h"
// Local Includes
#include "VC_Types.h"
// System Includes
#include <AudioToolbox/AudioServices.h>
#pragma mark Construction/Destruction
VCAudioDevice::VCAudioDevice(AudioObjectID inAudioDevice)
:
CAHALAudioDevice(inAudioDevice)
{
}
VCAudioDevice::VCAudioDevice(CFStringRef inUID)
:
CAHALAudioDevice(inUID)
{
}
VCAudioDevice::VCAudioDevice(const CAHALAudioDevice& inDevice)
:
VCAudioDevice(inDevice.GetObjectID())
{
}
VCAudioDevice::~VCAudioDevice()
{
}
bool VCAudioDevice::CanBeOutputDeviceInVCApp() const
{
CFStringRef uid = CopyDeviceUID();
assert(uid != nullptr);
CFRelease(uid);
bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0;
bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
return !IsVCDeviceInstance() &&
!IsHidden() &&
hasOutputChannels &&
canBeDefault;
}
#pragma mark Available Controls
bool VCAudioDevice::HasSettableMasterVolume(AudioObjectPropertyScope inScope) const
{
return HasVolumeControl(inScope, kMasterChannel) &&
VolumeControlIsSettable(inScope, kMasterChannel);
}
bool VCAudioDevice::HasSettableVirtualMasterVolume(AudioObjectPropertyScope inScope) const
{
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
inScope,
kAudioObjectPropertyElementMaster
};
// TODO: Replace these calls deprecated AudioToolbox functions. There are more below.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
Boolean virtualMasterVolumeIsSettable;
OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(),
&virtualMasterVolumeAddress,
&virtualMasterVolumeIsSettable);
virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError);
bool hasVirtualMasterVolume =
AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress);
#pragma clang diagnostic pop
return hasVirtualMasterVolume && virtualMasterVolumeIsSettable;
}
bool VCAudioDevice::HasSettableMasterMute(AudioObjectPropertyScope inScope) const
{
return HasMuteControl(inScope, kMasterChannel) &&
MuteControlIsSettable(inScope, kMasterChannel);
}
#pragma mark Control Values Accessors
void VCAudioDevice::CopyMuteFrom(const VCAudioDevice inDevice,
AudioObjectPropertyScope inScope)
{
// TODO: Support for devices that have per-channel mute controls but no master mute control
if(HasSettableMasterMute(inScope) && inDevice.HasMuteControl(inScope, kMasterChannel))
{
SetMuteControlValue(inScope,
kMasterChannel,
inDevice.GetMuteControlValue(inScope, kMasterChannel));
}
}
void VCAudioDevice::CopyVolumeFrom(const VCAudioDevice inDevice,
AudioObjectPropertyScope inScope)
{
// Get the volume of the other device.
bool didGetVolume = false;
Float32 volume = FLT_MIN;
if(inDevice.HasVolumeControl(inScope, kMasterChannel))
{
volume = inDevice.GetVolumeControlScalarValue(inScope, kMasterChannel);
didGetVolume = true;
}
// Use the average channel volume of the other device if it has no master volume.
if(!didGetVolume)
{
UInt32 numChannels =
inDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
volume = 0;
for(UInt32 channel = 1; channel <= numChannels; channel++)
{
if(inDevice.HasVolumeControl(inScope, channel))
{
volume += inDevice.GetVolumeControlScalarValue(inScope, channel);
didGetVolume = true;
}
}
if(numChannels > 0) // Avoid divide by zero.
{
volume /= numChannels;
}
}
// Set the volume of this device.
if(didGetVolume && volume != FLT_MIN)
{
bool didSetVolume = false;
try
{
didSetVolume = SetMasterVolumeScalar(inScope, volume);
}
catch(CAException e)
{
OSStatus err = e.GetError();
char err4CC[5] = CA4CCToCString(err);
CFStringRef uid = CopyDeviceUID();
LogWarning("VCAudioDevice::CopyVolumeFrom: CAException '%s' trying to set master "
"volume of %s",
err4CC,
CFStringGetCStringPtr(uid, kCFStringEncodingUTF8));
CFRelease(uid);
}
if(!didSetVolume)
{
// Couldn't find a master volume control to set, so try to find a virtual one
Float32 virtualMasterVolume;
bool success = inDevice.GetVirtualMasterVolumeScalar(inScope, virtualMasterVolume);
if(success)
{
didSetVolume = SetVirtualMasterVolumeScalar(inScope, virtualMasterVolume);
}
}
if(!didSetVolume)
{
// Couldn't set a master or virtual master volume, so as a fallback try to set each
// channel individually.
UInt32 numChannels = GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
for(UInt32 channel = 1; channel <= numChannels; channel++)
{
if(HasVolumeControl(inScope, channel) && VolumeControlIsSettable(inScope, channel))
{
SetVolumeControlScalarValue(inScope, channel, volume);
}
}
}
}
}
bool VCAudioDevice::SetMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume)
{
if(HasSettableMasterVolume(inScope))
{
SetVolumeControlScalarValue(inScope, kMasterChannel, inVolume);
return true;
}
return false;
}
bool VCAudioDevice::GetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
Float32& outVirtualMasterVolume) const
{
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
inScope,
kAudioObjectPropertyElementMaster
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress))
{
return false;
}
#pragma clang diagnostic pop
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(),
&virtualMasterVolumeAddress,
&virtualMasterVolumePropertySize,
&outVirtualMasterVolume);
}
bool VCAudioDevice::SetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
Float32 inVolume)
{
// TODO: For me, setting the virtual master volume sets all the device's channels to the same volume, meaning you can't
// keep any channels quieter than the others. The expected behaviour is to scale the channel volumes
// proportionally. So to do this properly I think we'd have to store VCDevice's previous volume and calculate
// each channel's new volume from its current volume and the distance between VCDevice's old and new volumes.
//
// The docs kAudioHardwareServiceDeviceProperty_VirtualMasterVolume for say
// "If the device has individual channel volume controls, this property will apply to those identified by the
// device's preferred multi-channel layout (or preferred stereo pair if the device is stereo only). Note that
// this control maintains the relative balance between all the channels it affects.
// so I'm not sure why that's not working here. As a workaround we take the to device's (virtual master) balance
// before changing the volume and set it back after, but of course that'll only work for stereo devices.
bool didSetVolume = false;
if(HasSettableVirtualMasterVolume(inScope))
{
// Not sure why, but setting the virtual master volume sets all channels to the same volume. As a workaround, we store
// the current balance here so we can reset it after setting the volume.
Float32 virtualMasterBalance;
bool didGetVirtualMasterBalance = GetVirtualMasterBalance(inScope, virtualMasterBalance);
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
inScope,
kAudioObjectPropertyElementMaster
};
didSetVolume = (kAudioServicesNoError == AHSSetPropertyData(GetObjectID(),
&virtualMasterVolumeAddress,
sizeof(Float32),
&inVolume));
// Reset the balance
AudioObjectPropertyAddress virtualMasterBalanceAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMainBalance,
inScope,
kAudioObjectPropertyElementMaster
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if(didSetVolume &&
didGetVirtualMasterBalance &&
AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress))
{
Boolean balanceIsSettable;
OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(),
&virtualMasterBalanceAddress,
&balanceIsSettable);
if(err == kAudioServicesNoError && balanceIsSettable)
{
AHSSetPropertyData(GetObjectID(),
&virtualMasterBalanceAddress,
sizeof(Float32),
&virtualMasterBalance);
}
}
#pragma clang diagnostic pop
}
return didSetVolume;
}
bool VCAudioDevice::GetVirtualMasterBalance(AudioObjectPropertyScope inScope,
Float32& outVirtualMasterBalance) const
{
AudioObjectPropertyAddress virtualMasterBalanceAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMainBalance,
inScope,
kAudioObjectPropertyElementMaster
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress))
{
return false;
}
#pragma clang diagnostic pop
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(),
&virtualMasterBalanceAddress,
&virtualMasterVolumePropertySize,
&outVirtualMasterBalance);
}
#pragma mark Implementation
bool VCAudioDevice::IsVCDevice(bool inIncludeUISoundsInstance) const
{
bool isVCDevice = false;
if(GetObjectID() != kAudioObjectUnknown)
{
// Check the device's UID to see whether it's VCDevice.
CFStringRef uid = CopyDeviceUID();
if (uid == nullptr) {
return isVCDevice;
}
isVCDevice = CFEqual(uid, CFSTR(kVCDeviceUID));
CFRelease(uid);
}
return isVCDevice;
}
// static
OSStatus VCAudioDevice::AHSGetPropertyData(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,
UInt32* ioDataSize,
void* outData)
{
// The docs for AudioHardwareServiceGetPropertyData specifically allow passing NULL for
// inQualifierData as we do here, but it's declared in an assume_nonnull section so we have to
// disable the warning here. I'm not sure why inQualifierData isn't __nullable. I'm assuming
// it's either a backwards compatibility thing or just a bug.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// The non-depreciated version of this (and the setter below) doesn't seem to support devices
// other than the default
return AudioHardwareServiceGetPropertyData(inObjectID, inAddress, 0, NULL, ioDataSize, outData);
#pragma clang diagnostic pop
}
// static
OSStatus VCAudioDevice::AHSSetPropertyData(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,
UInt32 inDataSize,
const void* inData)
{
// See the explanation about these pragmas in AHSGetPropertyData
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return AudioHardwareServiceSetPropertyData(inObjectID, inAddress, 0, NULL, inDataSize, inData);
#pragma clang diagnostic pop
}