Ignore UI sounds when auto-pausing.

On macOS, apps are supposed to play UI-related sounds using the "system
default" device. This commit creates a new instance of BGM_Driver, which
BGMApp sets as the system default device. BGMApp ignores audio played to
that device when deciding whether to pause/unpause the user's music
player.

Since UI sounds are short, this helps avoid pausing the music player and
unpausing it shortly after.
This commit is contained in:
Kyle Neideck 2017-07-30 18:16:25 +10:00
parent a6e9179f2d
commit b715212cab
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
39 changed files with 2311 additions and 1054 deletions

View file

@ -244,7 +244,7 @@
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMOutputDevicePrefs.h; path = Preferences/BGMOutputDevicePrefs.h; sourceTree = "<group>"; };
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMOutputDevicePrefs.mm; path = Preferences/BGMOutputDevicePrefs.mm; sourceTree = "<group>"; };
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 1; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = "<group>"; };
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAudioDeviceManager.mm; sourceTree = "<group>"; };
1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMAudioDevice.cpp; sourceTree = "<group>"; };
@ -272,7 +272,7 @@
2743CA1C1D86DA9B0089613B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
275343BF1DFD01BC00DF3858 /* SystemPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemPreferences.h; sourceTree = "<group>"; };
2769728B1CAFCEE8007A2F7C /* post_install.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = post_install.sh; path = BGMXPCHelper/post_install.sh; sourceTree = SOURCE_ROOT; };
2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */ = {isa = PBXFileReference; explicitFileType = text.xml; fileEncoding = 4; name = com.bearisdriving.BGM.XPCHelper.plist.template; path = BGMXPCHelper/com.bearisdriving.BGM.XPCHelper.plist.template; sourceTree = SOURCE_ROOT; };
2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */ = {isa = PBXFileReference; explicitFileType = text.xml; fileEncoding = 1; name = com.bearisdriving.BGM.XPCHelper.plist.template; path = BGMXPCHelper/com.bearisdriving.BGM.XPCHelper.plist.template; sourceTree = SOURCE_ROOT; };
276972901CB16008007A2F7C /* safe_install_dir.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = safe_install_dir.sh; path = BGMXPCHelper/safe_install_dir.sh; sourceTree = SOURCE_ROOT; };
2771700F1CA0C83B00AB34B4 /* BGM_Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_Utils.h; path = ../SharedSource/BGM_Utils.h; sourceTree = "<group>"; };
277170141CA24D7C00AB34B4 /* BGMXPCListenerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCListenerDelegate.h; path = BGMXPCHelper/BGMXPCListenerDelegate.h; sourceTree = SOURCE_ROOT; };

View file

@ -75,6 +75,12 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
[[NSWorkspace sharedWorkspace] removeObserver:self forKeyPath:@"runningApplications" context:nil];
}
// This method allows the Interface Builder Custom Classes for controls (below) to send their values
// directly to BGMDevice. Not public to other classes.
- (BGMAudioDeviceManager*) audioDevices {
return audioDevices;
}
#pragma mark UI Modifications
- (void) insertMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
@ -176,7 +182,7 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
}
}
- (void) setVolumeOfMenuItem:(NSMenuItem*)menuItem fromAppVolumes:(CACFArray&)appVolumes {
- (void) setVolumeOfMenuItem:(NSMenuItem*)menuItem fromAppVolumes:(const CACFArray&)appVolumes {
// Set menuItem's volume slider to the volume of the app in appVolumes that menuItem represents
// Leaves menuItem unchanged if it doesn't match any of the apps in appVolumes
NSRunningApplication* representedApp = menuItem.representedObject;
@ -269,8 +275,8 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
// KVO callback for the apps currently running on the system. Adds/removes the associated menu items.
if ([keyPath isEqualToString:@"runningApplications"]) {
NSArray<NSRunningApplication*>* newApps = [change objectForKey:NSKeyValueChangeNewKey];
NSArray<NSRunningApplication*>* oldApps = [change objectForKey:NSKeyValueChangeOldKey];
NSArray<NSRunningApplication*>* newApps = change[NSKeyValueChangeNewKey];
NSArray<NSRunningApplication*>* oldApps = change[NSKeyValueChangeOldKey];
int changeKind = [[change valueForKey:NSKeyValueChangeKindKey] intValue];
switch (changeKind) {
@ -295,70 +301,6 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
}
}
#pragma mark BGMDevice Communication
- (void) sendVolumeChangeToBGMDevice:(SInt32)newVolume appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
CACFDictionary appVolumeChange(true);
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), (__bridge CFStringRef)appBundleID);
// The values from our sliders are in [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume), newVolume);
CACFArray appVolumeChanges(true);
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
}
- (void) sendPanPositionChangeToBGMDevice:(SInt32)newPanPosition appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
CACFDictionary appVolumeChange(true);
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), (__bridge CFStringRef)appBundleID);
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_PanPosition), newPanPosition);
CACFArray appVolumeChanges(true);
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
}
// This is a temporary solution that lets us control the volumes of some multiprocess apps, i.e.
// apps that play their audio from a process with a different bundle ID.
//
// We can't just check the child processes of the apps' main processes because they're usually
// created with launchd rather than being actual child processes. There's a private API to get the
// processes that an app is "responsible for", so we'll try to use it in the proper fix and only use
// this list if the API doesn't work.
//
// TODO: Consider moving the logic to a new class when we fix this issue properly so this class is
// only responsible for UI.
+ (NSArray<NSString*>*) responsibleBundleIDsOf:(NSString*)parentBundleID {
NSDictionary<NSString*, NSArray<NSString*>*>* bundleIDMap = @{
// Safari
@"com.apple.Safari": @[@"com.apple.WebKit.WebContent"],
// Firefox
@"org.mozilla.firefox": @[@"org.mozilla.plugincontainer"],
// Firefox Nightly
@"org.mozilla.nightly": @[@"org.mozilla.plugincontainer"],
// VMWare Fusion
@"com.vmware.fusion": @[@"com.vmware.vmware-vmx"],
// Parallels
@"com.parallels.desktop.console": @[@"com.parallels.vm"],
// MPlayer OSX Extended
@"hu.mplayerhq.mplayerosx.extended": @[@"ch.sttz.mplayerosx.extended.binaries.officialsvn"]
};
// Parallels' VM "dock helper" apps have bundle IDs like
// com.parallels.winapp.87f6bfc236d64d70a81c47f6243add4c.f5a25fdede514f7aa0a475a1873d3287.fs
if ([parentBundleID hasPrefix:@"com.parallels.winapp."]) {
return @[@"com.parallels.vm"];
}
return bundleIDMap[parentBundleID];
}
@end
#pragma mark Custom Classes (IB)
@ -464,11 +406,11 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
[self snap];
[context sendVolumeChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
for (NSString* bundleID : [BGMAppVolumes responsibleBundleIDsOf:appBundleID]) {
[context sendVolumeChangeToBGMDevice:self.intValue appProcessID:-1 appBundleID:bundleID];
}
// The values from our sliders are in
// [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already.
[context.audioDevices sendAppVolumeToBGMDevice:self.intValue
appProcessID:appProcessID
appBundleID:appBundleID];
}
@end
@ -511,11 +453,10 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue);
[context sendPanPositionChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
for (NSString* bundleID : [BGMAppVolumes responsibleBundleIDsOf:appBundleID]) {
[context sendPanPositionChangeToBGMDevice:self.intValue appProcessID:-1 appBundleID:bundleID];
}
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already.
[context.audioDevices sendAppPanPositionToBGMDevice:self.intValue
appProcessID:appProcessID
appBundleID:appBundleID];
}
@end

View file

@ -61,14 +61,17 @@ BGMAudioDevice::~BGMAudioDevice()
bool BGMAudioDevice::CanBeOutputDeviceInBGMApp() const
{
CFStringRef uid = CopyDeviceUID();
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
bool isNullDevice = CFEqual(uid, CFSTR(kBGMNullDeviceUID));
CFRelease(uid);
bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0;
bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
return !isBGMDevice && !isNullDevice && !IsHidden() && hasOutputChannels && canBeDefault;
return !IsBGMDeviceInstance() &&
!isNullDevice &&
!IsHidden() &&
hasOutputChannels &&
canBeDefault;
}
#pragma mark Available Controls
@ -331,6 +334,27 @@ bool BGMAudioDevice::GetVirtualMasterBalance(AudioObjectPropertyScope inScope
&outVirtualMasterBalance);
}
#pragma mark Implementation
bool BGMAudioDevice::IsBGMDevice(bool inIncludeUISoundsInstance) const
{
bool isBGMDevice = false;
if(GetObjectID() != kAudioObjectUnknown)
{
// Check the device's UID to see whether it's BGMDevice.
CFStringRef uid = CopyDeviceUID();
isBGMDevice =
CFEqual(uid, CFSTR(kBGMDeviceUID)) ||
(inIncludeUISoundsInstance && CFEqual(uid, CFSTR(kBGMDeviceUID_UISounds)));
CFRelease(uid);
}
return isBGMDevice;
}
// static
OSStatus BGMAudioDevice::AHSGetPropertyData(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,

View file

@ -51,7 +51,23 @@ public:
operator AudioObjectID() const { return GetObjectID(); }
/*! @throws CAException */
/*!
@return True if this device is BGMDevice. (Specifically, the main instance of BGMDevice.)
@throws CAException If the HAL returns an error when queried.
*/
bool IsBGMDevice() const { return IsBGMDevice(false); };
/*!
@return True if this device is either the main instance of BGMDevice (the device named
"Background Music") or the instance used for UI sounds (the device named "Background
Music (UI Sounds)").
@throws CAException If the HAL returns an error when queried.
*/
bool IsBGMDeviceInstance() const { return IsBGMDevice(true); };
/*!
@return True if this device can be set as the output device in BGMApp.
@throws CAException If the HAL returns an error when queried.
*/
bool CanBeOutputDeviceInBGMApp() const;
#pragma mark Available Controls
@ -77,7 +93,11 @@ public:
bool GetVirtualMasterBalance(AudioObjectPropertyScope inScope,
Float32& outVirtualMasterBalance) const;
#pragma mark Implementation
private:
bool IsBGMDevice(bool inIncludingUISoundsInstance) const;
static OSStatus AHSGetPropertyData(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,
UInt32* ioDataSize,

View file

@ -17,11 +17,10 @@
// BGMAudioDeviceManager.h
// BGMApp
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
//
// Manages the BGMDevice and the output device. Sets the system's current default device as the
// output device on init, then starts playthrough and mirroring the devices' controls. The output
// device can be changed but the BGMDevice is fixed.
// Manages BGMDevice and the output device. Sets the system's current default device as the output
// device on init, then starts playthrough and mirroring the devices' controls.
//
// PublicUtility Includes
@ -61,6 +60,13 @@ const int kBGMErrorCode_ReturningEarly = 3;
- (BOOL) isOutputDevice:(AudioObjectID)deviceID;
- (BOOL) isOutputDataSource:(UInt32)dataSourceID;
- (void) sendAppVolumeToBGMDevice:(SInt32)newVolume
appProcessID:(pid_t)appProcessID
appBundleID:(NSString*)appBundleID;
- (void) sendAppPanPositionToBGMDevice:(SInt32)newPanPosition
appProcessID:(pid_t)appProcessID
appBundleID:(NSString*)appBundleID;
// Set the audio output device that BGMApp uses.
//
// Returns an error if the output device couldn't be changed. If revertOnFailure is true in that case,
@ -85,7 +91,7 @@ const int kBGMErrorCode_ReturningEarly = 3;
//
// Returns one of the error codes defined by this class or BGMPlayThrough, or an AudioHardware error
// code received from the HAL.
- (OSStatus) startPlayThroughSync;
- (OSStatus) startPlayThroughSync:(BOOL)forUISoundsDevice;
@end

View file

@ -18,6 +18,7 @@
// BGMApp
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2017 Andrew Tonner
//
// Self Include
@ -33,14 +34,18 @@
// PublicUtility Includes
#include "CAHALAudioSystemObject.h"
#include "CAAutoDisposer.h"
#include "CACFDictionary.h"
#include "CACFArray.h"
@implementation BGMAudioDeviceManager {
BGMAudioDevice bgmDevice;
BGMAudioDevice bgmDevice_UISounds;
BGMAudioDevice outputDevice;
BGMDeviceControlSync deviceControlSync;
BGMPlayThrough playThrough;
BGMPlayThrough playThrough_UISounds;
NSRecursiveLock* stateLock;
}
@ -52,8 +57,9 @@
stateLock = [NSRecursiveLock new];
bgmDevice = BGMAudioDevice(CFSTR(kBGMDeviceUID));
bgmDevice_UISounds = BGMAudioDevice(CFSTR(kBGMDeviceUID_UISounds));
if (bgmDevice.GetObjectID() == kAudioObjectUnknown) {
if ((bgmDevice == kAudioObjectUnknown) || (bgmDevice_UISounds == kAudioObjectUnknown)) {
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found");
if (error) {
@ -63,8 +69,14 @@
self = nil;
return self;
}
[self initOutputDevice];
try {
[self initOutputDevice];
} catch (const CAException& e) {
LogError("BGMAudioDeviceManager::initWithError: failed to init output device (%u)",
e.GetError());
outputDevice.SetObjectID(kAudioObjectUnknown);
}
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
LogError("BGMAudioDeviceManager::initWithError: output device not found");
@ -81,64 +93,83 @@
return self;
}
// Throws a CAException if it fails to set the output device.
- (void) initOutputDevice {
CAHALAudioSystemObject audioSystem;
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
AudioObjectID defaultDeviceID = audioSystem.GetDefaultAudioDevice(false, false);
BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);
if (defaultDeviceID == bgmDevice.GetObjectID()) {
// TODO: If BGMDevice is already the default (because BGMApp didn't shutdown properly or it was set manually)
// we should temporarily disable BGMDevice so we can find out what the previous default was.
// For now, just pick the device with the lowest latency
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
if (numDevices > 0) {
SInt32 minLatencyDeviceIdx = -1;
UInt32 minLatency = UINT32_MAX;
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
audioSystem.GetAudioDevices(numDevices, devices);
for (UInt32 i = 0; i < numDevices; i++) {
BGMAudioDevice device(devices[i]);
BOOL isBGMDevice = device.GetObjectID() == bgmDevice.GetObjectID();
BOOL hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
if (!isBGMDevice && hasOutputChannels) {
if (minLatencyDeviceIdx == -1) {
// First, look for any device other than BGMDevice
minLatencyDeviceIdx = i;
} else if (device.GetLatency(false) < minLatency) {
// Then compare the devices by their latencies
minLatencyDeviceIdx = i;
minLatency = device.GetLatency(false);
}
}
}
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
"setOutputDeviceWithID:devices[minLatencyDeviceIdx]", [&]() {
// TODO: On error, try a different output device.
[self setOutputDeviceWithID:devices[minLatencyDeviceIdx] revertOnFailure:NO];
});
}
if (defaultDevice.IsBGMDeviceInstance()) {
// BGMDevice is already the default (it could have been set manually or BGMApp could have
// failed to change it back the last time it closed), so just pick the device with the
// lowest latency.
//
// TODO: Temporarily disable BGMDevice so we can find out what the previous default was and
// use that instead.
[self setOutputDeviceByLatency];
} else {
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
"setOutputDeviceWithID:defaultDeviceID", [&]() {
// TODO: Return the error from setOutputDeviceWithID so it can be returned by initWithError.
[self setOutputDeviceWithID:defaultDeviceID revertOnFailure:NO];
});
// TODO: Return the error from setOutputDeviceWithID so it can be returned by initWithError.
[self setOutputDeviceWithID:defaultDevice revertOnFailure:NO];
}
if (outputDevice == kAudioObjectUnknown) {
LogError("BGMAudioDeviceManager::initOutputDevice: Failed to set output device");
Throw(CAException(kAudioHardwareUnspecifiedError));
}
if (outputDevice.IsBGMDeviceInstance()) {
LogError("BGMAudioDeviceManager::initOutputDevice: Failed to change output device from "
"BGMDevice");
Throw(CAException(kAudioHardwareUnspecifiedError));
}
assert(outputDevice.GetObjectID() != bgmDevice.GetObjectID());
// Log message
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
DebugMsg("BGMAudioDeviceManager::initDevices: Set output device to %s",
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
CFRelease(outputDeviceUID);
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
DebugMsg("BGMAudioDeviceManager::initOutputDevice: Set output device to %s",
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
CFRelease(outputDeviceUID);
}
- (void) setOutputDeviceByLatency {
CAHALAudioSystemObject audioSystem;
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
if (numDevices > 0) {
BGMAudioDevice minLatencyDevice = kAudioObjectUnknown;
UInt32 minLatency = UINT32_MAX;
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
audioSystem.GetAudioDevices(numDevices, devices);
for (UInt32 i = 0; i < numDevices; i++) {
BGMAudioDevice device(devices[i]);
if (!device.IsBGMDeviceInstance()) {
BOOL hasOutputChannels = NO;
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::setOutputDeviceByLatency",
"GetTotalNumberChannels", ([&] {
hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
}));
if (hasOutputChannels) {
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::setOutputDeviceByLatency",
"GetLatency", ([&] {
UInt32 latency = device.GetLatency(false);
if (latency < minLatency) {
minLatencyDevice = devices[i];
minLatency = latency;
}
}));
}
}
}
if (minLatencyDevice != kAudioObjectUnknown) {
// TODO: On error, try a different output device.
[self setOutputDeviceWithID:minLatencyDevice revertOnFailure:NO];
}
}
}
@ -152,14 +183,17 @@
"device to BGMDevice");
CAHALAudioSystemObject audioSystem;
// Copy the device IDs so we can call the HAL without holding stateLock. See startPlayThroughSync.
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
AudioDeviceID bgmDeviceUISoundsID = kAudioObjectUnknown;
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
@try {
[stateLock lock];
bgmDeviceID = bgmDevice.GetObjectID();
bgmDeviceUISoundsID = bgmDevice_UISounds.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
} @finally {
[stateLock unlock];
@ -168,7 +202,8 @@
if (outputDeviceID == kAudioObjectUnknown) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
}
if (bgmDeviceID == kAudioObjectUnknown) {
if ((bgmDeviceID == kAudioObjectUnknown) || (bgmDeviceUISoundsID == kAudioObjectUnknown)) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
}
@ -178,7 +213,11 @@
try {
if (currentDefault == outputDeviceID) {
// The default system device was the same as the default device, so change that as well
audioSystem.SetDefaultAudioDevice(false, true, bgmDeviceID);
//
// Use the UI sounds instance of BGMDevice because the default system output device
// is the device "to use for system related sound". The allows BGMDriver to tell
// when the audio it receives is UI-related.
audioSystem.SetDefaultAudioDevice(false, true, bgmDeviceUISoundsID);
}
audioSystem.SetDefaultAudioDevice(false, false, bgmDeviceID);
@ -199,22 +238,25 @@
bool bgmDeviceIsDefault = true;
bool bgmDeviceIsSystemDefault = true;
// Copy the device IDs so we can call the HAL without holding stateLock. See startPlayThroughSync.
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
AudioDeviceID bgmDeviceUISoundsID = kAudioObjectUnknown;
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
@try {
[stateLock lock];
bgmDeviceID = bgmDevice.GetObjectID();
bgmDeviceUISoundsID = bgmDevice_UISounds.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&]() {
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&] {
bgmDeviceIsDefault =
(audioSystem.GetDefaultAudioDevice(false, false) == bgmDeviceID);
bgmDeviceIsSystemDefault =
(audioSystem.GetDefaultAudioDevice(false, true) == bgmDeviceID);
(audioSystem.GetDefaultAudioDevice(false, true) == bgmDeviceUISoundsID);
});
} @finally {
[stateLock unlock];
@ -280,6 +322,8 @@
}
- (BOOL) isOutputDataSource:(UInt32)dataSourceID {
BOOL isOutputDataSource = NO;
@try {
[stateLock lock];
@ -287,17 +331,107 @@
AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
UInt32 channel = 0;
return outputDevice.HasDataSourceControl(scope, channel) &&
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
isOutputDataSource =
outputDevice.HasDataSourceControl(scope, channel) &&
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
} catch (CAException e) {
BGMLogException(e);
return false;
}
} @finally {
[stateLock unlock];
}
return isOutputDataSource;
}
#pragma mark App Volumes
- (void) sendAppVolumeToBGMDevice:(SInt32)newVolume
appProcessID:(pid_t)appProcessID
appBundleID:(NSString*)appBundleID {
[self sendAppVolumePanToBGMDevice:newVolume
volumeTypeKey:CFSTR(kBGMAppVolumesKey_RelativeVolume)
appProcessID:appProcessID
appBundleID:appBundleID];
}
- (void) sendAppPanPositionToBGMDevice:(SInt32)newPanPosition
appProcessID:(pid_t)appProcessID
appBundleID:(NSString*)appBundleID {
[self sendAppVolumePanToBGMDevice:newPanPosition
volumeTypeKey:CFSTR(kBGMAppVolumesKey_PanPosition)
appProcessID:appProcessID
appBundleID:appBundleID];
}
- (void) sendAppVolumePanToBGMDevice:(SInt32)newValue
volumeTypeKey:(CFStringRef)typeKey
appProcessID:(pid_t)appProcessID
appBundleID:(NSString*)appBundleID {
CACFArray appVolumeChanges(true);
auto addVolumeChange = [&] (pid_t pid, NSString* bundleID) {
CACFDictionary appVolumeChange(true);
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), pid);
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID),
(__bridge CFStringRef)bundleID);
appVolumeChange.AddSInt32(typeKey, newValue);
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
};
addVolumeChange(appProcessID, appBundleID);
// Add the same change for each process the app is responsible for.
for (NSString* responsibleBundleID :
[BGMAudioDeviceManager responsibleBundleIDsOf:appBundleID]) {
// Send -1 as the PID so this volume will only ever be matched by bundle ID.
addVolumeChange(-1, responsibleBundleID);
}
CFPropertyListRef changesPList = appVolumeChanges.AsPropertyList();
// Send the change to BGMDevice.
bgmDevice.SetPropertyData_CFType(kBGMAppVolumesAddress, changesPList);
// Also send it to the instance of BGMDevice that handles UI sounds.
bgmDevice_UISounds.SetPropertyData_CFType(kBGMAppVolumesAddress, changesPList);
}
// This is a temporary solution that lets us control the volumes of some multiprocess apps, i.e.
// apps that play their audio from a process with a different bundle ID.
//
// We can't just check the child processes of the apps' main processes because they're usually
// created with launchd rather than being actual child processes. There's a private API to get the
// processes that an app is "responsible for", so we'll try to use it in the proper fix and only use
// this list if the API doesn't work.
+ (NSArray<NSString*>*) responsibleBundleIDsOf:(NSString*)parentBundleID {
NSDictionary<NSString*, NSArray<NSString*>*>* bundleIDMap = @{
// Safari
@"com.apple.Safari": @[@"com.apple.WebKit.WebContent"],
// Firefox
@"org.mozilla.firefox": @[@"org.mozilla.plugincontainer"],
// Firefox Nightly
@"org.mozilla.nightly": @[@"org.mozilla.plugincontainer"],
// VMWare Fusion
@"com.vmware.fusion": @[@"com.vmware.vmware-vmx"],
// Parallels
@"com.parallels.desktop.console": @[@"com.parallels.vm"],
// MPlayer OSX Extended
@"hu.mplayerhq.mplayerosx.extended": @[@"ch.sttz.mplayerosx.extended.binaries.officialsvn"]
};
// Parallels' VM "dock helper" apps have bundle IDs like
// com.parallels.winapp.87f6bfc236d64d70a81c47f6243add4c.f5a25fdede514f7aa0a475a1873d3287.fs
if ([parentBundleID hasPrefix:@"com.parallels.winapp."]) {
return @[@"com.parallels.vm"];
}
return bundleIDMap[parentBundleID];
}
#pragma mark Output Device
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
revertOnFailure:(BOOL)revertOnFailure {
@ -317,7 +451,7 @@
- (NSError* __nullable) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
dataSourceID:(UInt32* __nullable)dataSourceID
revertOnFailure:(BOOL)revertOnFailure {
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting output device. newDeviceID=%u",
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Setting output device. newDeviceID=%u",
newDeviceID);
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (GetObjectID doesn't throw.)
@ -337,6 +471,7 @@
// Deactivate playthrough rather than stopping it so it can't be started by HAL
// notifications while we're updating deviceControlSync.
playThrough.Deactivate();
playThrough_UISounds.Deactivate();
deviceControlSync.SetDevices(bgmDevice, newOutputDevice);
deviceControlSync.Activate();
@ -346,6 +481,11 @@
playThrough.SetDevices(&bgmDevice, &newOutputDevice);
playThrough.Activate();
// TODO: Support setting different devices as the default output device and the
// default system output device, the way OS X does.
playThrough_UISounds.SetDevices(&bgmDevice_UISounds, &newOutputDevice);
playThrough_UISounds.Activate();
outputDevice = newOutputDevice;
}
@ -361,8 +501,10 @@
// playing. (If we only changed the data source, playthrough will already be running if it
// needs to be.)
playThrough.Start();
playThrough_UISounds.Start();
// But stop playthrough if audio isn't playing, since it uses CPU.
playThrough.StopIfIdle();
playThrough_UISounds.StopIfIdle();
}
} catch (CAException e) {
BGMAssert(e.GetError() != kAudioHardwareNoError,
@ -426,7 +568,7 @@
return [NSError errorWithDomain:@kBGMAppBundleID code:errorCode userInfo:info];
}
- (OSStatus) startPlayThroughSync {
- (OSStatus) startPlayThroughSync:(BOOL)forUISoundsDevice {
// We can only try for stateLock because setOutputDeviceWithID might have already taken it, then made a
// HAL request to BGMDevice and be waiting for the response. Some of the requests setOutputDeviceWithID
// makes to BGMDevice block in the HAL if another thread is in BGM_Device::StartIO.
@ -442,6 +584,8 @@
gotLock = [stateLock tryLock];
if (gotLock) {
BGMPlayThrough& pt = (forUISoundsDevice ? playThrough_UISounds : playThrough);
// Playthrough might not have been notified that BGMDevice is starting yet, so make sure
// playthrough is starting. This way we won't drop any frames while waiting for the HAL to send
// that notification. We can't be completely sure this is safe from deadlocking, though, since
@ -451,27 +595,30 @@
// cause deadlocks.
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::startPlayThroughSync",
"Starting playthrough", [&] {
playThrough.Start();
pt.Start();
});
err = playThrough.WaitForOutputDeviceToStart();
err = pt.WaitForOutputDeviceToStart();
BGMAssert(err != BGMPlayThrough::kDeviceNotStarting, "Playthrough didn't start");
} else {
LogWarning("BGMAudioDeviceManager::startPlayThroughSync: Didn't get state lock. Returning "
"early with kBGMErrorCode_ReturningEarly.");
err = kBGMErrorCode_ReturningEarly;
// TODO: The QOS_CLASS_USER_INTERACTIVE constant isn't available on OS X 10.9.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
@try {
[stateLock lock];
BGMPlayThrough& pt = (forUISoundsDevice ? playThrough_UISounds : playThrough);
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::startPlayThroughSync",
"Starting playthrough (dispatched)", [&] {
playThrough.Start();
pt.Start();
});
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::startPlayThroughSync", [&] {
playThrough.StopIfIdle();
pt.StopIfIdle();
});
} @finally {
[stateLock unlock];

View file

@ -50,10 +50,10 @@ static UInt64 const kPauseDelayNSec = 1500 * NSEC_PER_MSEC;
// immediately if we haven't been paused for long and the non-music-player client stops IO? That would usually indicate that
// it doesn't intend to start playing audio again soon. We'd also have to deal with music players that don't stop IO when
// they're paused.
static UInt64 const kMaxUnpauseDelayNSec = 3000 * NSEC_PER_MSEC;
static UInt64 const kMaxUnpauseDelayNSec = 3500 * NSEC_PER_MSEC;
static UInt64 const kMinUnpauseDelayNSec = kMaxUnpauseDelayNSec / 10;
// We multiply the time spent paused by this factor to calculate the delay before we consider unpausing.
static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
static Float32 const kUnpauseDelayWeightingFactor = 0.1f;
@implementation BGMAutoPauseMusic {
BOOL enabled;
@ -118,7 +118,7 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
// so we have to check them all
for (int i = 0; i < inNumberAddresses; i++) {
if (inAddresses[i].mSelector == kAudioDeviceCustomPropertyDeviceAudibleState) {
SInt32 audibleState = [weakSelf deviceAudibleState];
BGMDeviceAudibleState audibleState = [weakSelf deviceAudibleState];
#if DEBUG
const char audibleStateStr[5] = CA4CCToCString(audibleState);
@ -144,8 +144,8 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
};
}
- (SInt32) deviceAudibleState {
SInt32 audibleState;
- (BGMDeviceAudibleState) deviceAudibleState {
BGMDeviceAudibleState audibleState;
CFNumberRef audibleStateRef =
static_cast<CFNumberRef>([audioDevices bgmDevice].GetPropertyData_CFType(kBGMAudibleStateAddress));
@ -188,8 +188,8 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
// Unpause sooner if we've only been paused for a short time. This is so a notification sound causing an auto-pause is
// less of an interruption.
//
// TODO: Would it help much if we ignored all audio played on the "system default" device rather than the "default"
// device? IIRC apps are supposed to use the former for UI sounds.
// TODO: Fading in and out would make short pauses a lot less jarring because, if they were short enough, we wouldn't
// actually pause the music player. So you'd hear a dip in the music's volume rather than a gap.
UInt64 unpauseDelayNsec =
static_cast<UInt64>((wentSilent - wentAudible) * kUnpauseDelayWeightingFactor);

View file

@ -47,8 +47,7 @@ BGMDeviceControlsList::BGMDeviceControlsList(AudioObjectID inBGMDevice,
mBGMDevice(inBGMDevice),
mAudioSystem(inAudioSystem)
{
BGMAssert((mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)) ||
mBGMDevice.GetObjectID() == kAudioObjectUnknown),
BGMAssert((mBGMDevice.IsBGMDevice() || mBGMDevice.GetObjectID() == kAudioObjectUnknown),
"BGMDeviceControlsList::BGMDeviceControlsList: Given device is not BGMDevice");
#pragma clang diagnostic push
@ -117,7 +116,7 @@ void BGMDeviceControlsList::SetBGMDevice(AudioObjectID inBGMDeviceID)
mBGMDevice = inBGMDeviceID;
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
BGMAssert(mBGMDevice.IsBGMDevice(),
"BGMDeviceControlsList::SetBGMDevice: Given device is not BGMDevice");
}
@ -127,7 +126,7 @@ bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID)
{
CAMutex::Locker locker(mMutex);
if(mBGMDevice.GetObjectID() != mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)))
if(!mBGMDevice.IsBGMDevice())
{
LogWarning("BGMDeviceControlsList::MatchControlsListOf: BGMDevice ID not set");
return false;
@ -291,7 +290,7 @@ void BGMDeviceControlsList::InitDeviceToggling()
return;
}
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
BGMAssert(mBGMDevice.IsBGMDevice(),
"BGMDeviceControlsList::InitDeviceToggling: mBGMDevice device is not set to "
"BGMDevice's ID");
@ -308,7 +307,7 @@ void BGMDeviceControlsList::InitDeviceToggling()
#pragma clang diagnostic pop
mListenerQueue = dispatch_queue_create("com.bearisdriving.BGM.BGMDeviceControlsList", attr);
mListenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
auto listenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
// Ignore the notification if we're not toggling the default device, which would just mean
// the default device has been changed for an unrelated reason.
if(mDeviceToggleState == ToggleState::NotToggling)
@ -339,10 +338,15 @@ void BGMDeviceControlsList::InitDeviceToggling()
mDeviceToggleBlock);
}
break;
default:
break;
}
}
};
mListenerBlock = Block_copy(listenerBlock);
BGMLogAndSwallowExceptions("BGMDeviceControlsList::InitDeviceToggling", [&] {
mAudioSystem.AddPropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
mListenerQueue,
@ -471,8 +475,7 @@ dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
SetNullDeviceEnabled(false);
}));
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
"BGMDevice's AudioObjectID changed");
BGMAssert(mBGMDevice.IsBGMDevice(), "BGMDevice's AudioObjectID changed");
});
}

