Skip to content

Commit 34ba468

Browse files
authored
Merge pull request kyleneideck#818 from jackdrowlands/feature/configurable-auto-pause-delays
Add configurable auto-pause delays for YouTube video compatibility
2 parents 0a27538 + 53e4f45 commit 34ba468

File tree

8 files changed

+282
-35
lines changed

8 files changed

+282
-35
lines changed

BGMApp/BGMApp/BGMAppDelegate.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ - (void) continueLaunchAfterInputDevicePermissionGranted {
177177
userDefaults:userDefaults];
178178

179179
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
180-
musicPlayers:musicPlayers];
180+
musicPlayers:musicPlayers
181+
userDefaults:userDefaults];
181182

182183
[self setUpMainMenu];
183184

@@ -317,7 +318,8 @@ - (void) setUpMainMenu {
317318
musicPlayers:musicPlayers
318319
statusBarItem:statusBarItem
319320
aboutPanel:self.aboutPanel
320-
aboutPanelLicenseView:self.aboutPanelLicenseView];
321+
aboutPanelLicenseView:self.aboutPanelLicenseView
322+
userDefaults:userDefaults];
321323

322324
// Enable/disable debug logging. Hidden unless you option-click the status bar icon.
323325
debugLoggingMenuItem =

BGMApp/BGMApp/BGMAutoPauseMusic.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
// Local Includes
2727
#import "BGMAudioDeviceManager.h"
2828
#import "BGMMusicPlayers.h"
29+
#import "BGMUserDefaults.h"
2930

3031
// System Includes
3132
#import <Foundation/Foundation.h>
@@ -35,7 +36,7 @@
3536

3637
@interface BGMAutoPauseMusic : NSObject
3738

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

4041
- (void) enable;
4142
- (void) disable;

BGMApp/BGMApp/BGMAutoPauseMusic.mm

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,6 @@
3535
#include <mach/mach_time.h>
3636

3737

38-
// How long to wait before pausing/unpausing. This is so short sounds can play without awkwardly causing a short period of silence,
39-
// and other audio can have short periods of silence without causing music to play and quickly pause again. Of course, it's a
40-
// trade-off against how long the music will overlap the other audio before it gets paused and how long the music will stay paused
41-
// after a sound that was only slightly longer than the pause delay.
42-
static UInt64 const kPauseDelayNSec = 1500 * NSEC_PER_MSEC;
43-
// The delay before unpausing the music player is proportional to how long we paused it for, bounded by these limits. This makes it
44-
// a bit less annoying when a sound is just long enough to cause an auto-pause.
45-
//
46-
// I haven't spent much time experimenting with different values for these constants, so they could probably be improved a fair
47-
// bit.
48-
//
49-
// TODO: Would it be worth listening for kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp so we can unpause
50-
// immediately if we haven't been paused for long and the non-music-player client stops IO? That would usually indicate that
51-
// 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
52-
// they're paused.
53-
static UInt64 const kMaxUnpauseDelayNSec = 3500 * NSEC_PER_MSEC;
54-
static UInt64 const kMinUnpauseDelayNSec = kMaxUnpauseDelayNSec / 10;
5538
// We multiply the time spent paused by this factor to calculate the delay before we consider unpausing.
5639
static Float32 const kUnpauseDelayWeightingFactor = 0.1f;
5740

