Skip to content

Commit

Permalink
Store the preferred devices list in User Defaults.
Browse files Browse the repository at this point in the history
BGMApp has to set BGMDevice, and often also the Null Device for a short
time, as the systemwide default audio device, which makes CoreAudio put
them in the preferred devices list in its Plist file. And since the list
is limited to three devices, it only gives us one or two usable ones.
Ideally, CoreAudio just wouldn't add our devices to its list, but I
don't think we can prevent that.

As a partial workaround, we now store our own copy of the preferred
devices list without our devices, which BGMApp can use to figure out
which devices were pushed out of CoreAudio's list by our devices.

This doesn't fix the problem entirely because our devices still take up
room in CoreAudio's list when BGMApp is closed, but I think that would
be harder to solve.

See #167.

Also:
 - Handle setting the initial output device in BGMPreferredOutputDevices
   instead of BGMAudioDeviceManager.
 - Fix a crash in BGMOutputVolumeMenuItem::dealloc caused by using
   dispatch_sync to dispatch to the main queue while running on the main
   queue.
 - Fix a crash in BGMPreferredOutputDevices if
   /Library/Preferences/Audio/com.apple.audio.SystemSettings.plist
   doesn't exist.
 - Add Swinsian to the list of music players in the README. (I must have
   forgotten to do that when I added support for it.)
  • Loading branch information
kyleneideck committed Oct 28, 2018
1 parent 871bb97 commit 4c0c656
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 248 deletions.
115 changes: 88 additions & 27 deletions BGMApp/BGMApp/BGMAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
#import "BGMTermination.h"
#import "SystemPreferences.h"

// PublicUtility Includes
#import "CAPropertyAddress.h"

// System Includes
#import <AVFoundation/AVCaptureDevice.h>

Expand Down Expand Up @@ -166,9 +163,18 @@ - (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
return;
}

// Persistently stores user settings and data.
BGMUserDefaults* userDefaults = [self createUserDefaults];

// Handles changing (or not changing) the output device when devices are added or removed. Must
// be initialised before calling setBGMDeviceAsDefault.
preferredOutputDevices = [[BGMPreferredOutputDevices alloc] initWithDevices:audioDevices];
preferredOutputDevices =
[[BGMPreferredOutputDevices alloc] initWithDevices:audioDevices userDefaults:userDefaults];

// Choose an output device for BGMApp to use to play audio.
if (![self setInitialOutputDevice]) {
return;
}