View file

@ -47,7 +47,7 @@ static const UInt32 kStopIOProcTimeoutInIOCycles = 600;
#pragma mark Construction/Destruction
BGMPlayThrough::BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
BGMPlayThrough::BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice)
:
mInputDevice(inInputDevice),
mOutputDevice(inOutputDevice)
@ -68,7 +68,7 @@ BGMPlayThrough::~BGMPlayThrough()
}
}
void BGMPlayThrough::Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
void BGMPlayThrough::Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice)
{
BGMAssert(mInputDeviceIOProcState.is_lock_free(),
"BGMPlayThrough::BGMPlayThrough: !mInputDeviceIOProcState.is_lock_free()");
@ -151,7 +151,7 @@ void BGMPlayThrough::Activate()
bool isBGMDevice = true;
CATry
isBGMDevice = IsBGMDevice(mInputDevice);
isBGMDevice = mInputDevice.IsBGMDeviceInstance();
CACatch
if(isBGMDevice)
@ -164,7 +164,7 @@ void BGMPlayThrough::Activate()
{
LogWarning("BGMPlayThrough::Activate: Playthrough activated with an output device other "
"than BGMDevice. This hasn't been tested and is almost definitely a bug.");
BGMAssert(false, "BGMPlayThrough::Activate: !IsBGMDevice(mInputDevice)");
BGMAssert(false, "BGMPlayThrough::Activate: !mInputDevice.IsBGMDeviceInstance()");
}
}
}
@ -180,7 +180,7 @@ void BGMPlayThrough::Deactivate()
bool inputDeviceIsBGMDevice = true;
CATry
inputDeviceIsBGMDevice = IsBGMDevice(mInputDevice);
inputDeviceIsBGMDevice = mInputDevice.IsBGMDeviceInstance();
CACatch
// Unregister notification listeners.
@ -188,30 +188,30 @@ void BGMPlayThrough::Deactivate()
{
// There's not much we can do if these calls throw. The docs for AudioObjectRemovePropertyListener
// just say that means it failed.
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
});
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
});
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
mInputDevice.RemovePropertyListener(kBGMRunningSomewhereOtherThanBGMAppAddress,
&BGMPlayThrough::BGMDeviceListenerProc,
this);
});
}
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
Stop();
});
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
DestroyIOProcIDs();
});
@ -240,15 +240,6 @@ void BGMPlayThrough::AllocateBuffer()
mOutputDevice.GetIOBufferSize() * 20);
}
// static
bool BGMPlayThrough::IsBGMDevice(CAHALAudioDevice inDevice)
{
CFStringRef uid = inDevice.CopyDeviceUID();
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
CFRelease(uid);
return isBGMDevice;
}
void BGMPlayThrough::CreateIOProcIDs()
{
CAMutex::Locker stateLocker(mStateMutex);
@ -326,7 +317,7 @@ void BGMPlayThrough::DestroyIOProcIDs()
DebugMsg("BGMPlayThrough::DestroyIOProcIDs: Destroying IOProcs");
auto destroy = [](CAHALAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) {
auto destroy = [](BGMAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) {
#if !DEBUG
#pragma unused (deviceName)
#endif
@ -382,8 +373,8 @@ bool BGMPlayThrough::CheckIOProcsAreStopped() const noexcept
return statesOK;
}
void BGMPlayThrough::SetDevices(CAHALAudioDevice* __nullable inInputDevice,
CAHALAudioDevice* __nullable inOutputDevice)
void BGMPlayThrough::SetDevices(BGMAudioDevice* __nullable inInputDevice,
BGMAudioDevice* __nullable inOutputDevice)
{
CAMutex::Locker stateLocker(mStateMutex);
@ -734,7 +725,7 @@ void BGMPlayThrough::StopIfIdle()
CAMutex::Locker stateLocker(mStateMutex);
BGMAssert(IsBGMDevice(mInputDevice),
BGMAssert(mInputDevice.IsBGMDeviceInstance(),
"BGMDevice not set as input device. StopIfIdle can't tell if other devices are idle.");
if(!IsRunningSomewhereOtherThanBGMApp(mInputDevice))
@ -904,7 +895,7 @@ void BGMPlayThrough::HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlay
}
// static
bool BGMPlayThrough::IsRunningSomewhereOtherThanBGMApp(const CAHALAudioDevice& inBGMDevice)
bool BGMPlayThrough::IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice)
{
return CFBooleanGetValue(
static_cast<CFBooleanRef>(
@ -1078,7 +1069,7 @@ OSStatus BGMPlayThrough::OutputDeviceIOProc(AudioObjectID inDevice,
bool BGMPlayThrough::UpdateIOProcState(const char* __nullable callerName,
std::atomic<IOState>& inState,
AudioDeviceIOProcID __nullable inIOProcID,
CAHALAudioDevice& inDevice,
BGMAudioDevice& inDevice,
IOState& outNewState)
{
BGMAssert(inIOProcID != nullptr, "BGMPlayThrough::UpdateIOProcState: !inIOProcID");

View file

@ -39,9 +39,11 @@
#ifndef BGMApp__BGMPlayThrough
#define BGMApp__BGMPlayThrough
// Local Includes
#include "BGMAudioDevice.h"
// PublicUtility Includes
#include "CARingBuffer.h"
#include "CAHALAudioDevice.h"
#include "CAMutex.h"
// STL Includes
@ -62,7 +64,7 @@ public:
static const OSStatus kDeviceNotStarting = 100;
public:
BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice);
~BGMPlayThrough();
// Disallow copying
BGMPlayThrough(const BGMPlayThrough&) = delete;
@ -76,7 +78,7 @@ public:
private:
/*! @throws CAException */
void Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
void Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice);
public:
/*! @throws CAException */
@ -86,8 +88,6 @@ public:
private:
void AllocateBuffer();
static bool IsBGMDevice(CAHALAudioDevice inDevice);
/*! @throws CAException */
void CreateIOProcIDs();
@ -104,8 +104,8 @@ public:
Pass null for either param to only change one of the devices.
@throws CAException
*/
void SetDevices(CAHALAudioDevice* __nullable inInputDevice,
CAHALAudioDevice* __nullable inOutputDevice);
void SetDevices(BGMAudioDevice* __nullable inInputDevice,
BGMAudioDevice* __nullable inOutputDevice);
/*! @throws CAException */
void Start();
@ -130,7 +130,7 @@ private:
static void HandleBGMDeviceIsRunning(BGMPlayThrough* refCon);
static void HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlayThrough* refCon);
static bool IsRunningSomewhereOtherThanBGMApp(const CAHALAudioDevice& inBGMDevice);
static bool IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice);
static OSStatus InputDeviceIOProc(AudioObjectID inDevice,
const AudioTimeStamp* inNow,
@ -159,7 +159,7 @@ private:
static bool UpdateIOProcState(const char* __nullable callerName,
std::atomic<IOState>& inState,
AudioDeviceIOProcID __nullable inIOProcID,
CAHALAudioDevice& inDevice,
BGMAudioDevice& inDevice,
IOState& outNewState);
static void HandleRingBufferError(CARingBufferError err,
@ -172,8 +172,8 @@ private:
AudioDeviceIOProcID __nullable mInputDeviceIOProcID;
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID;
CAHALAudioDevice mInputDevice { kAudioObjectUnknown };
CAHALAudioDevice mOutputDevice { kAudioObjectUnknown };
BGMAudioDevice mInputDevice { kAudioObjectUnknown };
BGMAudioDevice mOutputDevice { kAudioObjectUnknown };
CAMutex mStateMutex { "Playthrough state" };

View file

@ -177,12 +177,12 @@
return YES;
}
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply {
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI {
NSString* description;
OSStatus err;
try {
err = [audioDevices startPlayThroughSync];
err = [audioDevices startPlayThroughSync:isUI];
} catch (CAException e) {
// startPlayThroughSync should never throw a CAException, but check anyway in case we change that at some point.
LogError("BGMXPCListener::startPlayThroughSyncWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<development version="7000" identifier="xcode"/>
@ -210,7 +210,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
<rect key="frame" x="1" y="1" width="565" height="243"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" id="LSG-PF-cl8">
<rect key="frame" x="-6" y="0.0" width="576.5" height="243"/>
@ -231,7 +231,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
<rect key="frame" x="550" y="1" width="16" height="243"/>
<rect key="frame" x="-15" y="1" width="16" height="0.0"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>

View file

@ -187,7 +187,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
item.toolTip = toolTip;
item.target = self;
item.indentationLevel = 1;
item.representedObject = @{ @"deviceID": [NSNumber numberWithUnsignedInt:device.GetObjectID()],
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
return item;

View file

@ -155,7 +155,7 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
}
}
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply {
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI {
[self debugWarnIfCalledByBGMApp];
// If this reply string isn't set before the end of this method, it's a bug
@ -173,7 +173,7 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
[remoteObjectProxy startPlayThroughSyncWithReply:^(NSError* bgmAppReply) {
replyToBGMDriver = bgmAppReply;
dispatch_semaphore_signal(bgmAppReplySemaphore);
}];
} forUISoundsDevice:isUI];
} errorHandler:^(NSError* error) {
replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_MessageFailure
description:[error localizedDescription]

View file

@ -67,7 +67,7 @@
@"Check that BGMApp isn't running, which would cause this failure");
dispatch_semaphore_signal(replySemaphore);
}];
} forUISoundsDevice:NO];
if (0 != dispatch_semaphore_wait(replySemaphore, dispatch_time(DISPATCH_TIME_NOW, kStartIOTimeoutNsec))) {
XCTFail(@"Timed out waiting for BGMXPCHelper");

View file

@ -7,12 +7,20 @@
objects = {
/* Begin PBXBuildFile section */
19FE742AEBE30B21C4CF9285 /* BGM_Control.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */; };
19FE761291BF07AEA278F25C /* BGM_MuteControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */; };
19FE766482B57D852CCF6F0A /* BGM_MuteControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */; };
19FE77D40F15EA060B462D83 /* BGM_Control.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */; };
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B01C642C600084C15A /* BGM_Client.cpp */; };
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B21C642C600084C15A /* BGM_ClientMap.cpp */; };
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B41C642C600084C15A /* BGM_Clients.cpp */; };
1C30A69F1C1E98F000C05AA5 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; };
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */; };
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */; };
1C7010751F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */; };
1C7010761F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */; };
1C7010791F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */; };
1C70107A1F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */; };
1C8034DD1BDD073B00668E00 /* BGM_ClientsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */; };
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; };
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */; };
@ -71,6 +79,10 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
19FE7431C588F36F4F1E70BB /* BGM_MuteControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_MuteControl.h; sourceTree = "<group>"; };
19FE7B8CE9148B3D8D7517C6 /* BGM_Control.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_Control.h; sourceTree = "<group>"; };
19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_Control.cpp; sourceTree = "<group>"; };
19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_MuteControl.cpp; sourceTree = "<group>"; };
1C0CB6A61C4E06C00084C15A /* CAAtomicStack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAtomicStack.h; path = PublicUtility/CAAtomicStack.h; sourceTree = "<group>"; };
1C0CB6A71C4E06F70084C15A /* CAAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAtomic.h; path = PublicUtility/CAAtomic.h; sourceTree = "<group>"; };
1C0CB6A91C50A3AF0084C15A /* CAAutoDisposer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAutoDisposer.h; path = PublicUtility/CAAutoDisposer.h; sourceTree = "<group>"; };
@ -89,6 +101,10 @@
1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
1C3821101C4A18DE00A0C8C6 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_DeviceTests.mm; sourceTree = "<group>"; };
1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_AudibleState.cpp; sourceTree = "<group>"; };
1C7010741F05ED5100D8CCDC /* BGM_AudibleState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_AudibleState.h; sourceTree = "<group>"; };
1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_VolumeControl.cpp; sourceTree = "<group>"; };
1C7010781F07A0BA00D8CCDC /* BGM_VolumeControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_VolumeControl.h; sourceTree = "<group>"; };
1C8034DA1BDD073B00668E00 /* BGMDriverTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMDriverTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_ClientsTests.mm; sourceTree = "<group>"; };
1C8034DE1BDD073B00668E00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -235,10 +251,18 @@
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */,
1CB8B37F1BBCCF87000E2DD1 /* BGM_Device.h */,
1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */,
1C7010741F05ED5100D8CCDC /* BGM_AudibleState.h */,
1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */,
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */,
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */,
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */,
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */,
19FE7B8CE9148B3D8D7517C6 /* BGM_Control.h */,
19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */,
1C7010781F07A0BA00D8CCDC /* BGM_VolumeControl.h */,
1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */,
19FE7431C588F36F4F1E70BB /* BGM_MuteControl.h */,
19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */,
1C0CB6AF1C642C600084C15A /* DeviceClients */,
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */,
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */,
@ -453,6 +477,7 @@
27E6B5F01E01966A00EC0AAB /* BGM_Utils.cpp in Sources */,
277170101CA0CFC300AB34B4 /* BGM_PlugInInterface.cpp in Sources */,
277170111CA0CFC300AB34B4 /* CACFNumber.cpp in Sources */,
1C7010761F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */,
27D643C31C9FBE1600737F6E /* BGM_XPCHelper.m in Sources */,
27379B821C76D62D0084A24C /* CADebugMacros.cpp in Sources */,
27379B831C76D62D0084A24C /* CADebugPrintf.cpp in Sources */,
@ -465,6 +490,7 @@
277EE65E1C728C9D0037F1EE /* BGM_PlugIn.cpp in Sources */,
277EE65F1C728C9D0037F1EE /* CAPThread.cpp in Sources */,
277EE65A1C728C630037F1EE /* BGM_Client.cpp in Sources */,
1C70107A1F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */,
277EE65B1C728C630037F1EE /* BGM_ClientMap.cpp in Sources */,
277EE65C1C728C630037F1EE /* BGM_Clients.cpp in Sources */,
277EE65D1C728C630037F1EE /* BGM_TaskQueue.cpp in Sources */,
@ -476,6 +502,8 @@
1CC1DF8D1BE5705700FB8FE4 /* CACFDictionary.cpp in Sources */,
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */,
1C8034DD1BDD073B00668E00 /* BGM_ClientsTests.mm in Sources */,
19FE761291BF07AEA278F25C /* BGM_MuteControl.cpp in Sources */,
19FE742AEBE30B21C4CF9285 /* BGM_Control.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -484,6 +512,7 @@
buildActionMask = 2147483647;
files = (
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */,
1C7010751F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */,
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */,
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */,
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */,
@ -491,12 +520,15 @@
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */,
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */,
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */,
1C7010791F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */,
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */,
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */,
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */,
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */,
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */,
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */,
19FE766482B57D852CCF6F0A /* BGM_MuteControl.cpp in Sources */,
19FE77D40F15EA060B462D83 /* BGM_Control.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -760,6 +792,7 @@
2743C9C81D7EF84B0089613B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
@ -788,6 +821,7 @@
2743C9C91D7EF84B0089613B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;

View file

@ -0,0 +1,212 @@
// 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/>.
//
// BGM_AudibleState.cpp
// BGMDriver
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2016 Josh Junon
//
// Self Include
#include "BGM_AudibleState.h"
// PublicUtility Includes
#include "CADebugMacros.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wsign-conversion"
#include "CAAtomic.h"
#pragma clang diagnostic pop
// STL Includes
#include <algorithm> // For std::min and std::max.
// TODO: This is just the first value I tried.
static const Float32 kSampleVolumeMarginRaw = 0.0001f;
BGM_AudibleState::BGM_AudibleState()
:
mState(kBGMDeviceIsSilent),
mSampleTimes({0, 0, 0, 0})
{
}
BGMDeviceAudibleState BGM_AudibleState::GetState() const noexcept
{
CAMemoryBarrier(); // Probably unnecessary.
return mState;
}
void BGM_AudibleState::Reset() noexcept
{
mState = kBGMDeviceIsSilent;
mSampleTimes.latestSilent = 0;
mSampleTimes.latestAudibleNonMusic = 0;
mSampleTimes.latestSilentMusic = 0;
mSampleTimes.latestAudibleMusic = 0;
}
void BGM_AudibleState::UpdateWithClientIO(bool inClientIsMusicPlayer,
UInt32 inIOBufferFrameSize,
Float64 inOutputSampleTime,
const Float32* inBuffer)
{
// Update the sample times of the most recent audible music, silent music and audible non-music
// samples we've received.
Float64 endFrameSampleTime = inOutputSampleTime + inIOBufferFrameSize - 1;
if(inClientIsMusicPlayer)
{
if(BufferIsAudible(inIOBufferFrameSize, inBuffer))
{
mSampleTimes.latestAudibleMusic = std::max(mSampleTimes.latestAudibleMusic,
endFrameSampleTime);
}
else
{
mSampleTimes.latestSilentMusic = std::max(mSampleTimes.latestSilentMusic,
endFrameSampleTime);
}
}
else if(endFrameSampleTime > mSampleTimes.latestAudibleNonMusic && // Don't bother checking the
// buffer if it won't change
// anything.
BufferIsAudible(inIOBufferFrameSize, inBuffer))
{
mSampleTimes.latestAudibleNonMusic = std::max(mSampleTimes.latestAudibleNonMusic,
endFrameSampleTime);
}
}
bool BGM_AudibleState::UpdateWithMixedIO(UInt32 inIOBufferFrameSize,
Float64 inOutputSampleTime,
const Float32* inBuffer)
{
// Update the sample time of the most recent silent sample we've received. (The music player
// client is not considered separate for the latest silent sample.)
bool audible = BufferIsAudible(inIOBufferFrameSize, inBuffer);
// The sample time of the last frame we're looking at.
Float64 endFrameSampleTime = inOutputSampleTime + inIOBufferFrameSize - 1;
if(!audible)
{
mSampleTimes.latestSilent = std::max(mSampleTimes.latestSilent, endFrameSampleTime);
}
return RecalculateState(endFrameSampleTime);
}
bool BGM_AudibleState::RecalculateState(Float64 inEndFrameSampleTime)
{
Float64 sinceLatestSilent = inEndFrameSampleTime - mSampleTimes.latestSilent;
Float64 sinceLatestMusicSilent = inEndFrameSampleTime - mSampleTimes.latestSilentMusic;
Float64 sinceLatestAudible = inEndFrameSampleTime - mSampleTimes.latestAudibleNonMusic;
Float64 sinceLatestMusicAudible = inEndFrameSampleTime - mSampleTimes.latestAudibleMusic;
bool didChangeState = false;
// Update mState
// Change from silent/silentExceptMusic to audible
if(mState != kBGMDeviceIsAudible &&
sinceLatestSilent >= kDeviceAudibleStateMinChangedFramesForUpdate &&
// Check that non-music audio is currently playing
sinceLatestAudible <= 0 && mSampleTimes.latestAudibleNonMusic != 0)
{
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
"kAudioDeviceCustomPropertyDeviceAudibleState to audible");
mState = kBGMDeviceIsAudible;
CAMemoryBarrier();
didChangeState = true;
}
// Change from silent to silentExceptMusic
else if(((mState == kBGMDeviceIsSilent &&
sinceLatestMusicSilent >= kDeviceAudibleStateMinChangedFramesForUpdate) ||
// ...or from audible to silentExceptMusic
(mState == kBGMDeviceIsAudible &&
sinceLatestAudible >= kDeviceAudibleStateMinChangedFramesForUpdate &&
sinceLatestMusicSilent >= kDeviceAudibleStateMinChangedFramesForUpdate)) &&
// In case we haven't seen any music samples yet (either audible or silent), check that
// music is currently playing
sinceLatestMusicAudible <= 0 && mSampleTimes.latestAudibleMusic != 0)
{
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
"kAudioDeviceCustomPropertyDeviceAudibleState to silent except music");
mState = kBGMDeviceIsSilentExceptMusic;
CAMemoryBarrier();
didChangeState = true;
}
// Change from audible/silentExceptMusic to silent
else if(mState != kBGMDeviceIsSilent &&
sinceLatestAudible >= kDeviceAudibleStateMinChangedFramesForUpdate &&
sinceLatestMusicAudible >= kDeviceAudibleStateMinChangedFramesForUpdate)
{
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
"kAudioDeviceCustomPropertyDeviceAudibleState to silent");
mState = kBGMDeviceIsSilent;
CAMemoryBarrier();
didChangeState = true;
}
return didChangeState;
}
// static
bool BGM_AudibleState::BufferIsAudible(UInt32 inIOBufferFrameSize, const Float32* inBuffer)
{
// Check each frame to see if any are audible. This could be much more accurate, but seems to
// work well enough for now.
//
// The trade off here is between pausing the music player at the wrong time and unpausing it at
// the wrong time. If a short sound (e.g. a UI alert) plays but has a long, barely-audible tail,
// we might not detect the silence quickly enough and pause the music player. Similarly, if
// we've paused the music player and there's a period of near-silence in the new audio, we might
// unpause the music and briefly interrupt the new audio.
//
// A fairly long period of silence before unpausing the music player isn't a big problem, which
// means BGMApp can wait much longer before unpausing than before pausing. So this function errs
// toward considering the buffer silent, which helps BGMApp ignore short sounds.
if(inIOBufferFrameSize > 0)
{
// Bounds for the left channel samples.
Float32 firstSampleLLower = inBuffer[0] - kSampleVolumeMarginRaw;
Float32 firstSampleLUpper = inBuffer[0] + kSampleVolumeMarginRaw;
// Bounds for the right channel samples.
Float32 firstSampleRLower = inBuffer[1] - kSampleVolumeMarginRaw;
Float32 firstSampleRUpper = inBuffer[1] + kSampleVolumeMarginRaw;
for(UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2)
{
bool audibleL =
(inBuffer[i] < firstSampleLLower) || (inBuffer[i] > firstSampleLUpper);
bool audibleR =
(inBuffer[i + 1] < firstSampleRLower) || (inBuffer[i + 1] > firstSampleRUpper);
if(audibleL || audibleR)
{
return true;
}
}
}
return false;
}

View file

@ -0,0 +1,103 @@
// 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/>.
//
// BGM_AudibleState.h
// BGMDriver
//
// Copyright © 2016, 2017 Kyle Neideck
//
// Inspects a stream of audio data and reports whether it's silent, silent except for the user's
// music player, or audible.
//
// See kAudioDeviceCustomPropertyDeviceAudibleState and the BGMDeviceAudibleState enum in
// BGM_Types.h for more info.
//
// Not thread-safe.
//
#ifndef BGMDriver__BGM_AudibleState
#define BGMDriver__BGM_AudibleState
// Local Includes
#include "BGM_Types.h"
// System Includes
#include <MacTypes.h>
#pragma clang assume_nonnull begin
class BGM_AudibleState
{
public:
BGM_AudibleState();
/*!
@return The current audible state of the device, to be used as the value of the
kAudioDeviceCustomPropertyDeviceAudibleState property.
*/
BGMDeviceAudibleState GetState() const noexcept;
/*! Set the audible state back to kBGMDeviceIsSilent and ignore all previous IO. */
void Reset() noexcept;
/*!
Read an audio buffer sent by a single device client (i.e. a process playing audio) and update
the audible state. The update will only affect the return value of GetState after the next
call to UpdateWithMixedIO, when all IO for the cycle has been read.
Real-time safe. Not thread safe.
*/
void UpdateWithClientIO(bool inClientIsMusicPlayer,
UInt32 inIOBufferFrameSize,
Float64 inOutputSampleTime,
const Float32* inBuffer);
/*!
Read a fully mixed audio buffer and update the audible state. All client (unmixed) buffers for
the same cycle must be read with UpdateWithClientIO before calling this function.
Real-time safe. Not thread safe.
@return True if the audible state changed.
*/
bool UpdateWithMixedIO(UInt32 inIOBufferFrameSize,
Float64 inOutputSampleTime,
const Float32* inBuffer);
private:
bool RecalculateState(Float64 inEndFrameSampleTime);
static bool BufferIsAudible(UInt32 inIOBufferFrameSize,
const Float32* inBuffer);
private:
BGMDeviceAudibleState mState;
struct
{
Float64 latestAudibleNonMusic;
Float64 latestSilent;
Float64 latestAudibleMusic;
Float64 latestSilentMusic;
} mSampleTimes;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_AudibleState */

