Update the preferred devices list when the user changes output device.

When the user chooses a different output device in BGMApp, the new
device is now added to the front of the list of preferred devices. This
stops BGMPreferredOutputDevices changing the output device back shortly
afterward when it gets a device connection/disconnection notification,
which is sent because BGMDriver's Null Device is enabled and then
disabled as part of changing the output device.

It also means BGMApp will now account for the times the output device
has been changed since BGMApp started when deciding whether to change to
a newly connected device and deciding which device to change to when the
current output device is removed.
This commit is contained in:
Kyle Neideck 2018-10-24 22:29:20 +11:00
parent 1bb3873a53
commit 29642da1cf
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
9 changed files with 145 additions and 65 deletions

View file

@ -270,6 +270,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
audioDevices:audioDevices audioDevices:audioDevices
preferredDevices:preferredOutputDevices
musicPlayers:musicPlayers musicPlayers:musicPlayers
aboutPanel:self.aboutPanel aboutPanel:self.aboutPanel
aboutPanelLicenseView:self.aboutPanelLicenseView]; aboutPanelLicenseView:self.aboutPanelLicenseView];

View file

@ -319,6 +319,35 @@
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.) AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
try { try {
[self setOutputDeviceWithIDImpl:newDeviceID
dataSourceID:dataSourceID
currentDeviceID:currentDeviceID];
} catch (const CAException& e) {
BGMAssert(e.GetError() != kAudioHardwareNoError,
"CAException with kAudioHardwareNoError");
return [self failedToSetOutputDevice:newDeviceID
errorCode:e.GetError()
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
} catch (...) {
return [self failedToSetOutputDevice:newDeviceID
errorCode:kAudioHardwareUnspecifiedError
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
}
// Tell other classes and BGMXPCHelper that we changed the output device.
[self propagateOutputDeviceChange];
} @finally {
[stateLock unlock];
}
return nil;
}
// Throws CAException.
- (void) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
dataSourceID:(UInt32* __nullable)dataSourceID
currentDeviceID:(AudioObjectID)currentDeviceID {
if (newDeviceID != currentDeviceID) { if (newDeviceID != currentDeviceID) {
BGMAudioDevice newOutputDevice(newDeviceID); BGMAudioDevice newOutputDevice(newDeviceID);
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice]; [self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
@ -342,25 +371,6 @@
playThrough.StopIfIdle(); playThrough.StopIfIdle();
playThrough_UISounds.StopIfIdle(); playThrough_UISounds.StopIfIdle();
} }
} catch (const CAException& e) {
BGMAssert(e.GetError() != kAudioHardwareNoError,
"CAException with kAudioHardwareNoError");
return [self failedToSetOutputDevice:newDeviceID
errorCode:e.GetError()
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
} catch (...) {
return [self failedToSetOutputDevice:newDeviceID
errorCode:kAudioHardwareUnspecifiedError
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
}
[self propagateOutputDeviceChange];
} @finally {
[stateLock unlock];
}
return nil;
} }
// Changes the output device that playthrough plays audio to and that BGMDevice's controls are // Changes the output device that playthrough plays audio to and that BGMDevice's controls are

View file

