mirror of
https://github.com/morgan9e/VolumeControl
synced 2026-04-15 00:34:09 +09:00
Initial commit
This commit is contained in:
387
app/VCAudioDevice.cpp
Normal file
387
app/VCAudioDevice.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user