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
audioDevices:audioDevices
preferredDevices:preferredOutputDevices
musicPlayers:musicPlayers
aboutPanel:self.aboutPanel
aboutPanelLicenseView:self.aboutPanelLicenseView];

View file

@ -319,29 +319,9 @@
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
try {
if (newDeviceID != currentDeviceID) {
BGMAudioDevice newOutputDevice(newDeviceID);
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
outputDevice = newOutputDevice;
}
// Set the output device to use the new data source.
if (dataSourceID) {
// TODO: If this fails, ideally we'd still start playthrough and return an error, but not
// revert the device. It would probably be a bit awkward, though.
[self setDataSource:*dataSourceID device:outputDevice];
}
if (newDeviceID != currentDeviceID) {
// We successfully changed to the new device. Start playthrough on it, since audio might be
// 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();
}
[self setOutputDeviceWithIDImpl:newDeviceID
dataSourceID:dataSourceID
currentDeviceID:currentDeviceID];
} catch (const CAException& e) {
BGMAssert(e.GetError() != kAudioHardwareNoError,
"CAException with kAudioHardwareNoError");
@ -355,6 +335,7 @@
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
}
// Tell other classes and BGMXPCHelper that we changed the output device.
[self propagateOutputDeviceChange];
} @finally {
[stateLock unlock];
@ -363,6 +344,35 @@
return nil;
}
// Throws CAException.
- (void) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
dataSourceID:(UInt32* __nullable)dataSourceID
currentDeviceID:(AudioObjectID)currentDeviceID {
if (newDeviceID != currentDeviceID) {
BGMAudioDevice newOutputDevice(newDeviceID);
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
outputDevice = newOutputDevice;
}
// Set the output device to use the new data source.
if (dataSourceID) {
// TODO: If this fails, ideally we'd still start playthrough and return an error, but not
// revert the device. It would probably be a bit awkward, though.
[self setDataSource:*dataSourceID device:outputDevice];
}
if (newDeviceID != currentDeviceID) {
// We successfully changed to the new device. Start playthrough on it, since audio might be
// 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();
}
}
// Changes the output device that playthrough plays audio to and that BGMDevice's controls are
// kept in sync with. Throws CAException.
- (void) setOutputDeviceForPlaythroughAndControlSync:(const BGMAudioDevice&)newOutputDevice {

View file

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

View file

@ -42,6 +42,8 @@ NSString* const kAudioSystemSettingsPlist =
@"/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist";
@implementation BGMPreferredOutputDevices {
NSRecursiveLock* _stateLock;
// Used to change BGMApp's output device.
BGMAudioDeviceManager* _devices;
@ -55,6 +57,7 @@ NSString* const kAudioSystemSettingsPlist =
- (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices {
if ((self = [super init])) {
_stateLock = [NSRecursiveLock new];
_devices = devices;
_preferredDevices = [self readPreferredDevices];
@ -67,6 +70,20 @@ NSString* const kAudioSystemSettingsPlist =
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.
- (NSArray<NSString*>*) readPreferredDevices {
// Read the Plist file into a dictionary.
@ -123,14 +140,6 @@ NSString* const kAudioSystemSettingsPlist =
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 {
// Create the block that will run when a device is added or removed.
BGMPreferredOutputDevices* __weak weakSelf = self;
@ -139,10 +148,9 @@ NSString* const kAudioSystemSettingsPlist =
const AudioObjectPropertyAddress* inAddresses) {
#pragma unused (inNumberAddresses, inAddresses)
BGMLogAndSwallowExceptions("BGMPreferredOutputDevices::listenForDevicesAddedOrRemoved",
([&] () {
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] () {
[weakSelf connectedDeviceListChanged];
}));
});
};
// Register the listener block with CoreAudio.
@ -153,31 +161,36 @@ NSString* const kAudioSystemSettingsPlist =
}
- (void) connectedDeviceListChanged {
// 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];
@try {
[_stateLock lock];
if (preferredDevice == kAudioObjectUnknown) {
// The current output device was disconnected and there are no preferred devices
// connected, so pick one arbitrarily.
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Changing to an arbitrary output device.");
[_devices setOutputDeviceByLatency];
} else if (_devices.outputDevice.GetObjectID() != preferredDevice) {
// Change to the preferred device.
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Changing output device to %d.",
preferredDevice);
NSError* __nullable error = [_devices setOutputDeviceWithID:preferredDevice
revertOnFailure:YES];
if (error) {
// There's not much we can do if this happens.
LogError("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Failed to change to preferred device. Error: %s",
error.debugDescription.UTF8String);
// 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];
if (preferredDevice == kAudioObjectUnknown) {
// The current output device was disconnected and there are no preferred devices
// connected, so pick one arbitrarily.
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Changing to an arbitrary output device.");
[_devices setOutputDeviceByLatency];
} else if (_devices.outputDevice.GetObjectID() != preferredDevice) {
// Change to the preferred device.
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Changing output device to %d.",
preferredDevice);
NSError* __nullable error = [_devices setOutputDeviceWithID:preferredDevice
revertOnFailure:YES];
if (error) {
// There's not much we can do if this happens.
LogError("BGMPreferredOutputDevices::connectedDeviceListChanged: "
"Failed to change to preferred device. Error: %s",
error.debugDescription.UTF8String);
}
}
} @finally {
[_stateLock unlock];
}
}
@ -246,6 +259,40 @@ NSString* const kAudioSystemSettingsPlist =
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
#pragma clang assume_nonnull end

View file

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

View file

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

View file

@ -17,7 +17,7 @@
// BGMPreferencesMenu.h
// 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
// than to other BGMApp classes.
@ -25,6 +25,7 @@
// Local Includes
#import "BGMAudioDeviceManager.h"
#import "BGMPreferredOutputDevices.h"
#import "BGMMusicPlayers.h"
// System Includes
@ -37,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;

View file

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

View file

@ -17,7 +17,7 @@
// BGM_Utils.h
// SharedSource
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2016-2018 Kyle Neideck
//
#ifndef SharedSource__BGM_Utils
@ -60,6 +60,11 @@
__FUNCTION__, \
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
#if defined(__OBJC__)