View file

@ -0,0 +1,182 @@
// 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/>.
//
// BGM_Control.cpp
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Self Include
#include "BGM_Control.h"
// PublicUtility Includes
#include "CADebugMacros.h"
#include "CAException.h"
// System Includes
#include <CoreAudio/AudioHardwareBase.h>
#pragma clang assume_nonnull begin
BGM_Control::BGM_Control(AudioObjectID inObjectID,
AudioClassID inClassID,
AudioClassID inBaseClassID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement)
:
BGM_Object(inObjectID, inClassID, inBaseClassID, inOwnerObjectID),
mScope(inScope),
mElement(inElement)
{
}
bool BGM_Control::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioControlPropertyScope:
case kAudioControlPropertyElement:
theAnswer = true;
break;
default:
theAnswer = BGM_Object::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_Control::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioControlPropertyScope:
case kAudioControlPropertyElement:
theAnswer = false;
break;
default:
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_Control::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const
{
CheckObjectID(inObjectID);
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioControlPropertyScope:
theAnswer = sizeof(AudioObjectPropertyScope);
break;
case kAudioControlPropertyElement:
theAnswer = sizeof(AudioObjectPropertyElement);
break;
default:
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_Control::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
CheckObjectID(inObjectID);
switch(inAddress.mSelector)
{
case kAudioControlPropertyScope:
// This property returns the scope that the control is attached to.
ThrowIf(inDataSize < sizeof(AudioObjectPropertyScope),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Control::GetPropertyData: not enough space for the return value of "
"kAudioControlPropertyScope for the control");
*reinterpret_cast<AudioObjectPropertyScope*>(outData) = mScope;
outDataSize = sizeof(AudioObjectPropertyScope);
break;
case kAudioControlPropertyElement:
// This property returns the element that the control is attached to.
ThrowIf(inDataSize < sizeof(AudioObjectPropertyElement),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Control::GetPropertyData: not enough space for the return value of "
"kAudioControlPropertyElement for the control");
*reinterpret_cast<AudioObjectPropertyElement*>(outData) = mElement;
outDataSize = sizeof(AudioObjectPropertyElement);
break;
default:
BGM_Object::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
void BGM_Control::CheckObjectID(AudioObjectID inObjectID) const
{
ThrowIf(inObjectID == kAudioObjectUnknown || inObjectID != GetObjectID(),
CAException(kAudioHardwareBadObjectError),
"BGM_Control::CheckObjectID: wrong audio object ID for the control");
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,87 @@
// 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/>.
//
// BGM_Control.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
// An AudioObject that represents a user-controllable aspect of a device or stream, such as volume
// or balance.
//
//
#ifndef BGMDriver__BGM_Control
#define BGMDriver__BGM_Control
// Superclass Includes
#include "BGM_Object.h"
#pragma clang assume_nonnull begin
class BGM_Control
:
public BGM_Object
{
protected:
BGM_Control(AudioObjectID inObjectID,
AudioClassID inClassID,
AudioClassID inBaseClassID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement
= kAudioObjectPropertyElementMaster);
#pragma mark Property Operations
public:
virtual bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const;
virtual void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
#pragma mark Implementation
protected:
void CheckObjectID(AudioObjectID inObjectID) const;
protected:
const AudioObjectPropertyScope mScope;
const AudioObjectPropertyElement mElement;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_Control */

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,10 @@
#include "BGM_WrappedAudioEngine.h"
#include "BGM_Clients.h"
#include "BGM_TaskQueue.h"
#include "BGM_AudibleState.h"
#include "BGM_Stream.h"
#include "BGM_VolumeControl.h"
#include "BGM_MuteControl.h"
// PublicUtility Includes
#include "CAMutex.h"
@ -55,12 +58,26 @@ class BGM_Device
public:
static BGM_Device& GetInstance();
static BGM_Device& GetUISoundsInstance();
private:
static void StaticInitializer();
protected:
BGM_Device();
BGM_Device(AudioObjectID inObjectID,
const CFStringRef __nonnull inDeviceName,
const CFStringRef __nonnull inDeviceUID,
const CFStringRef __nonnull inDeviceModelUID,
AudioObjectID inInputStreamID,
AudioObjectID inOutputStreamID);
BGM_Device(AudioObjectID inObjectID,
const CFStringRef __nonnull inDeviceName,
const CFStringRef __nonnull inDeviceUID,
const CFStringRef __nonnull inDeviceModelUID,
AudioObjectID inInputStreamID,
AudioObjectID inOutputStreamID,
AudioObjectID inOutputVolumeControlID,
AudioObjectID inOutputMuteControlID);
virtual ~BGM_Device();
virtual void Activate();
@ -87,15 +104,6 @@ private:
void Device_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
void Device_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
#pragma mark Control Property Operations
private:
bool Control_HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
bool Control_IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
UInt32 Control_GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData) const;
void Control_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
void Control_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
#pragma mark IO Operations
public:
@ -113,10 +121,6 @@ private:
void ReadInputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, void* __nonnull outBuffer);
void WriteOutputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, const void* __nonnull inBuffer);
void ApplyClientRelativeVolume(UInt32 inClientID, UInt32 inIOBufferFrameSize, void* __nonnull inBuffer) const;
bool BufferIsAudible(UInt32 inIOBufferFrameSize, const void* __nonnull inBuffer);
void UpdateAudibleStateSampleTimes_PreMix(UInt32 inClientID, UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
void UpdateAudibleStateSampleTimes_PostMix(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
void UpdateDeviceAudibleState(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime);
#pragma mark Accessors
@ -133,6 +137,22 @@ public:
void RequestSampleRate(Float64 inRequestedSampleRate);
private:
/*!
@return The Audio Object that has the ID inObjectID and belongs to this device.
@throws CAException if there is no such Audio Object.
*/
const BGM_Object& GetOwnedObjectByID(AudioObjectID inObjectID) const;
BGM_Object& GetOwnedObjectByID(AudioObjectID inObjectID);
/*! @return The number of Audio Objects belonging to this device, e.g. streams and controls. */
UInt32 GetNumberOfSubObjects() const;
/*! @return The number of Audio Objects with output scope belonging to this device. */
UInt32 GetNumberOfOutputSubObjects() const;
/*!
@return The number of control Audio Objects with output scope belonging to this device, e.g.
output volume and mute controls.
*/
UInt32 GetNumberOfOutputControls() const;
/*!
Enable or disable the device's volume and/or mute controls.
@ -154,13 +174,7 @@ private:
void SetSampleRate(Float64 inNewSampleRate);
/*! @return True if inObjectID is the ID of one of this device's streams. */
bool IsStreamID(AudioObjectID inObjectID) const noexcept;
/*!
@return The stream that has the ID inObjectID and belongs to this device.
@throws CAException if there is no such stream (i.e. if inObjectID is neither
kObjectID_Stream_Input nor kObjectID_Stream_Output.)
*/
const BGM_Stream& GetStreamByID(AudioObjectID inObjectID) const;
inline bool IsStreamID(AudioObjectID inObjectID) const noexcept;
#pragma mark Hardware Accessors
@ -172,15 +186,11 @@ private:
Float64 _HW_GetSampleRate() const;
kern_return_t _HW_SetSampleRate(Float64 inNewSampleRate);
UInt32 _HW_GetRingBufferFrameSize() const;
SInt32 _HW_GetVolumeControlValue(AudioObjectID inObjectID) const;
kern_return_t _HW_SetVolumeControlValue(AudioObjectID inObjectID, SInt32 inNewControlValue);
UInt32 _HW_GetMuteControlValue(AudioObjectID inObjectID) const;
kern_return_t _HW_SetMuteControlValue(AudioObjectID inObjectID, UInt32 inValue);
#pragma mark Implementation
public:
CFStringRef __nonnull CopyDeviceUID() const { return CFSTR(kBGMDeviceUID); }
CFStringRef __nonnull CopyDeviceUID() const { return mDeviceUID; }
void AddClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
void RemoveClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
/*!
@ -194,25 +204,29 @@ public:
private:
static pthread_once_t sStaticInitializer;
static BGM_Device* __nonnull sInstance;
static BGM_Device* __nonnull sUISoundsInstance;
#define kDeviceName "Background Music"
#define kDeviceName_UISounds "Background Music (UI Sounds)"
#define kDeviceManufacturerName "Background Music contributors"
const CFStringRef __nonnull mDeviceName;
const CFStringRef __nonnull mDeviceUID;
const CFStringRef __nonnull mDeviceModelUID;
enum
{
kNumberOfSubObjects = 4,
// The number of global/output sub-objects varies because the controls can be disabled.
kNumberOfInputSubObjects = 1,
kNumberOfOutputSubObjects = 3,
kNumberOfStreams = 2,
kNumberOfInputStreams = 1,
kNumberOfOutputStreams = 1
};
CAMutex mStateMutex;
CAMutex mIOMutex;
UInt64 __unused mSampleRateShadow; // Currently unused.
const Float64 kSampleRateDefault = 44100.0;
// Before we can change sample rate, the host has to stop the device. The new sample rate is
// stored here while it does.
@ -236,15 +250,8 @@ private:
BGM_Stream mInputStream;
BGM_Stream mOutputStream;
SInt32 mDeviceAudibleState;
struct
{
Float64 latestAudibleNonMusic;
Float64 latestSilent;
Float64 latestAudibleMusic;
Float64 latestSilentMusic;
} mAudibleStateSampleTimes;
BGM_AudibleState mAudibleState;
enum class ChangeAction : UInt64
{
@ -252,25 +259,10 @@ private:
SetEnabledControls
};
// This volume range will be used when the BGMDevice isn't wrapping another device (or we fail to
// get the range of the wrapped device for some reason).
#define kDefaultMinRawVolumeValue 0
#define kDefaultMaxRawVolumeValue 96
#define kDefaultMinDbVolumeValue -96.0f
#define kDefaultMaxDbVolumeValue 0.0f
bool mOutputVolumeControlEnabled = true;
bool mOutputMuteControlEnabled = true;
BGM_VolumeControl mVolumeControl;
BGM_MuteControl mMuteControl;
bool mPendingOutputVolumeControlEnabled = true;
bool mPendingOutputMuteControlEnabled = true;
SInt32 mOutputMasterVolumeControlRawValueShadow;
SInt32 mOutputMasterMinRawVolumeShadow;
SInt32 mOutputMasterMaxRawVolumeShadow;
Float32 mOutputMasterMinDbVolumeShadow;
Float32 mOutputMasterMaxDbVolumeShadow;
CAVolumeCurve mVolumeCurve;
UInt32 mOutputMuteValueShadow;
};

View file

@ -0,0 +1,225 @@
// 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/>.
//
// BGM_MuteControl.cpp
// BGMDriver
//
// Copyright © 2016, 2017 Kyle Neideck
//
// Self Include
#include "BGM_MuteControl.h"
// Local Includes
#include "BGM_PlugIn.h"
// PublicUtility Includes
#include "CADebugMacros.h"
#include "CAException.h"
#include "CADispatchQueue.h"
#pragma clang assume_nonnull begin
#pragma mark Construction/Destruction
BGM_MuteControl::BGM_MuteControl(AudioObjectID inObjectID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement)
:
BGM_Control(inObjectID,
kAudioMuteControlClassID,
kAudioBooleanControlClassID,
inOwnerObjectID,
inScope,
inElement),
mMutex("Mute Control"),
mMuted(false)
{
}
#pragma mark Property Operations
bool BGM_MuteControl::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioBooleanControlPropertyValue:
theAnswer = true;
break;
default:
theAnswer = BGM_Control::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_MuteControl::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioBooleanControlPropertyValue:
theAnswer = true;
break;
default:
theAnswer = BGM_Control::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_MuteControl::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const
{
CheckObjectID(inObjectID);
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioBooleanControlPropertyValue:
theAnswer = sizeof(UInt32);
break;
default:
theAnswer = BGM_Control::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_MuteControl::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
CheckObjectID(inObjectID);
switch(inAddress.mSelector)
{
case kAudioBooleanControlPropertyValue:
// This returns the mute value of the control.
{
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_MuteControl::GetPropertyData: not enough space for the return value "
"of kAudioBooleanControlPropertyValue for the mute control");
CAMutex::Locker theLocker(mMutex);
// Non-zero for true, which means audio is being muted.
*reinterpret_cast<UInt32*>(outData) = mMuted ? 1 : 0;
outDataSize = sizeof(UInt32);
}
break;
default:
BGM_Control::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
void BGM_MuteControl::SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData)
{
CheckObjectID(inObjectID);
switch(inAddress.mSelector)
{
case kAudioBooleanControlPropertyValue:
{
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_MuteControl::SetPropertyData: wrong size for the data for "
"kAudioBooleanControlPropertyValue");
CAMutex::Locker theLocker(mMutex);
// Non-zero for true, meaning audio will be muted.
bool theNewMuted = (*reinterpret_cast<const UInt32*>(inData) != 0);
if(mMuted != theNewMuted)
{
mMuted = theNewMuted;
// Send notifications.
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperty[1];
theChangedProperty[0] = {
kAudioBooleanControlPropertyValue, mScope, mElement
};
BGM_PlugIn::Host_PropertiesChanged(inObjectID, 1, theChangedProperty);
});
}
}
break;
default:
BGM_Control::SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
break;
};
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,97 @@
// 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/>.
//
// BGM_MuteControl.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
#ifndef BGMDriver__BGM_MuteControl
#define BGMDriver__BGM_MuteControl
// Superclass Includes
#include "BGM_Control.h"
// PublicUtility Includes
#include "CAMutex.h"
// System Includes
#include <MacTypes.h>
#include <CoreAudio/CoreAudio.h>
#pragma clang assume_nonnull begin
class BGM_MuteControl
:
public BGM_Control
{
#pragma mark Construction/Destruction
public:
BGM_MuteControl(AudioObjectID inObjectID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement =
kAudioObjectPropertyElementMaster);
#pragma mark Property Operations
bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const;
void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
void SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData);
#pragma mark Implementation
private:
CAMutex mMutex;
bool mMuted;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_MuteControl */

View file

@ -381,6 +381,20 @@ void BGM_NullDevice::SetPropertyData(AudioObjectID inObjectID,
inDataSize,
inData);
}
else if(inObjectID == GetObjectID())
{
BGM_AbstractDevice::SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
}
else
{
Throw(CAException(kAudioHardwareBadObjectError));
}
}
#pragma mark IO Operations

View file

@ -101,7 +101,6 @@ bool BGM_Object::IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID,
default:
Throw(CAException(kAudioHardwareUnknownPropertyError));
break;
};
return theAnswer;
}
@ -128,7 +127,6 @@ UInt32 BGM_Object::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
default:
Throw(CAException(kAudioHardwareUnknownPropertyError));
break;
};
return theAnswer;
}
@ -170,7 +168,6 @@ void BGM_Object::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
default:
Throw(CAException(kAudioHardwareUnknownPropertyError));
break;
};
}
@ -182,7 +179,6 @@ void BGM_Object::SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
{
default:
Throw(CAException(kAudioHardwareUnknownPropertyError));
break;
};
}

