From 944fc112128e2e9513fea73473c347b1e5bd64f0 Mon Sep 17 00:00:00 2001 From: Kyle Neideck Date: Sat, 24 Feb 2018 11:47:27 +1100 Subject: [PATCH] Add workaround for FaceTime volume. FaceTime plays call audio using a daemon called avconferenced, so BGMDriver can't tell where the audio is actually coming from. As a hopefully temporary fix, BGMApp now just sets avconferenced's volume to match FaceTime's. See #139. Also, - set a tooltip and accessibility label for BGMApp's status bar item (the thing you click to show the main menu), and - some minor refactoring. --- BGMApp/BGMApp/BGMAppDelegate.mm | 78 ++++++++++++++-------- BGMApp/BGMApp/BGMAppVolumes.h | 10 +-- BGMApp/BGMApp/BGMAppVolumes.m | 76 +++++++++++---------- BGMApp/BGMApp/BGMAppVolumesController.h | 10 +++ BGMApp/BGMApp/BGMAppVolumesController.mm | 62 +++++++++++++++-- BGMApp/BGMApp/BGMAudioDeviceManager.h | 7 -- BGMApp/BGMApp/BGMAudioDeviceManager.mm | 16 +---- BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp | 2 +- BGMApp/BGMApp/BGMBackgroundMusicDevice.h | 2 +- README.md | 2 + 10 files changed, 171 insertions(+), 94 deletions(-) diff --git a/BGMApp/BGMApp/BGMAppDelegate.mm b/BGMApp/BGMApp/BGMAppDelegate.mm index 7b2acb33..458eb0cc 100644 --- a/BGMApp/BGMApp/BGMAppDelegate.mm +++ b/BGMApp/BGMApp/BGMAppDelegate.mm @@ -17,7 +17,7 @@ // BGMAppDelegate.mm // BGMApp // -// Copyright © 2016, 2017 Kyle Neideck +// Copyright © 2016-2018 Kyle Neideck // // Self Includes @@ -43,7 +43,9 @@ #pragma clang assume_nonnull begin -static float const kStatusBarIconPadding = 0.25; +static float const kStatusBarIconPadding = 0.25; +static NSString* const kOptNoPersistentData = @"--no-persistent-data"; +static NSString* const kOptShowDockIcon = @"--show-dock-icon"; @implementation BGMAppDelegate { // The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main menu @@ -65,8 +67,9 @@ @implementation BGMAppDelegate { @synthesize audioDevices = audioDevices; - (void) 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:@"--show-dock-icon"] != NSNotFound) { + // 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]; } @@ -78,13 +81,24 @@ - (void) awakeFromNib { // Set up the status bar item. (The thing you click to show BGMApp's UI.) - (void) initStatusBarItem { statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - - // Set the icon - NSImage* icon = [NSImage imageNamed:@"FermataIcon"]; // NSStatusItem doesn't have the "button" property on OS X 10.9. BOOL buttonAvailable = (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10); + // Set the title/tooltip to "Background Music". + statusBarItem.title = [NSRunningApplication currentApplication].localizedName; + statusBarItem.toolTip = statusBarItem.title; + + if (buttonAvailable) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItem.button.accessibilityLabel = statusBarItem.title; +#pragma clang diagnostic pop + } + + // Set the icon. + NSImage* icon = [NSImage imageNamed:@"FermataIcon"]; + if (icon != nil) { NSRect statusBarItemFrame; @@ -151,45 +165,26 @@ - (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]; - + autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices musicPlayers:musicPlayers]; - autoPauseMenuItem = [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped - autoPauseMusic:autoPauseMusic - musicPlayers:musicPlayers - userDefaults:userDefaults]; + [self setUpMainMenu:userDefaults]; xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices helperConnectionErrorHandler:^(NSError* error) { NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) " "BGMXPCHelper connection error: %@", error); - [self showXPCHelperErrorMessage:error]; }]; - [self initVolumesMenuSection]; - prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu - audioDevices:audioDevices - musicPlayers:musicPlayers - aboutPanel:self.aboutPanel - aboutPanelLicenseView:self.aboutPanelLicenseView]; - // Handle events about the main menu. (See the NSMenuDelegate methods below.) - self.bgmMenu.delegate = self; -} - -- (BGMUserDefaults*) createUserDefaults { - BOOL persistentDefaults = [NSProcessInfo.processInfo.arguments indexOfObject:@"--no-persistent-data"] == NSNotFound; - NSUserDefaults* wrappedDefaults = persistentDefaults ? [NSUserDefaults standardUserDefaults] : nil; - return [[BGMUserDefaults alloc] initWithDefaults:wrappedDefaults]; } // Returns NO if (and only if) BGMApp is about to terminate because of a fatal error. @@ -214,6 +209,32 @@ - (BOOL) initAudioDeviceManager { return YES; } +- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults { + autoPauseMenuItem = + [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped + autoPauseMusic:autoPauseMusic + musicPlayers:musicPlayers + userDefaults:userDefaults]; + + [self initVolumesMenuSection]; + + prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu + audioDevices:audioDevices + musicPlayers:musicPlayers + aboutPanel:self.aboutPanel + aboutPanelLicenseView:self.aboutPanelLicenseView]; + + // 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 = @@ -249,6 +270,7 @@ - (void) applicationWillTerminate:(NSNotification*)aNotification { DebugMsg("BGMAppDelegate::applicationWillTerminate"); + // Change the user's default output device back. NSError* error = [audioDevices unsetBGMDeviceAsOSDefault]; if (error) { diff --git a/BGMApp/BGMApp/BGMAppVolumes.h b/BGMApp/BGMApp/BGMAppVolumes.h index 717a41d5..f18f8af9 100644 --- a/BGMApp/BGMApp/BGMAppVolumes.h +++ b/BGMApp/BGMApp/BGMAppVolumes.h @@ -21,18 +21,19 @@ // // Local Includes -#import "BGMAudioDeviceManager.h" +#import "BGMAppVolumesController.h" // System Includes #import + #pragma clang assume_nonnull begin @interface BGMAppVolumes : NSObject -- (id) initWithMenu:(NSMenu*)menu - appVolumeView:(NSView*)view - audioDevices:(BGMAudioDeviceManager*)audioDevices; +- (id) initWithController:(BGMAppVolumesController*)inController + bgmMenu:(NSMenu*)inMenu + appVolumeView:(NSView*)inView; // Pass -1 for initialVolume or initialPan to leave the volume/pan at its default level. - (void) insertMenuItemForApp:(NSRunningApplication*)app @@ -51,6 +52,7 @@ - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl menuItem:(NSMenuItem*)item; @end diff --git a/BGMApp/BGMApp/BGMAppVolumes.m b/BGMApp/BGMApp/BGMAppVolumes.m index c7a7cd9c..52af84d2 100644 --- a/BGMApp/BGMApp/BGMAppVolumes.m +++ b/BGMApp/BGMApp/BGMAppVolumes.m @@ -17,7 +17,7 @@ // BGMAppVolumes.m // BGMApp // -// Copyright © 2016, 2017 Kyle Neideck +// Copyright © 2016-2018 Kyle Neideck // Copyright © 2017 Andrew Tonner // @@ -38,27 +38,27 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps"; @implementation BGMAppVolumes { + BGMAppVolumesController* controller; + NSMenu* bgmMenu; NSMenu* moreAppsMenu; NSView* appVolumeView; CGFloat appVolumeViewFullHeight; - - BGMAudioDeviceManager* audioDevices; // The number of menu items this class has added to bgmMenu. Doesn't include the More Apps menu. NSInteger numMenuItems; } -- (id) initWithMenu:(NSMenu*)menu - appVolumeView:(NSView*)view - audioDevices:(BGMAudioDeviceManager*)devices { +- (id) initWithController:(BGMAppVolumesController*)inController + bgmMenu:(NSMenu*)inMenu + appVolumeView:(NSView*)inView { if ((self = [super init])) { - bgmMenu = menu; + controller = inController; + bgmMenu = inMenu; moreAppsMenu = [[NSMenu alloc] initWithTitle:kMoreAppsMenuTitle]; - appVolumeView = view; + appVolumeView = inView; appVolumeViewFullHeight = appVolumeView.frame.size.height; - audioDevices = devices; numMenuItems = 0; // Add the More Apps menu to the main menu. @@ -81,12 +81,6 @@ - (id) initWithMenu:(NSMenu*)menu return self; } -// This method allows the Interface Builder Custom Classes for controls (below) to send their values -// directly to BGMDevice. Not public to other classes. -- (BGMAudioDeviceManager*) audioDevices { - return audioDevices; -} - #pragma mark UI Modifications - (void) insertMenuItemForApp:(NSRunningApplication*)app @@ -99,6 +93,7 @@ - (void) insertMenuItemForApp:(NSRunningApplication*)app if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) { [(NSView*)subview setUpWithApp:app context:self + controller:controller menuItem:appVolItem]; } } @@ -276,8 +271,11 @@ - (void) removeAllAppVolumeMenuItems { @implementation BGMAVM_AppIcon -- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem { - #pragma unused (ctx, menuItem) +- (void) setUpWithApp:(NSRunningApplication*)app + context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl + menuItem:(NSMenuItem*)menuItem { + #pragma unused (ctx, ctrl, menuItem) self.image = app.icon; @@ -296,8 +294,11 @@ - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx men @implementation BGMAVM_AppNameLabel -- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem { - #pragma unused (ctx, menuItem) +- (void) setUpWithApp:(NSRunningApplication*)app + context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl + menuItem:(NSMenuItem*)menuItem { + #pragma unused (ctx, ctrl, menuItem) NSString* name = app.localizedName ? (NSString*)app.localizedName : @""; self.stringValue = name; @@ -307,8 +308,11 @@ - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx men @implementation BGMAVM_ShowMoreControlsButton -- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem { - #pragma unused (app) +- (void) setUpWithApp:(NSRunningApplication*)app + context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl + menuItem:(NSMenuItem*)menuItem { + #pragma unused (app, ctrl) // Set up the button that show/hide the extra controls (currently only a pan slider) for the app. self.cell.representedObject = menuItem; @@ -336,13 +340,16 @@ @implementation BGMAVM_VolumeSlider { // Will be set to -1 for apps without a pid pid_t appProcessID; NSString* __nullable appBundleID; - BGMAppVolumes* context; + BGMAppVolumesController* controller; } -- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem { - #pragma unused (menuItem) +- (void) setUpWithApp:(NSRunningApplication*)app + context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl + menuItem:(NSMenuItem*)menuItem { + #pragma unused (ctx, menuItem) - context = ctx; + controller = ctrl; self.target = self; self.action = @selector(appVolumeChanged); @@ -388,9 +395,7 @@ - (void) appVolumeChanged { // The values from our sliders are in // [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already. - [context.audioDevices setVolume:self.intValue - forAppWithProcessID:appProcessID - bundleID:appBundleID]; + [controller setVolume:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID]; } @end @@ -399,13 +404,16 @@ @implementation BGMAVM_PanSlider { // Will be set to -1 for apps without a pid pid_t appProcessID; NSString* __nullable appBundleID; - BGMAppVolumes* context; + BGMAppVolumesController* controller; } -- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem { - #pragma unused (menuItem) +- (void) setUpWithApp:(NSRunningApplication*)app + context:(BGMAppVolumes*)ctx + controller:(BGMAppVolumesController*)ctrl + menuItem:(NSMenuItem*)menuItem { + #pragma unused (ctx, menuItem) - context = ctx; + controller = ctrl; self.target = self; self.action = @selector(appPanPositionChanged); @@ -434,9 +442,7 @@ - (void) appPanPositionChanged { DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue); // The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already. - [context.audioDevices setPanPosition:self.intValue - forAppWithProcessID:appProcessID - bundleID:appBundleID]; + [controller setPanPosition:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID]; } @end diff --git a/BGMApp/BGMApp/BGMAppVolumesController.h b/BGMApp/BGMApp/BGMAppVolumesController.h index dc2aff99..24b0337d 100644 --- a/BGMApp/BGMApp/BGMAppVolumesController.h +++ b/BGMApp/BGMApp/BGMAppVolumesController.h @@ -35,6 +35,16 @@ appVolumeView:(NSView*)view audioDevices:(BGMAudioDeviceManager*)audioDevices; +// See BGMBackgroundMusicDevice::SetAppVolume. +- (void) setVolume:(SInt32)volume +forAppWithProcessID:(pid_t)processID + bundleID:(NSString* __nullable)bundleID; + +// See BGMBackgroundMusicDevice::SetPanVolume. +- (void) setPanPosition:(SInt32)pan + forAppWithProcessID:(pid_t)processID + bundleID:(NSString* __nullable)bundleID; + @end #pragma clang assume_nonnull end diff --git a/BGMApp/BGMApp/BGMAppVolumesController.mm b/BGMApp/BGMApp/BGMAppVolumesController.mm index 4fbf1097..5a1c05a0 100644 --- a/BGMApp/BGMApp/BGMAppVolumesController.mm +++ b/BGMApp/BGMApp/BGMAppVolumesController.mm @@ -17,7 +17,7 @@ // BGMAppVolumesController.mm // BGMApp // -// Copyright © 2017 Kyle Neideck +// Copyright © 2017, 2018 Kyle Neideck // Copyright © 2017 Andrew Tonner // @@ -34,6 +34,9 @@ #import "CACFDictionary.h" #import "CACFString.h" +// System Includes +#include + #pragma clang assume_nonnull begin @@ -48,14 +51,16 @@ @implementation BGMAppVolumesController { BGMAudioDeviceManager* audioDevices; } +#pragma mark Initialisation + - (id) initWithMenu:(NSMenu*)menu appVolumeView:(NSView*)view audioDevices:(BGMAudioDeviceManager*)devices { if ((self = [super init])) { audioDevices = devices; - appVolumes = [[BGMAppVolumes alloc] initWithMenu:menu - appVolumeView:view - audioDevices:devices]; + appVolumes = [[BGMAppVolumes alloc] initWithController:self + bgmMenu:menu + appVolumeView:view]; // Create the menu items for controlling app volumes. NSArray* apps = [[NSWorkspace sharedWorkspace] runningApplications]; @@ -150,6 +155,55 @@ - (void) removeMenuItemsForApps:(NSArray*)apps { } } +#pragma mark Accessors + +- (void) setVolume:(SInt32)volume +forAppWithProcessID:(pid_t)processID + bundleID:(NSString* __nullable)bundleID { + // Update the app's volume. + audioDevices.bgmDevice.SetAppVolume(volume, processID, (__bridge_retained CFStringRef)bundleID); + + // If this volume is for FaceTime, set the volume for the avconferenced process as well. This + // works around FaceTime not playing its own audio. It plays UI sounds through + // systemsoundserverd and call audio through avconferenced. + // + // This isn't ideal because other apps might play audio through avconferenced, but I don't see a + // good way we could find out which app is actually playing the audio. We could probably figure + // it out from reading avconferenced's logs, at least, if it turns out to be important. See + // https://github.com/kyleneideck/BackgroundMusic/issues/139. + if ([bundleID isEqual:@"com.apple.FaceTime"]) { + [self setAvconferencedVolume:volume]; + } +} + +- (void) setAvconferencedVolume:(SInt32)volume { + // TODO: This volume will be lost if avconferenced is restarted. + pid_t pids[1024]; + size_t procCount = proc_listallpids(pids, 1024); + char path[PROC_PIDPATHINFO_MAXSIZE]; + + for (int i = 0; i < procCount; i++) { + pid_t pid = pids[i]; + + if (proc_pidpath(pid, path, sizeof(path)) > 0 && + strncmp(path, "/usr/libexec/avconferenced", sizeof(path)) == 0) { + DebugMsg("Setting avconferenced volume: %d", volume); + audioDevices.bgmDevice.SetAppVolume(volume, pid, nullptr); + return; + } + } + + LogWarning("Failed to set avconferenced volume."); +} + +- (void) setPanPosition:(SInt32)pan + forAppWithProcessID:(pid_t)processID + bundleID:(NSString* __nullable)bundleID { + audioDevices.bgmDevice.SetAppPanPosition(pan, + processID, + (__bridge_retained CFStringRef)bundleID); +} + #pragma mark KVO - (void) observeValueForKeyPath:(NSString* __nullable)keyPath diff --git a/BGMApp/BGMApp/BGMAudioDeviceManager.h b/BGMApp/BGMApp/BGMAudioDeviceManager.h index d1144b31..8e197b69 100644 --- a/BGMApp/BGMApp/BGMAudioDeviceManager.h +++ b/BGMApp/BGMApp/BGMAudioDeviceManager.h @@ -68,13 +68,6 @@ static const int kBGMErrorCode_ReturningEarly = 3; - (CAHALAudioDevice) outputDevice; #endif -- (void) setVolume:(SInt32)volume -forAppWithProcessID:(pid_t)processID - bundleID:(NSString* __nullable)bundleID; -- (void) setPanPosition:(SInt32)pan - forAppWithProcessID:(pid_t)processID - bundleID:(NSString* __nullable)bundleID; - - (BOOL) isOutputDevice:(AudioObjectID)deviceID; - (BOOL) isOutputDataSource:(UInt32)dataSourceID; diff --git a/BGMApp/BGMApp/BGMAudioDeviceManager.mm b/BGMApp/BGMApp/BGMAudioDeviceManager.mm index 1f98d738..ba4f72e9 100644 --- a/BGMApp/BGMApp/BGMAudioDeviceManager.mm +++ b/BGMApp/BGMApp/BGMAudioDeviceManager.mm @@ -258,18 +258,6 @@ - (CAHALAudioDevice) outputDevice { return outputDevice; } -- (void) setVolume:(SInt32)volume -forAppWithProcessID:(pid_t)processID - bundleID:(NSString* __nullable)bundleID { - bgmDevice->SetAppVolume(volume, processID, (__bridge_retained CFStringRef)bundleID); -} - -- (void) setPanPosition:(SInt32)pan - forAppWithProcessID:(pid_t)processID - bundleID:(NSString* __nullable)bundleID { - bgmDevice->SetAppPanPosition(pan, processID, (__bridge_retained CFStringRef)bundleID); -} - - (BOOL) isOutputDevice:(AudioObjectID)deviceID { @try { [stateLock lock]; @@ -399,7 +387,7 @@ - (void) setOutputDeviceForPlaythroughAndControlSync:(const BGMAudioDevice&)newO } - (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice&)device { - BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", [&] { + BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", ([&] { AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput; UInt32 channel = 0; @@ -409,7 +397,7 @@ - (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice&)device { device.SetCurrentDataSourceByID(scope, channel, dataSourceID); } - }); + })); } - (void) propagateOutputDeviceChange { diff --git a/BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp b/BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp index 7ed9ceb3..3d82118d 100644 --- a/BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp +++ b/BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp @@ -132,7 +132,7 @@ CFArrayRef BGMBackgroundMusicDevice::GetAppVolumes() const void BGMBackgroundMusicDevice::SetAppVolume(SInt32 inVolume, pid_t inAppProcessID, - CFStringRef inAppBundleID) + CFStringRef __nullable inAppBundleID) { BGMAssert((kAppRelativeVolumeMinRawValue <= inVolume) && (inVolume <= kAppRelativeVolumeMaxRawValue), diff --git a/BGMApp/BGMApp/BGMBackgroundMusicDevice.h b/BGMApp/BGMApp/BGMBackgroundMusicDevice.h index 123ba6a4..7207862b 100644 --- a/BGMApp/BGMApp/BGMBackgroundMusicDevice.h +++ b/BGMApp/BGMApp/BGMBackgroundMusicDevice.h @@ -103,7 +103,7 @@ class BGMBackgroundMusicDevice */ void SetAppVolume(SInt32 inVolume, pid_t inAppProcessID, - CFStringRef inAppBundleID); + CFStringRef __nullable inAppBundleID); /*! @param inPanPosition A value between kAppPanLeftRawValue and kAppPanRightRawValue from BGM_Types.h. A negative value has a higher proportion of left channel, and diff --git a/README.md b/README.md index 76fbcb3e..11e2fca0 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Still very much in alpha. Not code signed, so you'll have to **right-click it an > [sig](https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.1.1/BackgroundMusic-0.1.1.pkg.asc), > [key (0595DF814E41A6F69334C5E2CAA8D9B8E39EC18C)](https://bearisdriving.com/kyle-neideck.gpg) +We also have [snapshot builds](https://github.com/kyleneideck/BackgroundMusic/releases). + ## Auto-pause music Background Music can pause your music player app when other audio starts playing and unpause it afterwards. The idea is