mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-09-20 14:31:56 +00:00
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:
parent
1bb3873a53
commit
29642da1cf
9 changed files with 145 additions and 65 deletions
|
@ -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];
|
||||
|
|
|
@ -319,6 +319,35 @@
|
|||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
|
||||
|
||||
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 ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : 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) {
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
|
||||
|
@ -342,25 +371,6 @@
|
|||
playThrough.StopIfIdle();
|
||||
playThrough_UISounds.StopIfIdle();
|
||||
}
|
||||
} catch (const CAException& e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:e.GetError()
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
|
||||
[self propagateOutputDeviceChange];
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Changes the output device that playthrough plays audio to and that BGMDevice's controls are
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,10 +161,12 @@ 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.
|
||||
@try {
|
||||
[_stateLock lock];
|
||||
|
||||
// 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) {
|
||||
|
@ -179,6 +189,9 @@ NSString* const kAudioSystemSettingsPlist =
|
|||
error.debugDescription.UTF8String);
|
||||
}
|
||||
}
|
||||
} @finally {
|
||||
[_stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the most-preferred device currently connected. If no preferred devices are connected,
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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__)
|
||||
|
|
Loading…
Reference in a new issue