mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-10 14:44:14 +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,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 ? ¤tDeviceID : 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 {
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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