-
Notifications
You must be signed in to change notification settings - Fork 685
/
BGMAppDelegate.mm
542 lines (449 loc) · 21.9 KB
/
BGMAppDelegate.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMAppDelegate.mm
// BGMApp
//
// Copyright © 2016-2022 Kyle Neideck
// Copyright © 2021 Marcus Wu
//
// Self Include
#import "BGMAppDelegate.h"
// Local Includes
#import "BGM_Utils.h"
#import "BGMAppVolumes.h"
#import "BGMAppVolumesController.h"
#import "BGMAutoPauseMusic.h"
#import "BGMAutoPauseMenuItem.h"
#import "BGMDebugLoggingMenuItem.h"
#import "BGMMusicPlayers.h"
#import "BGMOutputDeviceMenuSection.h"
#import "BGMOutputVolumeMenuItem.h"
#import "BGMPreferencesMenu.h"
#import "BGMPreferredOutputDevices.h"
#import "BGMStatusBarItem.h"
#import "BGMSystemSoundsVolume.h"
#import "BGMTermination.h"
#import "BGMUserDefaults.h"
#import "BGMXPCListener.h"
#import "SystemPreferences.h"
// System Includes
#import <AVFoundation/AVCaptureDevice.h>
#pragma clang assume_nonnull begin
static NSString* const kOptNoPersistentData = @"--no-persistent-data";
static NSString* const kOptShowDockIcon = @"--show-dock-icon";
@implementation BGMAppDelegate {
// The button in the system status bar that shows the main menu.
BGMStatusBarItem* statusBarItem;
// Only show the 'BGMXPCHelper is missing' error dialog once.
BOOL haveShownXPCHelperErrorMessage;
// Persistently stores user settings and data.
BGMUserDefaults* userDefaults;
BGMAutoPauseMusic* autoPauseMusic;
BGMAutoPauseMenuItem* autoPauseMenuItem;
BGMMusicPlayers* musicPlayers;
BGMSystemSoundsVolume* systemSoundsVolume;
BGMOutputDeviceMenuSection* outputDeviceMenuSection;
BGMPreferencesMenu* prefsMenu;
BGMDebugLoggingMenuItem* debugLoggingMenuItem;
BGMXPCListener* xpcListener;
BGMPreferredOutputDevices* preferredOutputDevices;
}
@synthesize audioDevices = audioDevices;
@synthesize appVolumes = appVolumes;
- (void) awakeFromNib {
[super awakeFromNib];
// Show BGMApp in the dock, if the command-line option for that was passed. This is used by the
// UI tests.
if ([NSProcessInfo.processInfo.arguments indexOfObject:kOptShowDockIcon] != NSNotFound) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
haveShownXPCHelperErrorMessage = NO;
// Set up audioDevices, which coordinates BGMDevice and the output device. It manages
// playthrough, volume/mute controls, etc.
if (![self initAudioDeviceManager]) {
return;
}
// Stored user settings
userDefaults = [self createUserDefaults];
// Add the status bar item. (The thing you click to show BGMApp's main menu.)
statusBarItem = [[BGMStatusBarItem alloc] initWithMenu:self.bgmMenu
audioDevices:audioDevices
userDefaults:userDefaults];
}
- (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
#pragma unused (aNotification)
// Log the version/build number.
//
// TODO: NSLog should only be used for logging errors.
// TODO: Automatically add the commit ID to the end of the build number for unreleased builds. (In the
// Info.plist or something -- not here.)
NSLog(@"BGMApp version: %@, BGMApp build number: %@",
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]);
// 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 userDefaults:userDefaults];
// Skip this if we're compiling on a version of macOS before 10.14 as won't compile and it
// isn't needed.
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
if (@available(macOS 10.14, *)) {
// On macOS 10.14+ we need to get the user's permission to use input devices before we can
// use BGMDevice for playthrough (see BGMPlayThrough), so we wait until they've given it
// before making BGMDevice the default device. This way, if the user is playing audio when
// they open Background Music, we won't interrupt it while we're waiting for them to click
// OK.
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
if (granted) {
DebugMsg("BGMAppDelegate::applicationDidFinishLaunching: Permission granted");
[self continueLaunchAfterInputDevicePermissionGranted];
} else {
NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: Permission denied");
// If they don't accept, Background Music won't work at all and the only way to
// fix it is in System Preferences, so show an error dialog with instructions.
//
// TODO: It would be nice if this dialog had a shortcut to open the System
// Preferences panel. See showSetDeviceAsDefaultError.
[self showErrorMessage:@"Background Music needs permission to use microphones."
informativeText:@"It uses a virtual microphone to access your system's "
"audio.\n\nYou can grant the permission by going to "
"System Preferences > Security and Privacy > "
"Microphone and checking the box for Background Music."
exitAfterMessageDismissed:YES];
}
});
}];
}
else
#endif
{
// We can change the device immediately on older versions of macOS because they don't
// require user permission for input devices.
[self continueLaunchAfterInputDevicePermissionGranted];
}
}
- (void) continueLaunchAfterInputDevicePermissionGranted {
// Choose an output device for BGMApp to use to play audio.
if (![self setInitialOutputDevice]) {
return;
}
// Make BGMDevice the default device.
[self setBGMDeviceAsDefault];
// Handle some of the unusual reasons BGMApp might have to exit, mostly crashes.
BGMTermination::SetUpTerminationCleanUp(audioDevices);
// Set up the rest of the UI and other external interfaces.
musicPlayers = [[BGMMusicPlayers alloc] initWithAudioDevices:audioDevices
userDefaults:userDefaults];
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
musicPlayers:musicPlayers];
[self setUpMainMenu];
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
helperConnectionErrorHandler:^(NSError* error) {
NSLog(@"BGMAppDelegate::continueLaunchAfterInputDevicePermissionGranted: "
"(helperConnectionErrorHandler) BGMXPCHelper connection error: %@",
error);
[self showXPCHelperErrorMessage:error];
}];
}
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
- (BOOL) initAudioDeviceManager {
audioDevices = [BGMAudioDeviceManager new];
if (!audioDevices) {
[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;
}
return YES;
}
// Sets the "Background Music" virtual audio device (BGMDevice) as the user's default audio device.
- (void) setBGMDeviceAsDefault {
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
if (error) {
[self showSetDeviceAsDefaultError:error
message:@"Could not set the Background Music device as your"
"default audio device."
informativeText:@"You might be able to change it yourself."];
}
}
- (void) menuWillOpen:(NSMenu*)menu {
if (@available(macOS 10.16, *)) {
// Set menu offset and check for any active menu items
float menuOffset = 12.0;
for (NSMenuItem* menuItem in self.bgmMenu.itemArray) {
if (menuItem.state == NSControlStateValueOn && menuItem.indentationLevel == 0) {
menuOffset += 10;
break;
}
}
// Align volume output device and slider
for (NSView* subview in self.outputVolumeView.subviews) {
CGRect newSubview = subview.frame;
newSubview.origin.x = menuOffset;
subview.frame = newSubview;
}
// Align system sounds and app volumes
double appIconTitleOffset = 0;
for (NSMenuItem* menuItem in self.bgmMenu.itemArray) {
if (menuItem.view.subviews.count == 7 || menuItem.view.subviews.count == 3) {
NSTextField* appTitle;
NSImageView* appIcon;
for (NSView* subview in menuItem.view.subviews) {
if (menuItem.view.subviews.count == 3) {
// System sounds
if ([subview isKindOfClass:[NSTextField class]]) {
appTitle = (NSTextField*)subview;
}
if ([subview isKindOfClass:[NSImageView class]]) {
appIcon = (NSImageView*)subview;
}
} else if (menuItem.view.subviews.count == 7) {
// App volumes
if ([subview isKindOfClass:[BGMAVM_AppNameLabel class]]) {
appTitle = (NSTextField*)subview;
}
if ([subview isKindOfClass:[BGMAVM_AppIcon class]]) {
appIcon = (NSImageView*)subview;
}
}
}
if (appIconTitleOffset == 0) {
appIconTitleOffset = appTitle.frame.origin.x - appIcon.frame.origin.x;
}
CGRect newAppIcon = appIcon.frame;
newAppIcon.origin.x = menuOffset;
appIcon.frame = newAppIcon;
CGRect newAppTitle = appTitle.frame;
newAppTitle.origin.x = menuOffset + appIconTitleOffset;
appTitle.frame = newAppTitle;
}
}
}
}
- (void) setUpMainMenu {
autoPauseMenuItem =
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
autoPauseMusic:autoPauseMusic
musicPlayers:musicPlayers
userDefaults:userDefaults];
[self initVolumesMenuSection];
// Output device selection.
outputDeviceMenuSection =
[[BGMOutputDeviceMenuSection alloc] initWithBGMMenu:self.bgmMenu
audioDevices:audioDevices
preferredDevices:preferredOutputDevices];
[audioDevices setOutputDeviceMenuSection:outputDeviceMenuSection];
// Preferences submenu.
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
audioDevices:audioDevices
musicPlayers:musicPlayers
statusBarItem:statusBarItem
aboutPanel:self.aboutPanel
aboutPanelLicenseView:self.aboutPanelLicenseView];
// Enable/disable debug logging. Hidden unless you option-click the status bar icon.
debugLoggingMenuItem =
[[BGMDebugLoggingMenuItem alloc] initWithMenuItem:self.debugLoggingMenuItemUnwrapped];
[statusBarItem setDebugLoggingMenuItem:debugLoggingMenuItem];
// Handle events about the main menu. (See the NSMenuDelegate methods below.)
self.bgmMenu.delegate = self;
}
- (BGMUserDefaults*) createUserDefaults {
BOOL persistentDefaults =
[NSProcessInfo.processInfo.arguments indexOfObject:kOptNoPersistentData] == NSNotFound;
NSUserDefaults* wrappedDefaults = persistentDefaults ? [NSUserDefaults standardUserDefaults] : nil;
return [[BGMUserDefaults alloc] initWithDefaults:wrappedDefaults];
}
- (void) initVolumesMenuSection {
// Create the menu item with the (main) output volume slider.
BGMOutputVolumeMenuItem* outputVolume =
[[BGMOutputVolumeMenuItem alloc] initWithAudioDevices:audioDevices
view:self.outputVolumeView
slider:self.outputVolumeSlider
deviceLabel:self.outputVolumeLabel];
[audioDevices setOutputVolumeMenuItem:outputVolume];
NSInteger headingIdx = [self.bgmMenu indexOfItemWithTag:kVolumesHeadingMenuItemTag];
// Add it to the main menu below the "Volumes" heading.
[self.bgmMenu insertItem:outputVolume atIndex:(headingIdx + 1)];
// Add the volume control for system (UI) sounds to the menu.
BGMAudioDevice uiSoundsDevice = [audioDevices bgmDevice].GetUISoundsBGMDeviceInstance();
systemSoundsVolume =
[[BGMSystemSoundsVolume alloc] initWithUISoundsDevice:uiSoundsDevice
view:self.systemSoundsView
slider:self.systemSoundsSlider];
[self.bgmMenu insertItem:systemSoundsVolume.menuItem atIndex:(headingIdx + 2)];
// Add the app volumes to the menu.
appVolumes = [[BGMAppVolumesController alloc] initWithMenu:self.bgmMenu
appVolumeView:self.appVolumeView
audioDevices:audioDevices];
}
- (void) applicationWillTerminate:(NSNotification*)aNotification {
#pragma unused (aNotification)
DebugMsg("BGMAppDelegate::applicationWillTerminate");
// Change the user's default output device back.
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
if (error) {
[self showSetDeviceAsDefaultError:error
message:@"Failed to reset your system's audio output device."
informativeText:@"You'll have to change it yourself to get audio working again."];
}
}
#pragma mark Error messages
- (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 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];
[alert runModal];
});
}
- (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;
// NSAlert should only be used on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert* alert = [NSAlert new];
// TODO: Offer to install BGMXPCHelper if it's missing.
// TODO: Show suppression button?
[alert setMessageText:@"Error connecting to BGMXPCHelper."];
[alert setInformativeText:[NSString stringWithFormat:@"%s%s%@ (%lu)",
"Make sure you have BGMXPCHelper installed. There are instructions in the "
"README.md file.\n\n"
"Background Music might still work, but it won't work as well as it could.",
"\n\nDetails:\n",
[error localizedDescription],
[error code]]];
[alert runModal];
});
}
}
- (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 {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@ %@ Error: %@", msg, info, error);
NSAlert* alert = [NSAlert alertWithError:error];
alert.messageText = msg;
alert.informativeText = info;
[alert addButtonWithTitle:@"OK"];
[alert addButtonWithTitle:@"Open Sound in System Preferences"];
NSModalResponse buttonClicked = [alert runModal];
if (buttonClicked != NSAlertFirstButtonReturn) { // 'OK' is the first button.
[self openSysPrefsSoundOutput];
}
});
}
- (void) openSysPrefsSoundOutput {
SystemPreferencesApplication* __nullable sysPrefs =
[SBApplication applicationWithBundleIdentifier:@"com.apple.systempreferences"];
if (!sysPrefs) {
NSLog(@"Could not open System Preferences");
return;
}
// In System Preferences, go to the "Output" tab on the "Sound" pane.
for (SystemPreferencesPane* pane : [sysPrefs panes]) {
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: pane = %s", [pane.name UTF8String]);
if ([pane.id isEqualToString:@"com.apple.preference.sound"]) {
sysPrefs.currentPane = pane;
for (SystemPreferencesAnchor* anchor : [pane anchors]) {
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: anchor = %s", [anchor.name UTF8String]);
if ([[anchor.name lowercaseString] isEqualToString:@"output"]) {
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: Showing Output in Sound pane.");
[anchor reveal];
}
}
}
}
// Bring System Preferences to the foreground.
[sysPrefs activate];
}
#pragma mark NSMenuDelegate
- (void) menuNeedsUpdate:(NSMenu*)menu {
if ([menu isEqual:self.bgmMenu]) {
[autoPauseMenuItem parentMenuNeedsUpdate];
} else {
DebugMsg("BGMAppDelegate::menuNeedsUpdate: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
}
}
- (void) menu:(NSMenu*)menu willHighlightItem:(NSMenuItem* __nullable)item {
if ([menu isEqual:self.bgmMenu]) {
[autoPauseMenuItem parentMenuItemWillHighlight:item];
} else {
DebugMsg("BGMAppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
}
}
@end
#pragma clang assume_nonnull end