View file

@ -140,7 +140,9 @@ UInt32 BGM_PlugIn::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
case kAudioObjectPropertyOwnedObjects:
case kAudioPlugInPropertyDeviceList:
theAnswer = (BGM_NullDevice::GetInstance().IsActive() ? 2 : 1) * sizeof(AudioObjectID);
// The plug-in owns the main BGM_Device, the instance of BGM_Device that handles UI
// sounds and, if it's enabled, the null device.
theAnswer = (BGM_NullDevice::GetInstance().IsActive() ? 3 : 2) * sizeof(AudioObjectID);
break;
case kAudioPlugInPropertyTranslateUIDToDevice:
@ -181,12 +183,32 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
case kAudioPlugInPropertyDeviceList:
{
AudioObjectID* theReturnedDeviceList = reinterpret_cast<AudioObjectID*>(outData);
if((inDataSize >= 2 * sizeof(AudioObjectID)) && BGM_NullDevice::GetInstance().IsActive())
if(inDataSize >= 3 * sizeof(AudioObjectID))
{
if(BGM_NullDevice::GetInstance().IsActive())
{
theReturnedDeviceList[0] = kObjectID_Device;
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
theReturnedDeviceList[2] = kObjectID_Device_Null;
// say how much we returned
outDataSize = 3 * sizeof(AudioObjectID);
}
else
{
theReturnedDeviceList[0] = kObjectID_Device;
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
// say how much we returned
outDataSize = 2 * sizeof(AudioObjectID);
}
}
else if(inDataSize >= 2 * sizeof(AudioObjectID))
{
theReturnedDeviceList[0] = kObjectID_Device;
theReturnedDeviceList[1] = kObjectID_Device_Null;
// say how much we returned
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
// say how much we returned
outDataSize = 2 * sizeof(AudioObjectID);
}
else if(inDataSize >= sizeof(AudioObjectID))
@ -218,6 +240,12 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
"kAudioPlugInPropertyTranslateUIDToDevice");
*outID = kObjectID_Device;
}
else if(CFEqual(theUID, BGM_Device::GetUISoundsInstance().CopyDeviceUID()))
{
DebugMsg("BGM_PlugIn::GetPropertyData: Returning BGMUISoundsDevice for "
"kAudioPlugInPropertyTranslateUIDToDevice");
*outID = kObjectID_Device_UI_Sounds;
}
else if(BGM_NullDevice::GetInstance().IsActive() &&
CFEqual(theUID, BGM_NullDevice::GetInstance().CopyDeviceUID()))
{

View file

@ -98,6 +98,7 @@ static AudioServerPlugInDriverInterface* gAudioServerPlugInDriverInterfacePtr =
static AudioServerPlugInDriverRef gAudioServerPlugInDriverRef = &gAudioServerPlugInDriverInterfacePtr;
static UInt32 gAudioServerPlugInDriverRefCount = 1;
// TODO: This name is a bit misleading because the devices are actually owned by the plug-in.
static BGM_Object& BGM_LookUpOwnerObject(AudioObjectID inObjectID)
{
switch(inObjectID)
@ -112,6 +113,11 @@ static BGM_Object& BGM_LookUpOwnerObject(AudioObjectID inObjectID)
case kObjectID_Mute_Output_Master:
return BGM_Device::GetInstance();
case kObjectID_Device_UI_Sounds:
case kObjectID_Stream_Input_UI_Sounds:
case kObjectID_Stream_Output_UI_Sounds:
return BGM_Device::GetUISoundsInstance();
case kObjectID_Device_Null:
case kObjectID_Stream_Null:
return BGM_NullDevice::GetInstance();
@ -128,6 +134,9 @@ static BGM_AbstractDevice& BGM_LookUpDevice(AudioObjectID inObjectID)
case kObjectID_Device:
return BGM_Device::GetInstance();
case kObjectID_Device_UI_Sounds:
return BGM_Device::GetUISoundsInstance();
case kObjectID_Device_Null:
return BGM_NullDevice::GetInstance();
}
@ -276,6 +285,7 @@ static OSStatus BGM_Initialize(AudioServerPlugInDriverRef inDriver, AudioServerP
// Init/activate the devices.
BGM_Device::GetInstance();
BGM_Device::GetUISoundsInstance();
BGM_NullDevice::GetInstance();
}
catch(const CAException& inException)
@ -325,7 +335,7 @@ static OSStatus BGM_AddDeviceClient(AudioServerPlugInDriverRef inDriver, AudioOb
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_AddDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadObjectError),
"BGM_AddDeviceClient: unknown device");
@ -361,7 +371,7 @@ static OSStatus BGM_RemoveDeviceClient(AudioServerPlugInDriverRef inDriver, Audi
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_RemoveDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadObjectError),
"BGM_RemoveDeviceClient: unknown device");
@ -404,7 +414,7 @@ static OSStatus BGM_PerformDeviceConfigurationChange(AudioServerPlugInDriverRef
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_PerformDeviceConfigurationChange: unknown device");
@ -436,7 +446,7 @@ static OSStatus BGM_AbortDeviceConfigurationChange(AudioServerPlugInDriverRef in
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_PerformDeviceConfigurationChange: unknown device");
@ -667,7 +677,7 @@ static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver,
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_StartIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_StartIO: unknown device");
@ -701,7 +711,7 @@ static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver,
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_StopIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_StopIO: unknown device");
@ -751,7 +761,7 @@ static OSStatus BGM_GetZeroTimeStamp(AudioServerPlugInDriverRef inDriver,
ThrowIfNULL(outSeed,
CAException(kAudioHardwareIllegalOperationError),
"BGM_GetZeroTimeStamp: no place to put the seed");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_GetZeroTimeStamp: unknown device");
@ -794,7 +804,7 @@ static OSStatus BGM_WillDoIOOperation(AudioServerPlugInDriverRef inDriver,
ThrowIfNULL(outWillDoInPlace,
CAException(kAudioHardwareIllegalOperationError),
"BGM_WillDoIOOperation: no place to put the in-place return value");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_WillDoIOOperation: unknown device");
@ -839,7 +849,7 @@ static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver,
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_BeginIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_BeginIOOperation: unknown device");
@ -887,7 +897,7 @@ static OSStatus BGM_DoIOOperation(AudioServerPlugInDriverRef inDriver,
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_EndIOOperation: unknown device");
@ -935,7 +945,7 @@ static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver,
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_EndIOOperation: unknown device");

View file

@ -157,10 +157,12 @@ void BGM_TaskQueue::QueueSync_SwapClientShadowMaps(BGM_ClientMap* inClientMap
QueueSync(kBGMTaskSwapClientShadowMaps, /* inRunOnRealtimeThread = */ true, reinterpret_cast<UInt64>(inClientMap));
}
void BGM_TaskQueue::QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty)
void BGM_TaskQueue::QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty, AudioObjectID inDeviceID)
{
DebugMsg("BGM_TaskQueue::QueueAsync_SendPropertyNotification: Queueing property notification. inProperty=%u", inProperty);
BGM_Task theTask(kBGMTaskSendPropertyNotification, /* inIsSync = */ false, inProperty);
DebugMsg("BGM_TaskQueue::QueueAsync_SendPropertyNotification: Queueing property notification. inProperty=%u inDeviceID=%u",
inProperty,
inDeviceID);
BGM_Task theTask(kBGMTaskSendPropertyNotification, /* inIsSync = */ false, inProperty, inDeviceID);
QueueOnNonRealtimeThread(theTask);
}
@ -468,7 +470,7 @@ bool BGM_TaskQueue::ProcessNonRealTimeThreadTask(BGM_Task* inTask)
{
AudioObjectPropertyAddress thePropertyAddress[] = {
{ static_cast<UInt32>(inTask->GetArg1()), kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster } };
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, 1, thePropertyAddress);
BGM_PlugIn::Host_PropertiesChanged(static_cast<AudioObjectID>(inTask->GetArg2()), 1, thePropertyAddress);
}
break;

View file

@ -118,7 +118,7 @@ public:
// Sends a property changed notification to the BGMDevice host. Assumes the scope and element are kAudioObjectPropertyScopeGlobal and
// kAudioObjectPropertyElementMaster because currently those are the only ones we use.
void QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty);
void QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty, AudioObjectID inDeviceID);
// Set/unset a client's is-doing-IO flag

