// 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 . // // VCAudioDevice.cpp // VCApp // // Copyright © 2017 Kyle Neideck // // Self Include #include "VCAudioDevice.h" // Local Includes #include "VC_Types.h" // System Includes #include #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 }