@ -34,6 +34,7 @@
#import "BGMAudioDeviceManager.h" #import "BGMAudioDeviceManager.h"
// System Includes // System Includes
#import <CoreAudio/AudioHardwareBase.h>
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@ -45,6 +46,8 @@
// deallocated. // deallocated.
- (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices; - (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices;
- (void) userChangedOutputDeviceTo:(AudioObjectID)device;
@end @end
#pragma clang assume_nonnull end #pragma clang assume_nonnull end

View file

@ -42,6 +42,8 @@ NSString* const kAudioSystemSettingsPlist =
@"/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist"; @"/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist";
@implementation BGMPreferredOutputDevices { @implementation BGMPreferredOutputDevices {
NSRecursiveLock* _stateLock;
// Used to change BGMApp's output device. // Used to change BGMApp's output device.
BGMAudioDeviceManager* _devices; BGMAudioDeviceManager* _devices;
@ -55,6 +57,7 @@ NSString* const kAudioSystemSettingsPlist =
- (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices { - (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices {
if ((self = [super init])) { if ((self = [super init])) {
_stateLock = [NSRecursiveLock new];
_devices = devices; _devices = devices;
_preferredDevices = [self readPreferredDevices]; _preferredDevices = [self readPreferredDevices];
@ -67,6 +70,20 @@ NSString* const kAudioSystemSettingsPlist =
return self; return self;
} }
- (void) dealloc {
@try {
[_stateLock lock];
// Tell CoreAudio not to call the listener block anymore.
CAHALAudioSystemObject().RemovePropertyListenerBlock(
CAPropertyAddress(kAudioHardwarePropertyDevices),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
_deviceListListener);
} @finally {
[_stateLock unlock];
}
}
// Reads the preferred devices list from CoreAudio's Plist file. // Reads the preferred devices list from CoreAudio's Plist file.
- (NSArray<NSString*>*) readPreferredDevices { - (NSArray<NSString*>*) readPreferredDevices {
// Read the Plist file into a dictionary. // Read the Plist file into a dictionary.
@ -123,14 +140,6 @@ NSString* const kAudioSystemSettingsPlist =
return deviceUIDs; return deviceUIDs;
} }
- (void) dealloc {
// Tell CoreAudio not to call the listener block anymore.
CAHALAudioSystemObject().RemovePropertyListenerBlock(
CAPropertyAddress(kAudioHardwarePropertyDevices),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
_deviceListListener);
}
- (void) listenForDevicesAddedOrRemoved { - (void) listenForDevicesAddedOrRemoved {
// Create the block that will run when a device is added or removed. // Create the block that will run when a device is added or removed.
BGMPreferredOutputDevices* __weak weakSelf = self; BGMPreferredOutputDevices* __weak weakSelf = self;
@ -139,10 +148,9 @@ NSString* const kAudioSystemSettingsPlist =
const AudioObjectPropertyAddress* inAddresses) { const AudioObjectPropertyAddress* inAddresses) {
#pragma unused (inNumberAddresses, inAddresses) #pragma unused (inNumberAddresses, inAddresses)
BGMLogAndSwallowExceptions("BGMPreferredOutputDevices::listenForDevicesAddedOrRemoved", BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] () {
([&] () {
[weakSelf connectedDeviceListChanged]; [weakSelf connectedDeviceListChanged];
})); });
}; };
// Register the listener block with CoreAudio. // Register the listener block with CoreAudio.
@ -153,10 +161,12 @@ NSString* const kAudioSystemSettingsPlist =
} }
- (void) connectedDeviceListChanged { - (void) connectedDeviceListChanged {
// Decide which device should be the output device now. If a device has been connected @try {
// and it's preferred over the current output device, we'll change to that device. If [_stateLock lock];
// the current output device has been removed, we'll change to the next most-preferred
// device. // Decide which device should be the output device now. If a device has been connected and
// it's preferred over the current output device, we'll change to that device. If the
// current output device has been removed, we'll change to the next most-preferred device.
AudioObjectID preferredDevice = [self findPreferredDevice]; AudioObjectID preferredDevice = [self findPreferredDevice];
if (preferredDevice == kAudioObjectUnknown) { if (preferredDevice == kAudioObjectUnknown) {
@ -179,6 +189,9 @@ NSString* const kAudioSystemSettingsPlist =
error.debugDescription.UTF8String); error.debugDescription.UTF8String);
} }
} }
} @finally {
[_stateLock unlock];
}
} }
// Returns the most-preferred device currently connected. If no preferred devices are connected, // Returns the most-preferred device currently connected. If no preferred devices are connected,
@ -246,6 +259,40 @@ NSString* const kAudioSystemSettingsPlist =
return false; return false;
} }
- (void) userChangedOutputDeviceTo:(AudioObjectID)device {
@try {
[_stateLock lock];
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] () {
// Add the new output device to the list.
NSString* __nullable outputDeviceUID =
(__bridge NSString* __nullable)CAHALAudioDevice(device).CopyDeviceUID();
if (outputDeviceUID) {
// Limit the list to three devices because that's what macOS does.
if (_preferredDevices.count >= 2) {
_preferredDevices = @[BGMNN(outputDeviceUID),
_preferredDevices[0],
_preferredDevices[1]];
} else if (_preferredDevices.count >= 1) {
_preferredDevices = @[BGMNN(outputDeviceUID), _preferredDevices[0]];
} else {
_preferredDevices = @[BGMNN(outputDeviceUID)];
}
DebugMsg("BGMPreferredOutputDevices::outputDeviceWillChangeTo: "
"Preferred devices: %s",
_preferredDevices.debugDescription.UTF8String);
} else {
LogWarning("BGMPreferredOutputDevices::outputDeviceWillChangeTo: "
"Output device has no UID");
}
});
} @finally {
[_stateLock unlock];
}
}
@end @end
#pragma clang assume_nonnull end #pragma clang assume_nonnull end

View file