View file

@ -0,0 +1,368 @@
// 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/>.
//
// BGM_VolumeControl.cpp
// BGMDriver
//
// Copyright © 2016, 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Self Include
#include "BGM_VolumeControl.h"
// Local Includes
#include "BGM_PlugIn.h"
// PublicUtility Includes
#include "CAException.h"
#include "CADebugMacros.h"
#include "CADispatchQueue.h"
// STL Includes
#include <algorithm>
// System Includes
#include <CoreAudio/AudioHardwareBase.h>
#pragma clang assume_nonnull begin
#pragma mark Construction/Destruction
BGM_VolumeControl::BGM_VolumeControl(AudioObjectID inObjectID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement)
:
BGM_Control(inObjectID,
kAudioVolumeControlClassID,
kAudioLevelControlClassID,
inOwnerObjectID,
inScope,
inElement),
mMutex("Volume Control"),
mVolumeRaw(kDefaultMinRawVolume),
mMinVolumeRaw(kDefaultMinRawVolume),
mMaxVolumeRaw(kDefaultMaxRawVolume),
mMinVolumeDb(kDefaultMinDbVolume),
mMaxVolumeDb(kDefaultMaxDbVolume)
{
// Setup the volume curve with the one range
mVolumeCurve.AddRange(mMinVolumeRaw, mMaxVolumeRaw, mMinVolumeDb, mMaxVolumeDb);
}
#pragma mark Property Operations
bool BGM_VolumeControl::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioLevelControlPropertyScalarValue:
case kAudioLevelControlPropertyDecibelValue:
case kAudioLevelControlPropertyDecibelRange:
case kAudioLevelControlPropertyConvertScalarToDecibels:
case kAudioLevelControlPropertyConvertDecibelsToScalar:
theAnswer = true;
break;
default:
theAnswer = BGM_Control::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_VolumeControl::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
CheckObjectID(inObjectID);
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioLevelControlPropertyDecibelRange:
case kAudioLevelControlPropertyConvertScalarToDecibels:
case kAudioLevelControlPropertyConvertDecibelsToScalar:
theAnswer = false;
break;
case kAudioLevelControlPropertyScalarValue:
case kAudioLevelControlPropertyDecibelValue:
theAnswer = true;
break;
default:
theAnswer = BGM_Control::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_VolumeControl::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const
{
CheckObjectID(inObjectID);
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioLevelControlPropertyScalarValue:
theAnswer = sizeof(Float32);
break;
case kAudioLevelControlPropertyDecibelValue:
theAnswer = sizeof(Float32);
break;
case kAudioLevelControlPropertyDecibelRange:
theAnswer = sizeof(AudioValueRange);
break;
case kAudioLevelControlPropertyConvertScalarToDecibels:
theAnswer = sizeof(Float32);
break;
case kAudioLevelControlPropertyConvertDecibelsToScalar:
theAnswer = sizeof(Float32);
break;
default:
theAnswer = BGM_Control::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_VolumeControl::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
CheckObjectID(inObjectID);
switch(inAddress.mSelector)
{
case kAudioLevelControlPropertyScalarValue:
// This returns the value of the control in the normalized range of 0 to 1.
{
ThrowIf(inDataSize < sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
"of kAudioLevelControlPropertyScalarValue for the volume control");
CAMutex::Locker theLocker(mMutex);
*reinterpret_cast<Float32*>(outData) = mVolumeCurve.ConvertRawToScalar(mVolumeRaw);
outDataSize = sizeof(Float32);
}
break;
case kAudioLevelControlPropertyDecibelValue:
// This returns the dB value of the control.
{
ThrowIf(inDataSize < sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
"of kAudioLevelControlPropertyDecibelValue for the volume control");
CAMutex::Locker theLocker(mMutex);
*reinterpret_cast<Float32*>(outData) = mVolumeCurve.ConvertRawToDB(mVolumeRaw);
outDataSize = sizeof(Float32);
}
break;
case kAudioLevelControlPropertyDecibelRange:
// This returns the dB range of the control.
ThrowIf(inDataSize < sizeof(AudioValueRange),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::GetPropertyData: not enough space for the return value of "
"kAudioLevelControlPropertyDecibelRange for the volume control");
reinterpret_cast<AudioValueRange*>(outData)->mMinimum = mVolumeCurve.GetMinimumDB();
reinterpret_cast<AudioValueRange*>(outData)->mMaximum = mVolumeCurve.GetMaximumDB();
outDataSize = sizeof(AudioValueRange);
break;
case kAudioLevelControlPropertyConvertScalarToDecibels:
// This takes the scalar value in outData and converts it to dB.
{
ThrowIf(inDataSize < sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
"of kAudioLevelControlPropertyConvertScalarToDecibels for the volume "
"control");
// clamp the value to be between 0 and 1
Float32 theVolumeValue = *reinterpret_cast<Float32*>(outData);
theVolumeValue = std::min(1.0f, std::max(0.0f, theVolumeValue));
// do the conversion
*reinterpret_cast<Float32*>(outData) =
mVolumeCurve.ConvertScalarToDB(theVolumeValue);
// report how much we wrote
outDataSize = sizeof(Float32);
}
break;
case kAudioLevelControlPropertyConvertDecibelsToScalar:
// This takes the dB value in outData and converts it to scalar.
{
ThrowIf(inDataSize < sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
"of kAudioLevelControlPropertyConvertDecibelsToScalar for the volume "
"control");
// clamp the value to be between mMinVolumeDb and mMaxVolumeDb
Float32 theVolumeValue = *reinterpret_cast<Float32*>(outData);
theVolumeValue = std::min(mMaxVolumeDb, std::max(mMinVolumeDb, theVolumeValue));
// do the conversion
*reinterpret_cast<Float32*>(outData) =
mVolumeCurve.ConvertDBToScalar(theVolumeValue);
// report how much we wrote
outDataSize = sizeof(Float32);
}
break;
default:
BGM_Control::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
void BGM_VolumeControl::SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData)
{
CheckObjectID(inObjectID);
switch(inAddress.mSelector)
{
case kAudioLevelControlPropertyScalarValue:
// For the scalar volume, we clamp the new value to [0, 1]. Note that if this
// value changes, it implies that the dB value changed too.
{
ThrowIf(inDataSize != sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::SetPropertyData: wrong size for the data for "
"kAudioLevelControlPropertyScalarValue");
// Read the new scalar volume and clamp it.
Float32 theNewVolumeScalar = *reinterpret_cast<const Float32*>(inData);
theNewVolumeScalar = std::min(1.0f, std::max(0.0f, theNewVolumeScalar));
// Store the new volume.
SInt32 theNewVolumeRaw = mVolumeCurve.ConvertScalarToRaw(theNewVolumeScalar);
SetVolumeRaw(theNewVolumeRaw);
}
break;
case kAudioLevelControlPropertyDecibelValue:
// For the dB value, we first convert it to a raw value since that is how
// the value is tracked. Note that if this value changes, it implies that the
// scalar value changes as well.
{
ThrowIf(inDataSize != sizeof(Float32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_VolumeControl::SetPropertyData: wrong size for the data for "
"kAudioLevelControlPropertyDecibelValue");
// Read the new volume in dB and clamp it.
Float32 theNewVolumeDb = *reinterpret_cast<const Float32*>(inData);
theNewVolumeDb =
std::min(mMaxVolumeDb, std::max(mMinVolumeDb, theNewVolumeDb));
// Store the new volume.
SInt32 theNewVolumeRaw = mVolumeCurve.ConvertDBToRaw(theNewVolumeDb);
SetVolumeRaw(theNewVolumeRaw);
}
break;
default:
BGM_Control::SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
break;
};
}
void BGM_VolumeControl::SetVolumeRaw(SInt32 inNewVolumeRaw)
{
CAMutex::Locker theLocker(mMutex);
// Make sure the new raw value is in the proper range.
inNewVolumeRaw = std::min(std::max(mMinVolumeRaw, inNewVolumeRaw), mMaxVolumeRaw);
// Store the new volume.
if(mVolumeRaw != inNewVolumeRaw)
{
mVolumeRaw = inNewVolumeRaw;
// Send notifications.
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[2];
theChangedProperties[0] = { kAudioLevelControlPropertyScalarValue, mScope, mElement };
theChangedProperties[1] = { kAudioLevelControlPropertyDecibelValue, mScope, mElement };
BGM_PlugIn::Host_PropertiesChanged(GetObjectID(), 2, theChangedProperties);
});
}
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,105 @@
// 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/>.
//
// BGM_VolumeControl.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
#ifndef BGMDriver__BGM_VolumeControl
#define BGMDriver__BGM_VolumeControl
// Superclass Includes
#include "BGM_Control.h"
// PublicUtility Includes
#include "CAVolumeCurve.h"
#include "CAMutex.h"
#pragma clang assume_nonnull begin
class BGM_VolumeControl
:
public BGM_Control
{
#pragma mark Construction/Destruction
public:
BGM_VolumeControl(AudioObjectID inObjectID,
AudioObjectID inOwnerObjectID,
AudioObjectPropertyScope inScope,
AudioObjectPropertyElement inElement =
kAudioObjectPropertyElementMaster);
#pragma mark Property Operations
virtual bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData) const;
virtual void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
virtual void SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData);
#pragma mark Implementation
protected:
void SetVolumeRaw(SInt32 inNewVolumeRaw);
private:
const SInt32 kDefaultMinRawVolume = 0;
const SInt32 kDefaultMaxRawVolume = 96;
const Float32 kDefaultMinDbVolume = -96.0f;
const Float32 kDefaultMaxDbVolume = 0.0f;
CAMutex mMutex;
SInt32 mVolumeRaw;
SInt32 mMinVolumeRaw;
SInt32 mMaxVolumeRaw;
Float32 mMinVolumeDb;
Float32 mMaxVolumeDb;
CAVolumeCurve mVolumeCurve;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_VolumeControl */

