Skip to content

Commit 53e4f45

Browse files
committed
Add configurable auto-pause delays for YouTube video compatibility
Implements user-configurable delays for auto-pause functionality to resolve issues where brief speaker pauses caused unwanted music resumption during YouTube videos. Users can now adjust both pause delay (time before pausing music when other audio starts) and max unpause delay (time before resuming music when other audio stops). Features: - New preferences UI with logarithmic sliders for fine control at small values - Range: 0ms (disabled) to 10000ms for both delays - Default values: 1500ms pause delay, 3500ms max unpause delay - Persistent storage via NSUserDefaults - Zero-delay support for immediate pause/unpause behavior
1 parent 0a27538 commit 53e4f45

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)