diff --git a/BGMApp/BGMApp.xcodeproj/project.pbxproj b/BGMApp/BGMApp.xcodeproj/project.pbxproj index 4dfb4a4..8290679 100644 --- a/BGMApp/BGMApp.xcodeproj/project.pbxproj +++ b/BGMApp/BGMApp.xcodeproj/project.pbxproj @@ -225,6 +225,8 @@ 27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGM_Utils.cpp"; }; }; 27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; }; 27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; }; + 9E129A412602AE620005851B /* BGMASApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E129A402602AE620005851B /* BGMASApplication.m */; }; + 9E542C7026057FBA0016C0B5 /* BGMASApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E129A402602AE620005851B /* BGMASApplication.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -433,6 +435,8 @@ 27F7D48F1D2483B100821C4B /* BGMDecibel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMDecibel.m; path = "Music Players/BGMDecibel.m"; sourceTree = ""; }; 27F7D4911D2484A300821C4B /* Decibel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Decibel.h; path = "Music Players/Decibel.h"; sourceTree = ""; }; 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BGM_Utils.cpp; path = ../SharedSource/BGM_Utils.cpp; sourceTree = ""; }; + 9E129A3F2602AE620005851B /* BGMASApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMASApplication.h; path = Scripting/BGMASApplication.h; sourceTree = ""; }; + 9E129A402602AE620005851B /* BGMASApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BGMASApplication.m; path = Scripting/BGMASApplication.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -562,6 +566,8 @@ 1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */, 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */, 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */, + 9E129A3F2602AE620005851B /* BGMASApplication.h */, + 9E129A402602AE620005851B /* BGMASApplication.m */, ); name = Scripting; sourceTree = ""; @@ -1152,6 +1158,7 @@ 1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */, 1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */, 1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */, + 9E129A412602AE620005851B /* BGMASApplication.m in Sources */, 1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */, 1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */, 1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */, @@ -1218,6 +1225,7 @@ 1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */, 1CD989371ECFFC9E0014BBBF /* CACFNumber.cpp in Sources */, 1CD989381ECFFC9E0014BBBF /* CACFString.cpp in Sources */, + 9E542C7026057FBA0016C0B5 /* BGMASApplication.m in Sources */, 1CD989391ECFFC9E0014BBBF /* CADebugger.cpp in Sources */, 1CD9893A1ECFFC9E0014BBBF /* CADebugMacros.cpp in Sources */, 1CD9893B1ECFFC9E0014BBBF /* CADebugPrintf.cpp in Sources */, diff --git a/BGMApp/BGMApp/BGMAppDelegate.h b/BGMApp/BGMApp/BGMAppDelegate.h index a2166c6..1a162cd 100644 --- a/BGMApp/BGMApp/BGMAppDelegate.h +++ b/BGMApp/BGMApp/BGMAppDelegate.h @@ -24,6 +24,7 @@ // Local Includes #import "BGMAudioDeviceManager.h" +#import "BGMAppVolumesController.h" // System Includes #import @@ -53,6 +54,7 @@ static NSInteger const kSeparatorBelowVolumesMenuItemTag = 4; @property (weak) IBOutlet NSMenuItem* debugLoggingMenuItemUnwrapped; @property (readonly) BGMAudioDeviceManager* audioDevices; +@property BGMAppVolumesController* appVolumes; @end diff --git a/BGMApp/BGMApp/BGMAppDelegate.mm b/BGMApp/BGMApp/BGMAppDelegate.mm index e754c9a..e647026 100644 --- a/BGMApp/BGMApp/BGMAppDelegate.mm +++ b/BGMApp/BGMApp/BGMAppDelegate.mm @@ -64,7 +64,6 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; BGMAutoPauseMenuItem* autoPauseMenuItem; BGMMusicPlayers* musicPlayers; BGMSystemSoundsVolume* systemSoundsVolume; - BGMAppVolumesController* appVolumes; BGMOutputDeviceMenuSection* outputDeviceMenuSection; BGMPreferencesMenu* prefsMenu; BGMDebugLoggingMenuItem* debugLoggingMenuItem; @@ -73,6 +72,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; } @synthesize audioDevices = audioDevices; +@synthesize appVolumes = appVolumes; - (void) awakeFromNib { [super awakeFromNib]; diff --git a/BGMApp/BGMApp/BGMAppVolumes.h b/BGMApp/BGMApp/BGMAppVolumes.h index f18f8af..3946d9e 100644 --- a/BGMApp/BGMApp/BGMAppVolumes.h +++ b/BGMApp/BGMApp/BGMAppVolumes.h @@ -44,6 +44,9 @@ - (void) removeAllAppVolumeMenuItems; +- (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app; +- (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app; + @end // Protocol for the UI custom classes diff --git a/BGMApp/BGMApp/BGMAppVolumes.m b/BGMApp/BGMApp/BGMAppVolumes.m index d10802f..7ae9018 100644 --- a/BGMApp/BGMApp/BGMAppVolumes.m +++ b/BGMApp/BGMApp/BGMAppVolumes.m @@ -124,6 +124,79 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps"; } } +- (NSMenuItem*) getMenuItemForApp:(NSRunningApplication*)app { + NSInteger lastAppVolumeMenuItemIndex = [self lastMenuItemIndex] - 2; + + for (NSInteger i = [self firstMenuItemIndex]; i <= lastAppVolumeMenuItemIndex; i++) { + NSMenuItem* item = [bgmMenu itemAtIndex:i]; + NSRunningApplication* itemApp = item.representedObject; + BGMAssert(itemApp, "!itemApp for %s", item.title.UTF8String); + + if ([itemApp isEqual:app]) { + return item; + } + } + for (NSInteger i = 0; i < [moreAppsMenu numberOfItems]; i++) { + NSMenuItem* item = [moreAppsMenu itemAtIndex:i]; + NSRunningApplication* itemApp = item.representedObject; + BGMAssert(itemApp, "!itemApp for %s", item.title.UTF8String); + + if ([itemApp isEqual:app]) { + return item; + } + } + + return nil; +} + +- (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app { + BGMAppVolumeAndPan result = { + .volume = -1, + .pan = -1 + }; + + NSMenuItem *item = [self getMenuItemForApp:app]; + + if (item == nil) { + return result; + } + + for (NSView* subview in item.view.subviews) { + // Get the volume. + if ([subview isKindOfClass:[BGMAVM_VolumeSlider class]]) { + result.volume = [(BGMAVM_VolumeSlider*)subview intValue]; + } + + // Get the pan position. + if ([subview isKindOfClass:[BGMAVM_PanSlider class]]) { + result.pan = [(BGMAVM_PanSlider*)subview intValue]; + } + } + + return result; +} + +- (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app { + NSMenuItem *item = [self getMenuItemForApp:app]; + + if (item == nil) { + return; + } + + for (NSView* subview in item.view.subviews) { + // Set the volume. + if (volumeAndPan.volume != -1 && [subview isKindOfClass:[BGMAVM_VolumeSlider class]]) { + [(BGMAVM_VolumeSlider*)subview setRelativeVolume:volumeAndPan.volume]; + } + + // Set the pan position. + if (volumeAndPan.pan != -1 && [subview isKindOfClass:[BGMAVM_PanSlider class]]) { + [(BGMAVM_PanSlider*)subview setPanPosition:volumeAndPan.pan]; + } + } + +} + // Create a blank menu item to copy as a template. - (NSMenuItem*) createBlankAppVolumeMenuItem { NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; @@ -447,4 +520,3 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps"; } @end - diff --git a/BGMApp/BGMApp/BGMAppVolumesController.h b/BGMApp/BGMApp/BGMAppVolumesController.h index 24b0337..2daeaaa 100644 --- a/BGMApp/BGMApp/BGMAppVolumesController.h +++ b/BGMApp/BGMApp/BGMAppVolumesController.h @@ -29,6 +29,11 @@ #pragma clang assume_nonnull begin +typedef struct BGMAppVolumeAndPan { + int volume; + int pan; +} BGMAppVolumeAndPan; + @interface BGMAppVolumesController : NSObject - (id) initWithMenu:(NSMenu*)menu @@ -45,6 +50,9 @@ forAppWithProcessID:(pid_t)processID forAppWithProcessID:(pid_t)processID bundleID:(NSString* __nullable)bundleID; +- (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication *)app; +- (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app; + @end #pragma clang assume_nonnull end diff --git a/BGMApp/BGMApp/BGMAppVolumesController.mm b/BGMApp/BGMApp/BGMAppVolumesController.mm index 5a1c05a..dc34a7b 100644 --- a/BGMApp/BGMApp/BGMAppVolumesController.mm +++ b/BGMApp/BGMApp/BGMAppVolumesController.mm @@ -40,11 +40,6 @@ #pragma clang assume_nonnull begin -typedef struct BGMAppVolumeAndPan { - int volume; - int pan; -} BGMAppVolumeAndPan; - @implementation BGMAppVolumesController { // The App Volumes UI. BGMAppVolumes* appVolumes; @@ -104,6 +99,20 @@ typedef struct BGMAppVolumeAndPan { } } +- (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication *)app { + return [appVolumes getVolumeAndPanForApp:app]; +} + +- (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app { + [appVolumes setVolumeAndPan:volumeAndPan forApp:app]; + if (volumeAndPan.volume != -1) { + [self setVolume:volumeAndPan.volume forAppWithProcessID:app.processIdentifier bundleID:app.bundleIdentifier]; + } + if (volumeAndPan.pan != -1) { + [self setPanPosition:volumeAndPan.pan forAppWithProcessID:app.processIdentifier bundleID:app.bundleIdentifier]; + } +} + - (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app fromVolumes:(const CACFArray&)volumes { BGMAppVolumeAndPan volumeAndPan = { diff --git a/BGMApp/BGMApp/Scripting/BGMASApplication.h b/BGMApp/BGMApp/Scripting/BGMASApplication.h new file mode 100644 index 0000000..d53fc6a --- /dev/null +++ b/BGMApp/BGMApp/Scripting/BGMASApplication.h @@ -0,0 +1,48 @@ +// 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 . + +// +// BGMASApplication.h +// BGMApp +// +// Copyright © 2021 Marcus Wu +// +// An AppleScript class for volume and pan settings for running applications. +// + + +// Local Includes +#import "BGMAppVolumesController.h" + +// System Includes +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface BGMASApplication : NSObject + +- (instancetype) initWithApplication:(NSRunningApplication*)app + volumeController:(BGMAppVolumesController*)volumeController + parentSpecifier:(NSScriptObjectSpecifier* __nullable)parentSpecifier + index:(int)i; + +@property (readonly) NSString* name; +@property int volume; +@property int pan; +@end + +NS_ASSUME_NONNULL_END diff --git a/BGMApp/BGMApp/Scripting/BGMASApplication.m b/BGMApp/BGMApp/Scripting/BGMASApplication.m new file mode 100644 index 0000000..5138507 --- /dev/null +++ b/BGMApp/BGMApp/Scripting/BGMASApplication.m @@ -0,0 +1,83 @@ +// 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 . + +// +// BGMASApplication.m +// BGMApp +// +// Copyright © 2021 Marcus Wu +// + +// Self Include +#import "BGMASApplication.h" + +@implementation BGMASApplication { + NSScriptObjectSpecifier* parentSpecifier; + NSRunningApplication *application; + BGMAppVolumesController* appVolumesController; + int index; +} + +- (instancetype) initWithApplication:(NSRunningApplication*)app + volumeController:(BGMAppVolumesController*)volumeController + parentSpecifier:(NSScriptObjectSpecifier* __nullable)parent + index:(int)i { + if ((self = [super init])) { + parentSpecifier = parent; + application = app; + appVolumesController = volumeController; + index = i; + } + + return self; +} + +- (NSString*) name { + return [NSString stringWithFormat:@"%@", [application localizedName]]; +} + +- (int) volume { + return [appVolumesController getVolumeAndPanForApp:application].volume; +} + +- (void) setVolume:(int)vol { + BGMAppVolumeAndPan volume = { + .volume = vol, + .pan = -1 + }; + [appVolumesController setVolumeAndPan:volume forApp:application]; +} + +- (int) pan { + return [appVolumesController getVolumeAndPanForApp:application].pan; +} + +- (void) setPan:(int)pan { + BGMAppVolumeAndPan thePan = { + .volume = -1, + .pan = pan + }; + [appVolumesController setVolumeAndPan:thePan forApp:application]; +} + +- (NSScriptObjectSpecifier* __nullable) objectSpecifier { + NSScriptClassDescription* parentClassDescription = [parentSpecifier keyClassDescription]; + return [[NSNameSpecifier alloc] initWithContainerClassDescription:parentClassDescription + containerSpecifier:parentSpecifier + key:@"applications" + name:self.name]; +} + +@end diff --git a/BGMApp/BGMApp/Scripting/BGMApp.sdef b/BGMApp/BGMApp/Scripting/BGMApp.sdef index 81ddb43..52e03a1 100644 --- a/BGMApp/BGMApp/Scripting/BGMApp.sdef +++ b/BGMApp/BGMApp/Scripting/BGMApp.sdef @@ -9,8 +9,7 @@ + plural="output devices"> @@ -19,14 +18,50 @@ code="pnam" description="The name of the output device." type="text" - access="r"/> + access="r"> + + - + + + + + + + + + + + + + + + + + + + + @@ -48,11 +83,23 @@ + + + + + + + + diff --git a/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.h b/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.h index b25fcf5..9a5d5ae 100644 --- a/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.h +++ b/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.h @@ -24,6 +24,7 @@ // Local Includes #import "BGMASOutputDevice.h" +#import "BGMASApplication.h" // System Includes #import @@ -37,6 +38,8 @@ @property BGMASOutputDevice* selectedOutputDevice; @property (readonly) NSArray* outputDevices; +@property double mainVolume; +@property (readonly) NSArray* applications; @end diff --git a/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.mm b/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.mm index 927dcd4..6d6ed0b 100644 --- a/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.mm +++ b/BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.mm @@ -30,6 +30,7 @@ #import "CAHALAudioSystemObject.h" #import "CAAutoDisposer.h" +const AudioObjectPropertyScope kScope = kAudioDevicePropertyScopeOutput; #pragma clang assume_nonnull begin @@ -43,7 +44,7 @@ [key UTF8String]); } - return [@[@"selectedOutputDevice", @"outputDevices"] containsObject:key]; + return [@[@"selectedOutputDevice", @"outputDevices", @"mainVolume", @"applications"] containsObject:key]; } - (BGMASOutputDevice*) selectedOutputDevice { @@ -83,6 +84,30 @@ return outputDevices; } +- (double) mainVolume { + BGMAudioDevice bgmDevice = [self.audioDevices bgmDevice]; + return bgmDevice.GetVolumeControlScalarValue(kScope, kMasterChannel); +} + +- (void) setMainVolume:(double)mainVolume { + BGMAudioDevice bgmDevice = [self.audioDevices bgmDevice]; + bgmDevice.SetMasterVolumeScalar(kScope, (Float32)mainVolume); + [self.outputVolumeSlider setFloatValue:(float)mainVolume]; +} + +- (NSArray*) applications { + NSArray* apps = [[NSWorkspace sharedWorkspace] runningApplications]; + NSMutableArray* applications = [NSMutableArray arrayWithCapacity:[apps count]]; + + for (UInt32 i = 0; i < [apps count]; i++) { + BGMASApplication *app = [[BGMASApplication alloc] initWithApplication:apps[i] volumeController:self.appVolumes parentSpecifier:[self objectSpecifier] index:i]; + + [applications addObject:app]; + } + + return applications; +} + @end #pragma clang assume_nonnull end