@ -17,11 +17,12 @@
// BGMOutputDevicePrefs.h // BGMOutputDevicePrefs.h
// BGMApp // BGMApp
// //
// Copyright © 2016 Kyle Neideck // Copyright © 2016, 2018 Kyle Neideck
// //
// Local Includes // Local Includes
#import "BGMAudioDeviceManager.h" #import "BGMAudioDeviceManager.h"
#import "BGMPreferredOutputDevices.h"
// System Includes // System Includes
#import <AppKit/AppKit.h> #import <AppKit/AppKit.h>
@ -31,7 +32,8 @@
@interface BGMOutputDevicePrefs : NSObject @interface BGMOutputDevicePrefs : NSObject
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices; - (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu; - (void) populatePreferencesMenu:(NSMenu*)prefsMenu;
@end @end

View file

@ -17,7 +17,7 @@
// BGMOutputDevicePrefs.mm // BGMOutputDevicePrefs.mm
// BGMApp // BGMApp
// //
// Copyright © 2016, 2017 Kyle Neideck // Copyright © 2016-2018 Kyle Neideck
// //
// Self Include // Self Include
@ -40,12 +40,15 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
@implementation BGMOutputDevicePrefs { @implementation BGMOutputDevicePrefs {
BGMAudioDeviceManager* audioDevices; BGMAudioDeviceManager* audioDevices;
BGMPreferredOutputDevices* preferredDevices;
NSMutableArray<NSMenuItem*>* outputDeviceMenuItems; NSMutableArray<NSMenuItem*>* outputDeviceMenuItems;
} }
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices { - (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
if ((self = [super init])) { if ((self = [super init])) {
audioDevices = inAudioDevices; audioDevices = inAudioDevices;
preferredDevices = inPreferredDevices;
outputDeviceMenuItems = [NSMutableArray new]; outputDeviceMenuItems = [NSMutableArray new];
} }
@ -220,6 +223,11 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
// Dispatched because it usually blocks. (Note that we're using // Dispatched because it usually blocks. (Note that we're using
// DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.) // DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (changingDevice) {
// Add the new output device to the list of preferred devices.
[preferredDevices userChangedOutputDeviceTo:newDeviceID];
}
[self changeToOutputDevice:newDeviceID [self changeToOutputDevice:newDeviceID
newDataSource:newDataSourceID newDataSource:newDataSourceID
deviceName:deviceName]; deviceName:deviceName];

View file

@ -17,7 +17,7 @@
// BGMPreferencesMenu.h // BGMPreferencesMenu.h
// BGMApp // BGMApp
// //
// Copyright © 2016 Kyle Neideck // Copyright © 2016, 2018 Kyle Neideck
// //
// Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather // Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather
// than to other BGMApp classes. // than to other BGMApp classes.
@ -25,6 +25,7 @@
// Local Includes // Local Includes
#import "BGMAudioDeviceManager.h" #import "BGMAudioDeviceManager.h"
#import "BGMPreferredOutputDevices.h"
#import "BGMMusicPlayers.h" #import "BGMMusicPlayers.h"
// System Includes // System Includes
@ -37,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu - (id) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices audioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
musicPlayers:(BGMMusicPlayers*)inMusicPlayers musicPlayers:(BGMMusicPlayers*)inMusicPlayers
aboutPanel:(NSPanel*)inAboutPanel aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView; aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;

View file

@ -17,7 +17,7 @@
// BGMPreferencesMenu.mm // BGMPreferencesMenu.mm
// BGMApp // BGMApp
// //
// Copyright © 2016 Kyle Neideck // Copyright © 2016, 2018 Kyle Neideck
// //
// Self Include // Self Include
@ -46,6 +46,7 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu - (id) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices audioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
musicPlayers:(BGMMusicPlayers*)inMusicPlayers musicPlayers:(BGMMusicPlayers*)inMusicPlayers
aboutPanel:(NSPanel*)inAboutPanel aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView { aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
@ -57,7 +58,8 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
audioDevices:inAudioDevices audioDevices:inAudioDevices
musicPlayers:inMusicPlayers]; musicPlayers:inMusicPlayers];
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices]; outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices
preferredDevices:inPreferredDevices];
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView]; aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];

View file

@ -17,7 +17,7 @@
// BGM_Utils.h // BGM_Utils.h
// SharedSource // SharedSource
// //
// Copyright © 2016, 2017 Kyle Neideck // Copyright © 2016-2018 Kyle Neideck
// //
#ifndef SharedSource__BGM_Utils #ifndef SharedSource__BGM_Utils
@ -60,6 +60,11 @@
__FUNCTION__, \ __FUNCTION__, \
expressionStr); expressionStr);
// Used to give the first 3 arguments of BGM_Utils::LogAndSwallowExceptions and
// BGM_Utils::LogUnexpectedExceptions (and probably others in future). Mainly so we can call those
// functions directly instead of using the macro wrappers.
#define BGMDbgArgs __FILE__, __LINE__, __FUNCTION__
#pragma mark Objective-C Macros #pragma mark Objective-C Macros
#if defined(__OBJC__) #if defined(__OBJC__)