Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions BGMApp/BGMApp/BGMAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ - (void) continueLaunchAfterInputDevicePermissionGranted {
userDefaults:userDefaults];

autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
musicPlayers:musicPlayers];
musicPlayers:musicPlayers
userDefaults:userDefaults];

[self setUpMainMenu];

Expand Down Expand Up @@ -317,7 +318,8 @@ - (void) setUpMainMenu {
musicPlayers:musicPlayers
statusBarItem:statusBarItem
aboutPanel:self.aboutPanel
aboutPanelLicenseView:self.aboutPanelLicenseView];
aboutPanelLicenseView:self.aboutPanelLicenseView
userDefaults:userDefaults];

// Enable/disable debug logging. Hidden unless you option-click the status bar icon.
debugLoggingMenuItem =
Expand Down
3 changes: 2 additions & 1 deletion BGMApp/BGMApp/BGMAutoPauseMusic.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
// Local Includes
#import "BGMAudioDeviceManager.h"
#import "BGMMusicPlayers.h"
#import "BGMUserDefaults.h"

// System Includes
#import <Foundation/Foundation.h>
Expand All @@ -35,7 +36,7 @@

@interface BGMAutoPauseMusic : NSObject

- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers;
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers userDefaults:(BGMUserDefaults*)inUserDefaults;

- (void) enable;
- (void) disable;
Expand Down
83 changes: 56 additions & 27 deletions BGMApp/BGMApp/BGMAutoPauseMusic.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,6 @@
#include <mach/mach_time.h>


// How long to wait before pausing/unpausing. This is so short sounds can play without awkwardly causing a short period of silence,
// and other audio can have short periods of silence without causing music to play and quickly pause again. Of course, it's a
// trade-off against how long the music will overlap the other audio before it gets paused and how long the music will stay paused
// after a sound that was only slightly longer than the pause delay.
static UInt64 const kPauseDelayNSec = 1500 * NSEC_PER_MSEC;
// The delay before unpausing the music player is proportional to how long we paused it for, bounded by these limits. This makes it
// a bit less annoying when a sound is just long enough to cause an auto-pause.
//
// I haven't spent much time experimenting with different values for these constants, so they could probably be improved a fair
// bit.
//
// TODO: Would it be worth listening for kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp so we can unpause
// immediately if we haven't been paused for long and the non-music-player client stops IO? That would usually indicate that
// it doesn't intend to start playing audio again soon. We'd also have to deal with music players that don't stop IO when
// they're paused.
static UInt64 const kMaxUnpauseDelayNSec = 3500 * NSEC_PER_MSEC;
static UInt64 const kMinUnpauseDelayNSec = kMaxUnpauseDelayNSec / 10;
// We multiply the time spent paused by this factor to calculate the delay before we consider unpausing.
static Float32 const kUnpauseDelayWeightingFactor = 0.1f;