// Make BGMDevice the default device.
[self setBGMDeviceAsDefault];
Expand All @@ -177,8 +183,6 @@ - (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
BGMTermination::SetUpTerminationCleanUp(audioDevices);

// Set up the rest of the UI and other external interfaces.
BGMUserDefaults* userDefaults = [self createUserDefaults];

musicPlayers = [[BGMMusicPlayers alloc] initWithAudioDevices:audioDevices
userDefaults:userDefaults];

Expand All @@ -198,11 +202,31 @@ - (void) applicationDidFinishLaunching:(NSNotification*)aNotification {

// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
- (BOOL) initAudioDeviceManager {
NSError* error;
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&error];
audioDevices = [BGMAudioDeviceManager new];

if (!audioDevices) {
[self showDeviceNotFoundErrorMessageAndExit:error.code];
[self showBGMDeviceNotFoundErrorMessageAndExit];
return NO;
}

return YES;
}

// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
- (BOOL) setInitialOutputDevice {
AudioObjectID preferredDevice = [preferredOutputDevices findPreferredDevice];

if (preferredDevice != kAudioObjectUnknown) {
NSError* __nullable error = [audioDevices setOutputDeviceWithID:preferredDevice
revertOnFailure:NO];
if (error) {
// Show the error message.
[self showFailedToSetOutputDeviceErrorMessage:BGMNN(error)
preferredDevice:preferredDevice];
}
} else {
// We couldn't find a device to use, so show an error message and quit.
[self showOutputDeviceNotFoundErrorMessageAndExit];
return NO;
}

Expand Down Expand Up @@ -333,30 +357,48 @@ - (void) applicationWillTerminate:(NSNotification*)aNotification {

#pragma mark Error messages

- (void) showDeviceNotFoundErrorMessageAndExit:(NSInteger)code {
// Show an error dialog and exit if either BGMDevice wasn't found on the system or we couldn't find any output devices

// NSAlert should only be used on the main thread.
- (void) showBGMDeviceNotFoundErrorMessageAndExit {
// BGMDevice wasn't found on the system. Most likely, BGMDriver isn't installed. Show an error
// dialog and exit.
//
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL? Might even want to
// offer to install them if not.
[self showErrorMessage:@"Could not find the Background Music virtual audio device."
informativeText:@"Make sure you've installed Background Music Device.driver to "
"/Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo "
"killall coreaudiod\")."
exitAfterMessageDismissed:YES];
}

- (void) showFailedToSetOutputDeviceErrorMessage:(NSError*)error
preferredDevice:(BGMAudioDevice)device {
NSLog(@"Failed to set initial output device. Error: %@", error);

dispatch_async(dispatch_get_main_queue(), ^{
NSAlert* alert = [NSAlert new];

if (code == kBGMErrorCode_BGMDeviceNotFound) {
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL and offer to install them if not. Also,
// it would be nice if we could restart coreaudiod automatically (using launchd).
[alert setMessageText:@"Could not find the Background Music virtual audio device."];
[alert setInformativeText:@"Make sure you've installed Background Music Device.driver to /Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo killall coreaudiod\")."];
} else if (code == kBGMErrorCode_OutputDeviceNotFound) {
[alert setMessageText:@"Could not find an audio output device."];
[alert setInformativeText:@"If you do have one installed, this is probably a bug. Sorry about that. Feel free to file an issue on GitHub."];
}
NSAlert* alert = [NSAlert alertWithError:BGMNN(error)];
alert.messageText = @"Failed to set the output device.";

NSString* __nullable name = nil;
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
name = (__bridge NSString* __nullable)device.CopyName();
});

alert.informativeText =
[NSString stringWithFormat:@"Could not start the device '%@'. (Error: %ld)",
name, error.code];

// This crashes if built with Xcode 9.0.1, but works with versions of Xcode before 9 and
// with 9.1.
[alert runModal];
[NSApp terminate:self];
});
}

- (void) showOutputDeviceNotFoundErrorMessageAndExit {
// We couldn't find any output devices. Show an error dialog and exit.
[self showErrorMessage:@"Could not find an audio output device."
informativeText:@"If you do have one installed, this is probably a bug. Sorry about "
"that. Feel free to file an issue on GitHub."
exitAfterMessageDismissed:YES];
}

- (void) showXPCHelperErrorMessage:(NSError*)error {
if (!haveShownXPCHelperErrorMessage) {
haveShownXPCHelperErrorMessage = YES;
Expand All @@ -380,6 +422,25 @@ - (void) showXPCHelperErrorMessage:(NSError*)error {
}
}

- (void) showErrorMessage:(NSString*)message
informativeText:(NSString*)informativeText
exitAfterMessageDismissed:(BOOL)fatal {
// NSAlert should only be used on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert* alert = [NSAlert new];
[alert setMessageText:message];
[alert setInformativeText:informativeText];

// This crashes if built with Xcode 9.0.1, but works with versions of Xcode before 9 and
// with 9.1.
[alert runModal];

if (fatal) {
[NSApp terminate:self];
}
});
}

- (void) showSetDeviceAsDefaultError:(NSError*)error
message:(NSString*)msg
informativeText:(NSString*)info {
Expand Down
14 changes: 5 additions & 9 deletions BGMApp/BGMApp/BGMAudioDeviceManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// BGMAudioDeviceManager.h
// BGMApp
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2016-2018 Kyle Neideck
//
// 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.
Expand All @@ -43,13 +43,13 @@

#pragma clang assume_nonnull begin

static const int kBGMErrorCode_BGMDeviceNotFound = 1;
static const int kBGMErrorCode_OutputDeviceNotFound = 2;
static const int kBGMErrorCode_ReturningEarly = 3;
static const int kBGMErrorCode_OutputDeviceNotFound = 1;
static const int kBGMErrorCode_ReturningEarly = 2;

@interface BGMAudioDeviceManager : NSObject

- (instancetype) initWithError:(NSError**)error;
// Returns nil if BGMDevice isn't installed.
- (instancetype) init;

// Set the BGMOutputVolumeMenuItem to be notified when the output device is changed.
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item;
Expand Down Expand Up @@ -90,10 +90,6 @@ static const int kBGMErrorCode_ReturningEarly = 3;
dataSourceID:(UInt32)dataSourceID
revertOnFailure:(BOOL)revertOnFailure;

// Sets the output device to the device with the lowest latency. Used when we have no better way to
// choose the output device.
- (void) setOutputDeviceByLatency;

// Start playthrough synchronously. Blocks until IO has started on the output device and playthrough
// is running. See BGMPlayThrough.
//
Expand Down
110 changes: 9 additions & 101 deletions BGMApp/BGMApp/BGMAudioDeviceManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -66,35 +66,17 @@ @implementation BGMAudioDeviceManager {

#pragma mark Construction/Destruction

- (instancetype) initWithError:(NSError** __nullable)error {
- (instancetype) init {
if ((self = [super init])) {
stateLock = [NSRecursiveLock new];
bgmXPCHelperConnection = nil;
outputVolumeMenuItem = nil;
outputDevice = kAudioObjectUnknown;

try {
bgmDevice = new BGMBackgroundMusicDevice;
} catch (const CAException& e) {
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found. (%d)", e.GetError());

if (error) {
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
}

self = nil;
return self;
}

try {
[self initOutputDevice];
} catch (const CAException& e) {
LogError("BGMAudioDeviceManager::initWithError: failed to init output device (%d)",
e.GetError());

if (error) {
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
}

LogError("BGMAudioDeviceManager::init: BGMDevice not found. (%d)", e.GetError());
self = nil;
return self;
}
Expand All @@ -116,86 +98,6 @@ - (void) dealloc {
}
}

// Throws a CAException if it fails to set the output device.
- (void) initOutputDevice {
CAHALAudioSystemObject audioSystem;
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);

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 {
// 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));
}

// Log message
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];
}
}
}

- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item {
outputVolumeMenuItem = item;
}
Expand Down Expand Up @@ -371,6 +273,12 @@ - (void) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
playThrough.StopIfIdle();
playThrough_UISounds.StopIfIdle();
}

CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Set output device to %s (%d)",
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8),
outputDevice.GetObjectID());
CFRelease(outputDeviceUID);
}

// Changes the output device that playthrough plays audio to and that BGMDevice's controls are
Expand Down
34 changes: 16 additions & 18 deletions BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm
Original file line number Diff line number Diff line change
Expand Up @@ -84,27 +84,25 @@ - (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
return self;
}

// We currently only use one instance of this class and it's never deallocated, but it's probably
// good practice to define dealloc anyway.
- (void) dealloc {
dispatch_sync(dispatch_get_main_queue(), ^{
// Remove the audio property listeners.
[self removeOutputDeviceDataSourceListener];
// Remove the audio property listeners.
// TODO: This call isn't thread safe. (But currently this dealloc method is only called if
// there's an error.)
[self removeOutputDeviceDataSourceListener];

BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));

BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
});
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
}

- (void) initSlider {
Expand Down
Loading

0 comments on commit 4c0c656

Please sign in to comment.