// static bool BGMDeviceControlSync::SetVirtualMasterVolume(CAHALAudioDevice inDevice, 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 BGMDevice's previous volume and calculate // each channel's new volume from its current volume and the distance between BGMDevice'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; AudioObjectPropertyAddress virtualMasterVolumeAddress = { kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, inScope, kAudioObjectPropertyElementMaster }; bool hasVirtualMasterVolume = AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterVolumeAddress); Boolean virtualMasterVolumeIsSettable; OSStatus err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterVolumeAddress, &virtualMasterVolumeIsSettable); virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError); if(hasVirtualMasterVolume && virtualMasterVolumeIsSettable) { // 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(inDevice, inScope, virtualMasterBalance); didSetVolume = kAudioServicesNoError == AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterVolumeAddress, sizeof(Float32), &inVolume); // Reset the balance AudioObjectPropertyAddress virtualMasterBalanceAddress = { kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster }; if(didSetVolume && didGetVirtualMasterBalance && AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress)) { Boolean balanceIsSettable; err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterBalanceAddress, &balanceIsSettable); if(err == kAudioServicesNoError && balanceIsSettable) { AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterBalanceAddress, sizeof(Float32), &virtualMasterBalance); } } } return didSetVolume; }
AudioDeviceID defaultOutputDeviceID() { AudioDeviceID outputDeviceID = kAudioObjectUnknown; // get output device device UInt32 propertySize = 0; OSStatus status = noErr; AudioObjectPropertyAddress propertyAOPA; propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal; propertyAOPA.mElement = kAudioObjectPropertyElementMaster; propertyAOPA.mSelector = kAudioHardwarePropertyDefaultOutputDevice; if (!AudioHardwareServiceHasProperty(kAudioObjectSystemObject, &propertyAOPA)) { fprintf(stderr, "ERROR: Cannot find default output device!\n"); return outputDeviceID; } propertySize = sizeof(AudioDeviceID); status = AudioHardwareServiceGetPropertyData(kAudioObjectSystemObject, &propertyAOPA, 0, NULL, &propertySize, &outputDeviceID); if (status) { fprintf(stderr, "ERROR: Cannot find default output device!\n"); } return outputDeviceID; }
SystemVol (AudioObjectPropertySelector selector) : outputDeviceID (kAudioObjectUnknown) { addr.mScope = kAudioObjectPropertyScopeGlobal; addr.mElement = kAudioObjectPropertyElementMaster; addr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; if (AudioHardwareServiceHasProperty (kAudioObjectSystemObject, &addr)) { UInt32 deviceIDSize = sizeof (outputDeviceID); OSStatus status = AudioHardwareServiceGetPropertyData (kAudioObjectSystemObject, &addr, 0, nullptr, &deviceIDSize, &outputDeviceID); if (status == noErr) { addr.mElement = kAudioObjectPropertyElementMaster; addr.mSelector = selector; addr.mScope = kAudioDevicePropertyScopeOutput; if (! AudioHardwareServiceHasProperty (outputDeviceID, &addr)) outputDeviceID = kAudioObjectUnknown; } } }
// static bool BGMDeviceControlSync::GetVirtualMasterBalance(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance) { AudioObjectPropertyAddress virtualMasterBalanceAddress = { kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster }; if(!AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress)) { return false; } UInt32 virtualMasterVolumePropertySize = sizeof(Float32); return kAudioServicesNoError == AHSGetPropertyData(inDevice.GetObjectID(), &virtualMasterBalanceAddress, &virtualMasterVolumePropertySize, &outVirtualMasterBalance); }
// setting system volume. Mutes if under threshhold. // Courtesy: https://gist.github.com/atr000/205140 int setVolume(float newVolume) { if (newVolume < 0.0 || newVolume > 1.0) { fprintf(stderr, "ERROR: Requested volume out of range (%.2f)\n", newVolume); return 1; } // get output device UInt32 propertySize = 0; OSStatus status = noErr; AudioObjectPropertyAddress propertyAOPA; propertyAOPA.mElement = kAudioObjectPropertyElementMaster; propertyAOPA.mScope = kAudioDevicePropertyScopeOutput; if (newVolume < 0.001) { propertyAOPA.mSelector = kAudioDevicePropertyMute; } else { propertyAOPA.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMasterVolume; } AudioDeviceID outputDeviceID = defaultOutputDeviceID(); if (outputDeviceID == kAudioObjectUnknown) { fprintf(stderr, "ERROR: Unknown device\n"); return 2; } if (!AudioHardwareServiceHasProperty(outputDeviceID, &propertyAOPA)) { fprintf(stderr, "ERROR: Device 0x%0x does not support volume control\n", outputDeviceID); return 3; } Boolean canSetVolume = false; status = AudioHardwareServiceIsPropertySettable(outputDeviceID, &propertyAOPA, &canSetVolume); if (status || !canSetVolume) { fprintf(stderr, "ERROR: Device 0x%0x does not support volume control\n", outputDeviceID); return 4; } if (propertyAOPA.mSelector == kAudioDevicePropertyMute) { propertySize = sizeof(UInt32); UInt32 mute = 1; status = AudioHardwareServiceSetPropertyData(outputDeviceID, &propertyAOPA, 0, NULL, propertySize, &mute); } else { propertySize = sizeof(Float32); status = AudioHardwareServiceSetPropertyData(outputDeviceID, &propertyAOPA, 0, NULL, propertySize, &newVolume); if (status) { fprintf(stderr, "ERROR: Unable to set volume for device 0x%0x\n", outputDeviceID); return 5; } // make sure we're not muted propertyAOPA.mSelector = kAudioDevicePropertyMute; propertySize = sizeof(UInt32); UInt32 mute = 0; if (!AudioHardwareServiceHasProperty(outputDeviceID, &propertyAOPA)) { fprintf(stderr, "ERROR: Device 0x%0x does not support muting\n", outputDeviceID); return 6; } Boolean canSetMute = false; status = AudioHardwareServiceIsPropertySettable(outputDeviceID, &propertyAOPA, &canSetMute); if (status || !canSetMute) { fprintf(stderr, "ERROR: Device 0x%0x does not support muting\n", outputDeviceID); return 7; } status = AudioHardwareServiceSetPropertyData(outputDeviceID, &propertyAOPA, 0, NULL, propertySize, &mute); } if (status) { fprintf(stderr, "ERROR: Unable to set volume for device 0x%0x\n", outputDeviceID); return 8; } return 0; }