Expand All @@ -60,6 +43,7 @@ @implementation BGMAutoPauseMusic {

BGMAudioDeviceManager* audioDevices;
BGMMusicPlayers* musicPlayers;
BGMUserDefaults* userDefaults;

dispatch_queue_t listenerQueue;
// Have to keep track of the listener block we add so we can remove it later.
Expand All @@ -76,10 +60,11 @@ @implementation BGMAutoPauseMusic {
UInt64 wentAudible;
}

- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers {
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers userDefaults:(BGMUserDefaults*)inUserDefaults {
if ((self = [super init])) {
audioDevices = inAudioDevices;
musicPlayers = inMusicPlayers;
userDefaults = inUserDefaults;

enabled = NO;
wePaused = NO;
Expand Down Expand Up @@ -135,6 +120,7 @@ - (void) initListenerBlock {
} else if (audibleState == kBGMDeviceIsSilentExceptMusic) {
// If we pause the music player and then the user unpauses it before the other audio stops, we need to set
// wePaused to false at some point before the other audio starts again so we know we should pause
DebugMsg("BGMAutoPauseMusic: Device is silent except music, resetting wePaused flag");
wePaused = NO;
}
// TODO: Add a fourth audible state, something like "AudibleAndMusicPlaying", and check it here to
Expand All @@ -153,8 +139,23 @@ - (void) queuePauseBlock {
wentAudible = now;
UInt64 startedPauseDelay = now;

UInt64 pauseDelayMS = userDefaults.pauseDelayMS;

// If pause delay is 0, pause immediately (no delay)
if (pauseDelayMS == 0) {
DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Pause delay is 0, pausing immediately");

// Pause immediately if device is audible and we haven't already paused
if (!wePaused && ([self deviceAudibleState] == kBGMDeviceIsAudible)) {
wePaused = ([musicPlayers.selectedMusicPlayer pause] || wePaused);
}
return;
}

UInt64 pauseDelayNSec = pauseDelayMS * NSEC_PER_MSEC;

DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Dispatching pause block at %llu", now);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kPauseDelayNSec),
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, pauseDelayNSec),
pauseUnpauseMusicQueue,
^{
BOOL stillAudible = ([self deviceAudibleState] == kBGMDeviceIsAudible);
Expand All @@ -178,6 +179,27 @@ - (void) queueUnpauseBlock {
wentSilent = now;
UInt64 startedUnpauseDelay = now;

// Get user-configurable max delay
UInt64 maxUnpauseDelayMS = userDefaults.maxUnpauseDelayMS;

// If max unpause delay is 0, unpause immediately (no delay)
if (maxUnpauseDelayMS == 0) {
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Max unpause delay is 0, unpausing immediately");

// Unpause immediately if we were the one who paused and device is still silent
BGMDeviceAudibleState currentState = [self deviceAudibleState];
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Immediate unpause - wePaused=%s, currentState=%s",
wePaused ? "YES" : "NO",
currentState == kBGMDeviceIsSilent ? "Silent" :
(currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic"));

if (wePaused && (currentState == kBGMDeviceIsSilent)) {
wePaused = NO;
[musicPlayers.selectedMusicPlayer unpause];
}
return;
}

// Unpause sooner if we've only been paused for a short time. This is so a notification sound causing an auto-pause is
// less of an interruption.
//
Expand All @@ -191,9 +213,11 @@ - (void) queueUnpauseBlock {
mach_timebase_info(&info);
unpauseDelayNsec = unpauseDelayNsec * info.numer / info.denom;

// Clamp.
unpauseDelayNsec = std::min(kMaxUnpauseDelayNSec, unpauseDelayNsec);
unpauseDelayNsec = std::max(kMinUnpauseDelayNSec, unpauseDelayNsec);
// Clamp using user-configurable max delay and calculated min delay.
UInt64 maxUnpauseDelayNSec = maxUnpauseDelayMS * NSEC_PER_MSEC;
UInt64 minUnpauseDelayNSec = maxUnpauseDelayNSec / 10;
unpauseDelayNsec = std::min(maxUnpauseDelayNSec, unpauseDelayNsec);
unpauseDelayNsec = std::max(minUnpauseDelayNSec, unpauseDelayNsec);

DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Dispatched unpause block at %llu. unpauseDelayNsec=%llu",
now,
Expand All @@ -202,17 +226,22 @@ - (void) queueUnpauseBlock {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, unpauseDelayNsec),
pauseUnpauseMusicQueue,
^{
BOOL stillSilent = ([self deviceAudibleState] == kBGMDeviceIsSilent);
BGMDeviceAudibleState currentState = [self deviceAudibleState];
BOOL stillSilent = (currentState == kBGMDeviceIsSilent);
BOOL isLatestUnpause = (startedUnpauseDelay == wentSilent);

DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu.%s%s wentSilent=%llu",
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu. wePaused=%s, isLatest=%s, currentState=%s, wentSilent=%llu",
startedUnpauseDelay,
wePaused ? "" : " Not unpausing because we weren't the one who paused.",
stillSilent ? "" : " Not unpausing because the device isn't silent.",
wePaused ? "YES" : "NO",
isLatestUnpause ? "YES" : "NO",
currentState == kBGMDeviceIsSilent ? "Silent" :
(currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic"),
wentSilent);

// Unpause if we were the one who paused. Also check that this is the most recent unpause block and the
// device is still silent, which means the audible state hasn't changed since this block was queued.
if (wePaused && (startedUnpauseDelay == wentSilent) && stillSilent) {
if (wePaused && isLatestUnpause && stillSilent) {
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Unpausing music player");
wePaused = NO;
[musicPlayers.selectedMusicPlayer unpause];
}
Expand Down
5 changes: 3 additions & 2 deletions BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

// STL Includes
#import <set>
#import <vector>


#pragma clang assume_nonnull begin
Expand Down Expand Up @@ -220,9 +221,9 @@ - (void) insertMenuItemsForDevice:(BGMAudioDevice)device {
});

if (numDataSources > 0) {
UInt32 dataSourceIDs[numDataSources];
std::vector<UInt32> dataSourceIDs(numDataSources);
// This call updates numDataSources to the real number of IDs it added to our array.
device.GetAvailableDataSources(scope, channel, numDataSources, dataSourceIDs);
device.GetAvailableDataSources(scope, channel, numDataSources, dataSourceIDs.data());

for (UInt32 i = 0; i < numDataSources; i++) {
DebugMsg("BGMOutputDeviceMenuSection::createMenuItemsForDevice: "
Expand Down
5 changes: 5 additions & 0 deletions BGMApp/BGMApp/BGMUserDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
// exception is thrown.
@property NSString* __nullable googlePlayMusicDesktopPlayerPermanentAuthCode;

// Auto-pause delay settings in milliseconds. These control how long to wait before pausing/unpausing
// music when other audio starts/stops playing.
@property NSUInteger pauseDelayMS;
@property NSUInteger maxUnpauseDelayMS;

@end

#pragma clang assume_nonnull end
Expand Down
36 changes: 35 additions & 1 deletion BGMApp/BGMApp/BGMUserDefaults.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
static NSString* const kDefaultKeySelectedMusicPlayerID = @"SelectedMusicPlayerID";
static NSString* const kDefaultKeyPreferredDeviceUIDs = @"PreferredDeviceUIDs";
static NSString* const kDefaultKeyStatusBarIcon = @"StatusBarIcon";
static NSString* const kDefaultKeyPauseDelayMS = @"PauseDelayMS";
static NSString* const kDefaultKeyMaxUnpauseDelayMS = @"MaxUnpauseDelayMS";

// Labels for Keychain Data
static NSString* const kKeychainLabelGPMDPAuthCode =
Expand All @@ -57,7 +59,11 @@ - (instancetype) initWithDefaults:(NSUserDefaults* __nullable)inDefaults {
// here so we know when it's never been set. (If it hasn't, we try using BGMDevice's
// kAudioDeviceCustomPropertyMusicPlayerBundleID property to tell which music player should
// be selected. See BGMMusicPlayers.)
NSDictionary* defaultsDict = @{ kDefaultKeyAutoPauseMusicEnabled: @YES };
NSDictionary* defaultsDict = @{
kDefaultKeyAutoPauseMusicEnabled: @YES,
kDefaultKeyPauseDelayMS: @1500,
kDefaultKeyMaxUnpauseDelayMS: @3500
};

if (defaults) {
[defaults registerDefaults:defaultsDict];
Expand Down Expand Up @@ -89,6 +95,34 @@ - (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
[self setBool:kDefaultKeyAutoPauseMusicEnabled to:autoPauseMusicEnabled];
}

#pragma mark Auto-pause Delays

- (NSUInteger) pauseDelayMS {
NSInteger delay = [self getInt:kDefaultKeyPauseDelayMS or:1500];
// Clamp to reasonable range: 0ms to 10000ms
delay = MAX(0, MIN(10000, delay));
return (NSUInteger)delay;
}

- (void) setPauseDelayMS:(NSUInteger)pauseDelayMS {
// Clamp to reasonable range: 0ms to 10000ms
NSUInteger clampedDelay = MAX(0, MIN(10000, pauseDelayMS));
[self setInt:kDefaultKeyPauseDelayMS to:(NSInteger)clampedDelay];
}

- (NSUInteger) maxUnpauseDelayMS {
NSInteger delay = [self getInt:kDefaultKeyMaxUnpauseDelayMS or:3500];
// Clamp to reasonable range: 0ms to 10000ms
delay = MAX(0, MIN(10000, delay));
return (NSUInteger)delay;
}

- (void) setMaxUnpauseDelayMS:(NSUInteger)maxUnpauseDelayMS {
// Clamp to reasonable range: 0ms to 10000ms
NSUInteger clampedDelay = MAX(0, MIN(10000, maxUnpauseDelayMS));
[self setInt:kDefaultKeyMaxUnpauseDelayMS to:(NSInteger)clampedDelay];
}

- (NSArray<NSString*>*) preferredDeviceUIDs {
NSArray<NSString*>* __nullable uids = [self get:kDefaultKeyPreferredDeviceUIDs];
return uids ? BGMNN(uids) : @[];
Expand Down
4 changes: 3 additions & 1 deletion BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "BGMAudioDeviceManager.h"
#import "BGMMusicPlayers.h"
#import "BGMStatusBarItem.h"
#import "BGMUserDefaults.h"

// System Includes
#import <Cocoa/Cocoa.h>
Expand All @@ -41,7 +42,8 @@ NS_ASSUME_NONNULL_BEGIN
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView
userDefaults:(BGMUserDefaults*)inUserDefaults;

@end

Expand Down
Loading
Loading