View file

@ -31,7 +31,7 @@ extern "C" {
#endif
// On failure, returns one of the kBGMXPC_* error codes, or the error code received from BGMXPCHelper. Returns kBGMXPC_Success otherwise.
UInt64 StartBGMAppPlayThroughSync(void);
UInt64 StartBGMAppPlayThroughSync(bool inIsForUISoundsDevice);
#if defined(__cplusplus)
}

View file

@ -58,7 +58,7 @@ static NSXPCConnection* CreateXPCHelperConnection()
return theConnection;
}
UInt64 StartBGMAppPlayThroughSync()
UInt64 StartBGMAppPlayThroughSync(bool inIsForUISoundsDevice)
{
__block UInt64 theAnswer = kBGMXPC_Success;
@ -113,7 +113,7 @@ UInt64 StartBGMAppPlayThroughSync()
// Tell the enclosing function it can return now.
dispatch_semaphore_signal(theReplySemaphore);
}];
} forUISoundsDevice:inIsForUISoundsDevice];
DebugMsg("BGM_XPCHelper::StartBGMAppPlayThroughSync: Waiting for BGMApp to tell us the output device is ready for IO");

View file

@ -36,8 +36,9 @@
#pragma mark Construction/Destruction
BGM_Clients::BGM_Clients(BGM_TaskQueue* inTaskQueue)
BGM_Clients::BGM_Clients(AudioObjectID inOwnerDeviceID, BGM_TaskQueue* inTaskQueue)
:
mOwnerDeviceID(inOwnerDeviceID),
mClientMap(inTaskQueue)
{
mRelativeVolumeCurve.AddRange(kAppRelativeVolumeMinRawValue,
@ -110,9 +111,10 @@ bool BGM_Clients::StartIONonRT(UInt32 inClientID)
// Make sure we can start
ThrowIf(mStartCount == UINT64_MAX, CAException(kAudioHardwareIllegalOperationError), "BGM_Clients::StartIO: failed to start because the ref count was maxxed out already");
DebugMsg("BGM_Clients::StartIO: Client %u (%s) starting IO",
DebugMsg("BGM_Clients::StartIO: Client %u (%s, %d) starting IO",
inClientID,
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8));
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8),
theClient.mProcessID);
mClientMap.StartIONonRT(inClientID);
@ -160,9 +162,10 @@ bool BGM_Clients::StopIONonRT(UInt32 inClientID)
if(theClient.mDoingIO)
{
DebugMsg("BGM_Clients::StopIO: Client %u (%s) stopping IO",
DebugMsg("BGM_Clients::StopIO: Client %u (%s, %d) stopping IO",
inClientID,
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8));
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8),
theClient.mProcessID);
mClientMap.StopIONonRT(inClientID);
@ -228,7 +231,7 @@ void BGM_Clients::SendIORunningNotifications(bool sendIsRunningNotification,
theNotificationCount++;
}
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, theNotificationCount, theChangedProperties);
BGM_PlugIn::Host_PropertiesChanged(mOwnerDeviceID, theNotificationCount, theChangedProperties);
});
}
}