@@ -60,6 +43,7 @@ @implementation BGMAutoPauseMusic {
6043

6144
BGMAudioDeviceManager* audioDevices;
6245
BGMMusicPlayers* musicPlayers;
46+
BGMUserDefaults* userDefaults;
6347

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

79-
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers {
63+
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers userDefaults:(BGMUserDefaults*)inUserDefaults {
8064
if ((self = [super init])) {
8165
audioDevices = inAudioDevices;
8266
musicPlayers = inMusicPlayers;
67+
userDefaults = inUserDefaults;
8368

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

142+
UInt64 pauseDelayMS = userDefaults.pauseDelayMS;
143+
144+
// If pause delay is 0, pause immediately (no delay)
145+
if (pauseDelayMS == 0) {
146+
DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Pause delay is 0, pausing immediately");
147+
148+
// Pause immediately if device is audible and we haven't already paused
149+
if (!wePaused && ([self deviceAudibleState] == kBGMDeviceIsAudible)) {
150+
wePaused = ([musicPlayers.selectedMusicPlayer pause] || wePaused);
151+
}
152+
return;
153+
}
154+
155+
UInt64 pauseDelayNSec = pauseDelayMS * NSEC_PER_MSEC;
156+
156157
DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Dispatching pause block at %llu", now);
157-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kPauseDelayNSec),
158+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, pauseDelayNSec),
158159
pauseUnpauseMusicQueue,
159160
^{
160161
BOOL stillAudible = ([self deviceAudibleState] == kBGMDeviceIsAudible);
@@ -178,6 +179,27 @@ - (void) queueUnpauseBlock {
178179
wentSilent = now;
179180
UInt64 startedUnpauseDelay = now;
180181

182+
// Get user-configurable max delay
183+
UInt64 maxUnpauseDelayMS = userDefaults.maxUnpauseDelayMS;
184+
185+
// If max unpause delay is 0, unpause immediately (no delay)
186+
if (maxUnpauseDelayMS == 0) {
187+
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Max unpause delay is 0, unpausing immediately");
188+
189+
// Unpause immediately if we were the one who paused and device is still silent
190+
BGMDeviceAudibleState currentState = [self deviceAudibleState];
191+
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Immediate unpause - wePaused=%s, currentState=%s",
192+
wePaused ? "YES" : "NO",
193+
currentState == kBGMDeviceIsSilent ? "Silent" :
194+
(currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic"));
195+
196+
if (wePaused && (currentState == kBGMDeviceIsSilent)) {
197+
wePaused = NO;
198+
[musicPlayers.selectedMusicPlayer unpause];
199+
}
200+
return;
201+
}
202+
181203
// Unpause sooner if we've only been paused for a short time. This is so a notification sound causing an auto-pause is
182204
// less of an interruption.
183205
//
@@ -191,9 +213,11 @@ - (void) queueUnpauseBlock {
191213
mach_timebase_info(&info);
192214
unpauseDelayNsec = unpauseDelayNsec * info.numer / info.denom;
193215

194-
// Clamp.
195-
unpauseDelayNsec = std::min(kMaxUnpauseDelayNSec, unpauseDelayNsec);
196-
unpauseDelayNsec = std::max(kMinUnpauseDelayNSec, unpauseDelayNsec);
216+
// Clamp using user-configurable max delay and calculated min delay.
217+
UInt64 maxUnpauseDelayNSec = maxUnpauseDelayMS * NSEC_PER_MSEC;
218+
UInt64 minUnpauseDelayNSec = maxUnpauseDelayNSec / 10;
219+
unpauseDelayNsec = std::min(maxUnpauseDelayNSec, unpauseDelayNsec);
220+
unpauseDelayNsec = std::max(minUnpauseDelayNSec, unpauseDelayNsec);
197221

198222
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Dispatched unpause block at %llu. unpauseDelayNsec=%llu",
199223
now,
@@ -202,17 +226,22 @@ - (void) queueUnpauseBlock {
202226
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, unpauseDelayNsec),
203227
pauseUnpauseMusicQueue,
204228
^{
205-
BOOL stillSilent = ([self deviceAudibleState] == kBGMDeviceIsSilent);
229+
BGMDeviceAudibleState currentState = [self deviceAudibleState];
230+
BOOL stillSilent = (currentState == kBGMDeviceIsSilent);
231+
BOOL isLatestUnpause = (startedUnpauseDelay == wentSilent);
206232

207-
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu.%s%s wentSilent=%llu",
233+
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu. wePaused=%s, isLatest=%s, currentState=%s, wentSilent=%llu",
208234
startedUnpauseDelay,
209-
wePaused ? "" : " Not unpausing because we weren't the one who paused.",
210-
stillSilent ? "" : " Not unpausing because the device isn't silent.",
235+
wePaused ? "YES" : "NO",
236+
isLatestUnpause ? "YES" : "NO",
237+
currentState == kBGMDeviceIsSilent ? "Silent" :
238+
(currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic"),
211239
wentSilent);
212240

213241
// Unpause if we were the one who paused. Also check that this is the most recent unpause block and the
214242
// device is still silent, which means the audible state hasn't changed since this block was queued.
215-
if (wePaused && (startedUnpauseDelay == wentSilent) && stillSilent) {
243+
if (wePaused && isLatestUnpause && stillSilent) {
244+
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Unpausing music player");
216245
wePaused = NO;
217246
[musicPlayers.selectedMusicPlayer unpause];
218247
}

BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
// STL Includes
3838
#import <set>
39+
#import <vector>
3940

4041

4142
#pragma clang assume_nonnull begin
@@ -220,9 +221,9 @@ - (void) insertMenuItemsForDevice:(BGMAudioDevice)device {
220221
});
221222

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

227228
for (UInt32 i = 0; i < numDataSources; i++) {
228229
DebugMsg("BGMOutputDeviceMenuSection::createMenuItemsForDevice: "

BGMApp/BGMApp/BGMUserDefaults.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
// exception is thrown.
6161
@property NSString* __nullable googlePlayMusicDesktopPlayerPermanentAuthCode;
6262

63+
// Auto-pause delay settings in milliseconds. These control how long to wait before pausing/unpausing
64+
// music when other audio starts/stops playing.
65+
@property NSUInteger pauseDelayMS;
66+
@property NSUInteger maxUnpauseDelayMS;
67+
6368
@end
6469

6570
#pragma clang assume_nonnull end

BGMApp/BGMApp/BGMUserDefaults.m

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
static NSString* const kDefaultKeySelectedMusicPlayerID = @"SelectedMusicPlayerID";
3535
static NSString* const kDefaultKeyPreferredDeviceUIDs = @"PreferredDeviceUIDs";
3636
static NSString* const kDefaultKeyStatusBarIcon = @"StatusBarIcon";
37+
static NSString* const kDefaultKeyPauseDelayMS = @"PauseDelayMS";
38+
static NSString* const kDefaultKeyMaxUnpauseDelayMS = @"MaxUnpauseDelayMS";
3739

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

6268
if (defaults) {
6369
[defaults registerDefaults:defaultsDict];
@@ -89,6 +95,34 @@ - (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
8995
[self setBool:kDefaultKeyAutoPauseMusicEnabled to:autoPauseMusicEnabled];
9096
}
9197

98+
#pragma mark Auto-pause Delays
99+
100+
- (NSUInteger) pauseDelayMS {
101+
NSInteger delay = [self getInt:kDefaultKeyPauseDelayMS or:1500];
102+
// Clamp to reasonable range: 0ms to 10000ms
103+
delay = MAX(0, MIN(10000, delay));
104+
return (NSUInteger)delay;
105+
}
106+
107+
- (void) setPauseDelayMS:(NSUInteger)pauseDelayMS {
108+
// Clamp to reasonable range: 0ms to 10000ms
109+
NSUInteger clampedDelay = MAX(0, MIN(10000, pauseDelayMS));
110+
[self setInt:kDefaultKeyPauseDelayMS to:(NSInteger)clampedDelay];
111+
}
112+
113+
- (NSUInteger) maxUnpauseDelayMS {
114+
NSInteger delay = [self getInt:kDefaultKeyMaxUnpauseDelayMS or:3500];
115+
// Clamp to reasonable range: 0ms to 10000ms
116+
delay = MAX(0, MIN(10000, delay));
117+
return (NSUInteger)delay;
118+
}
119+
120+
- (void) setMaxUnpauseDelayMS:(NSUInteger)maxUnpauseDelayMS {
121+
// Clamp to reasonable range: 0ms to 10000ms
122+
NSUInteger clampedDelay = MAX(0, MIN(10000, maxUnpauseDelayMS));
123+
[self setInt:kDefaultKeyMaxUnpauseDelayMS to:(NSInteger)clampedDelay];
124+
}
125+
92126
- (NSArray<NSString*>*) preferredDeviceUIDs {
93127
NSArray<NSString*>* __nullable uids = [self get:kDefaultKeyPreferredDeviceUIDs];
94128
return uids ? BGMNN(uids) : @[];

BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#import "BGMAudioDeviceManager.h"
2828
#import "BGMMusicPlayers.h"
2929
#import "BGMStatusBarItem.h"
30+
#import "BGMUserDefaults.h"
3031

3132
// System Includes
3233
#import <Cocoa/Cocoa.h>
@@ -41,7 +42,8 @@ NS_ASSUME_NONNULL_BEGIN
4142
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
4243
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
4344
aboutPanel:(NSPanel*)inAboutPanel
44-
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
45+
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView
46+
userDefaults:(BGMUserDefaults*)inUserDefaults;
4547

4648
@end
4749

0 commit comments

Comments
 (0)