View file

@ -58,7 +58,7 @@ class BGM_Clients
friend class BGM_ClientTasks;
public:
BGM_Clients(BGM_TaskQueue* inTaskQueue);
BGM_Clients(AudioObjectID inOwnerDeviceID, BGM_TaskQueue* inTaskQueue);
~BGM_Clients() = default;
// Disallow copying. (It could make sense to implement these in future, but we don't need them currently.)
BGM_Clients(const BGM_Clients&) = delete;
@ -111,6 +111,7 @@ public:
bool SetClientsRelativeVolumes(const CACFArray inAppVolumes);
private:
AudioObjectID mOwnerDeviceID;
BGM_ClientMap mClientMap;
// Counters for the number of clients that are doing IO. These are used to tell whether any clients

View file

@ -57,7 +57,7 @@ static const AudioServerPlugInClientInfo client2Info = {
- (void)setUp {
[super setUp];
clients = new BGM_Clients(&taskQueue);
clients = new BGM_Clients(kAudioObjectUnknown, &taskQueue);
}
- (void)tearDown {

View file

@ -45,7 +45,7 @@ static NSString* kBGMXPCHelperMachServiceName = @kBGMXPCHelperBundleID;
//
// If BGMApp can be reached, the error it returns will be passed the reply block. Otherwise, the reply block will be passed an error with
// one of the kBGMXPC_* error codes. It may have an underlying error using one of the NSXPCConnection* error codes from FoundationErrors.h.
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply;
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI;
@end
@ -53,7 +53,7 @@ static NSString* kBGMXPCHelperMachServiceName = @kBGMXPCHelperBundleID;
// The protocol that BGMApp will vend as its XPC API.
@protocol BGMAppXPCProtocol
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply;
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI;
@end

View file

@ -40,23 +40,32 @@ static const char* const kBGMIssueTrackerURL = "https://github.com/kyleneideck/B
#define kBGMDeviceUID "BGMDevice"
#define kBGMDeviceModelUID "BGMDeviceModelUID"
#define kBGMDeviceUID_UISounds "BGMDevice_UISounds"
#define kBGMDeviceModelUID_UISounds "BGMDeviceModelUID_UISounds"
#define kBGMNullDeviceUID "BGMNullDevice"
#define kBGMNullDeviceModelUID "BGMNullDeviceModelUID"
// The object IDs for the audio objects this driver implements.
//
// BGMDevice always publishes this fixed set of objects (regardless of the wrapped device). We might need to
// change that at some point, but so far it hasn't caused any problems and it makes the driver much simpler.
// BGMDevice always publishes this fixed set of objects (except when BGMDevice's volume or mute
// controls are disabled). We might need to change that at some point, but so far it hasn't caused
// any problems and it makes the driver much simpler.
enum
{
kObjectID_PlugIn = kAudioObjectPlugInObject,
kObjectID_Device = 2, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Input = 3, // Belongs to kObjectID_Device
kObjectID_Stream_Output = 4, // Belongs to kObjectID_Device
kObjectID_Volume_Output_Master = 5, // Belongs to kObjectID_Device
kObjectID_Mute_Output_Master = 6, // Belongs to kObjectID_Device
kObjectID_Device_Null = 7, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Null = 8, // Belongs to kObjectID_Device_Null
kObjectID_PlugIn = kAudioObjectPlugInObject,
// BGMDevice
kObjectID_Device = 2, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Input = 3, // Belongs to kObjectID_Device
kObjectID_Stream_Output = 4, // Belongs to kObjectID_Device
kObjectID_Volume_Output_Master = 5, // Belongs to kObjectID_Device
kObjectID_Mute_Output_Master = 6, // Belongs to kObjectID_Device
// Null Device
kObjectID_Device_Null = 7, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Null = 8, // Belongs to kObjectID_Device_Null
// BGMDevice for UI sounds
kObjectID_Device_UI_Sounds = 9, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Input_UI_Sounds = 10, // Belongs to kObjectID_Device_UI_Sounds
kObjectID_Stream_Output_UI_Sounds = 11, // Belongs to kObjectID_Device_UI_Sounds
};
#pragma BGM Plug-in Custom Properties
@ -104,9 +113,9 @@ enum
};
// The number of silent/audible frames before BGMDriver will change kAudioDeviceCustomPropertyDeviceAudibleState
#define kDeviceAudibleStateMinChangedFramesForUpdate (2 << 12)
#define kDeviceAudibleStateMinChangedFramesForUpdate (2 << 11)
enum
enum BGMDeviceAudibleState : SInt32
{
// kAudioDeviceCustomPropertyDeviceAudibleState values
//