Add an option to use a volume icon instead of the Background Music logo.

This is so the icon can show the current volume. Then you can hide the
built-in volume status bar item in System Preferences.

Closes #183.
This commit is contained in:
Kyle Neideck 2019-03-04 23:52:10 +11:00
parent 14df80da24
commit e093e7d3b2
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
24 changed files with 776 additions and 144 deletions

2
.gitignore vendored
View file

@ -7,6 +7,8 @@ tags
cmake-build-debug/
/Background-Music-*/
BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
Images/*.aux
Images/*.log
# Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore

View file

@ -7,6 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusicPrefs.mm"; }; };
1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferencesMenu.mm"; }; };
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusic.mm"; }; };
@ -206,6 +212,10 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMVolumeChangeListener.cpp; sourceTree = "<group>"; };
19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMStatusBarItem.mm; sourceTree = "<group>"; };
19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMStatusBarItem.h; sourceTree = "<group>"; };
19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMVolumeChangeListener.h; sourceTree = "<group>"; };
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMAutoPauseMusicPrefs.h; path = Preferences/BGMAutoPauseMusicPrefs.h; sourceTree = "<group>"; };
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMAutoPauseMusicPrefs.mm; path = Preferences/BGMAutoPauseMusicPrefs.mm; sourceTree = "<group>"; };
1C0BD0A61BF1B029004F4CF5 /* BGMPreferencesMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMPreferencesMenu.h; path = Preferences/BGMPreferencesMenu.h; sourceTree = "<group>"; };
@ -579,10 +589,14 @@
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */,
1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */,
1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */,
19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */,
19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */,
1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */,
1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */,
2743C9ED1D8538700089613B /* BGMUserDefaults.h */,
2743C9F01D853FBB0089613B /* BGMUserDefaults.m */,
19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */,
19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */,
2795973C1C982E8C00A002FB /* BGMXPCListener.h */,
2795973A1C982E4E00A002FB /* BGMXPCListener.mm */,
1C2FC3161EC7078F00A76592 /* Scripting */,
@ -1004,6 +1018,8 @@
2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */,
27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */,
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */,
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */,
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1061,6 +1077,8 @@
1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */,
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */,
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */,
19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */,
19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1134,6 +1152,8 @@
1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */,
2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */,
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */,
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -17,10 +17,10 @@
// BGMAppDelegate.mm
// BGMApp
//
// Copyright © 2016-2018 Kyle Neideck
// Copyright © 2016-2019 Kyle Neideck
//
// Self Includes
// Self Include
#import "BGMAppDelegate.h"
// Local Includes
@ -33,6 +33,7 @@
#import "BGMOutputVolumeMenuItem.h"
#import "BGMPreferencesMenu.h"
#import "BGMPreferredOutputDevices.h"
#import "BGMStatusBarItem.h"
#import "BGMSystemSoundsVolume.h"
#import "BGMTermination.h"
#import "BGMUserDefaults.h"
@ -45,18 +46,19 @@
#pragma clang assume_nonnull begin
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
// for the app. These are called "menu bar extras" in the Human Interface Guidelines.
NSStatusItem* statusBarItem;
// The button in the system status bar that shows the main menu.
BGMStatusBarItem* statusBarItem;
// Only show the 'BGMXPCHelper is missing' error dialog once.
BOOL haveShownXPCHelperErrorMessage;
// Persistently stores user settings and data.
BGMUserDefaults* userDefaults;
BGMAutoPauseMusic* autoPauseMusic;
BGMAutoPauseMenuItem* autoPauseMenuItem;
BGMMusicPlayers* musicPlayers;
@ -71,6 +73,8 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
@synthesize audioDevices = audioDevices;
- (void) awakeFromNib {
[super 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:kOptShowDockIcon] != NSNotFound) {
@ -79,72 +83,19 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
haveShownXPCHelperErrorMessage = NO;
[self initStatusBarItem];
}
// Set up the status bar item. (The thing you click to show BGMApp's UI.)
- (void) initStatusBarItem {
statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
// 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 up audioDevices, which coordinates BGMDevice and the output device. It manages
// playthrough, volume/mute controls, etc.
if (![self initAudioDeviceManager]) {
return;
}
// Set the icon.
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
// Stored user settings
userDefaults = [self createUserDefaults];
if (icon != nil) {
NSRect statusBarItemFrame;
if (buttonAvailable) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItemFrame = statusBarItem.button.frame;
#pragma clang diagnostic pop
} else {
// OS X 10.9 fallback. I haven't tested this (or anything else on 10.9).
statusBarItemFrame = statusBarItem.view.frame;
}
CGFloat lengthMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding);
[icon setSize:NSMakeSize(lengthMinusPadding, lengthMinusPadding)];
// Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or the status
// bar's in dark mode
[icon setTemplate:YES];
if (buttonAvailable) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.image = icon;
#pragma clang diagnostic pop
} else {
statusBarItem.image = icon;
}
} else {
// If our icon is missing for some reason, fallback to a fermata character (1D110)
if (buttonAvailable) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.title = @"𝄐";
#pragma clang diagnostic pop
} else {
statusBarItem.title = @"𝄐";
}
}
// Set the main menu
statusBarItem.menu = self.bgmMenu;
// Add the status bar item. (The thing you click to show BGMApp's main menu.)
statusBarItem = [[BGMStatusBarItem alloc] initWithMenu:self.bgmMenu
audioDevices:audioDevices
userDefaults:userDefaults];
}
- (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
@ -159,15 +110,6 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]);
// Set up audioDevices, which coordinates BGMDevice and the output device. It manages
// playthrough, volume/mute controls, etc.
if (![self initAudioDeviceManager]) {
return;
}
// Persistently stores user settings and data.
BGMUserDefaults* userDefaults = [self createUserDefaults];
// Handles changing (or not changing) the output device when devices are added or removed. Must
// be initialised before calling setBGMDeviceAsDefault.
preferredOutputDevices =
@ -191,7 +133,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
musicPlayers:musicPlayers];
[self setUpMainMenu:userDefaults];
[self setUpMainMenu];
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
helperConnectionErrorHandler:^(NSError* error) {
@ -285,7 +227,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
}
}
- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults {
- (void) setUpMainMenu {
autoPauseMenuItem =
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
autoPauseMusic:autoPauseMusic
@ -305,6 +247,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
audioDevices:audioDevices
musicPlayers:musicPlayers
statusBarItem:statusBarItem
aboutPanel:self.aboutPanel
aboutPanelLicenseView:self.aboutPanelLicenseView];

View file

@ -202,7 +202,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 5;
NSMutableArray<NSMenuItem*>* items = [NSMutableArray new];
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
UInt32 channel = 0; // 0 is the master channel.
UInt32 channel = kAudioObjectPropertyElementMaster;
// If the device has data sources, create a menu item for each. Otherwise, create a single menu item
// for the device. This way the menu items' titles will be, for example, "Internal Speakers" rather

View file

@ -17,7 +17,7 @@
// BGMOutputVolumeMenuItem.mm
// BGMApp
//
// Copyright © 2017, 2018 Kyle Neideck
// Copyright © 2017-2019 Kyle Neideck
//
// Self Include
@ -26,6 +26,7 @@
// Local Includes
#import "BGM_Utils.h"
#import "BGMAudioDevice.h"
#import "BGMVolumeChangeListener.h"
// PublicUtility Includes
#import "CAException.h"
@ -46,7 +47,7 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
NSTextField* deviceLabel;
NSSlider* volumeSlider;
BGMAudioDevice outputDevice;
AudioObjectPropertyListenerBlock updateSliderListenerBlock;
BGMVolumeChangeListener* volumeChangeListener;
AudioObjectPropertyListenerBlock updateLabelListenerBlock;
}
@ -66,9 +67,8 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
volumeSlider = slider;
outputDevice = audioDevices.outputDevice;
// These are initialised in the methods called below.
updateSliderListenerBlock = nil;
updateLabelListenerBlock = nil;
// volumeChangeListener and updateLabelListenerBlock are initialised in the methods called
// below.
// Apply our custom view from MainMenu.xib.
self.view = view;
@ -89,20 +89,6 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
// TODO: This call isn't thread safe. (But currently this dealloc method is only called if
// there's an error.)
[self removeOutputDeviceDataSourceListener];
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
audioDevices.bgmDevice.RemovePropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
}
- (void) initSlider {
@ -118,32 +104,9 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
// Register a listener that will update the slider when the user changes the volume or
// mutes/unmutes their audio.
BGMOutputVolumeMenuItem* __weak weakSelf = self;
updateSliderListenerBlock =
^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
// at least one property the block is listening to, so there's no need to check it.
#pragma unused (inNumberAddresses, inAddresses)
[weakSelf updateVolumeSlider];
};
// Instead of swallowing exceptions, we could try again later, but I doubt it would be worth the
// effort. And the documentation doesn't actually explain what could cause this to fail.
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] {
// Register the listener to receive volume notifications.
audioDevices.bgmDevice.AddPropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] {
// Register the same listener for mute/unmute notifications.
audioDevices.bgmDevice.AddPropertyListenerBlock(
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
dispatch_get_main_queue(),
updateSliderListenerBlock);
}));
volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [&] {
[weakSelf updateVolumeSlider];
});
}
// Updates the value of the output volume slider. Should only be called on the main thread because
@ -178,7 +141,7 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
volumeSlider.doubleValue = 0.0;
}
}));
};
}
- (void) addOutputDeviceDataSourceListener {
// Create the block that updates deviceLabel when the output device's data source changes, e.g.

View file

@ -0,0 +1,63 @@
// 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 <http://www.gnu.org/licenses/>.
//
// BGMStatusBarItem.h
// BGMApp
//
// Copyright © 2019 Kyle Neideck
//
// The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main
// menu for the app. These are called "menu bar extras" in the Human Interface Guidelines.
//
// Local Includes
#import "BGMAudioDeviceManager.h"
// System Includes
#import <Cocoa/Cocoa.h>
// Forward Declarations
@class BGMUserDefaults;
#pragma clang assume_nonnull begin
typedef NS_ENUM(NSInteger, BGMStatusBarIcon) {
BGMFermataStatusBarIcon = 0,
BGMVolumeStatusBarIcon
};
static BGMStatusBarIcon const kBGMStatusBarIconMinValue = BGMFermataStatusBarIcon;
static BGMStatusBarIcon const kBGMStatusBarIconMaxValue = BGMVolumeStatusBarIcon;
static BGMStatusBarIcon const kBGMStatusBarIconDefaultValue = BGMFermataStatusBarIcon;
@interface BGMStatusBarItem : NSObject
- (instancetype) initWithMenu:(NSMenu*)bgmMenu
audioDevices:(BGMAudioDeviceManager*)devices
userDefaults:(BGMUserDefaults*)defaults;
// Set this to BGMFermataStatusBarIcon to change the icon to the Background Music logo.
//
// Set this to BGMFermataStatusBarIcon to change the icon to a volume icon. This icon has the
// advantage of indicating the volume level, but we can't make it the default because it looks the
// same as the icon for the macOS volume status bar item.
@property BGMStatusBarIcon icon;
@end
#pragma clang assume_nonnull end

View file

@ -0,0 +1,257 @@
// 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 <http://www.gnu.org/licenses/>.
//
// BGMStatusBarItem.m
// BGMApp
//
// Copyright © 2019 Kyle Neideck
//
// Self Include
#import "BGMStatusBarItem.h"
// Local Includes
#import "BGM_Utils.h"
#import "BGMUserDefaults.h"
#import "BGMVolumeChangeListener.h"
#pragma clang assume_nonnull begin
static CGFloat const kStatusBarIconPadding = 0.25;
static CGFloat const kVolumeIconAdditionalVerticalPadding = 0.075;
@implementation BGMStatusBarItem
{
BGMAudioDeviceManager* audioDevices;
// User settings and data.
BGMUserDefaults* userDefaults;
NSImage* fermataIcon;
NSImage* volumeIcon0SoundWaves;
NSImage* volumeIcon1SoundWave;
NSImage* volumeIcon2SoundWaves;
NSImage* volumeIcon3SoundWaves;
NSStatusItem* statusBarItem;
BGMVolumeChangeListener* volumeChangeListener;
BGMStatusBarIcon _icon;
}
- (instancetype) initWithMenu:(NSMenu*)bgmMenu
audioDevices:(BGMAudioDeviceManager*)devices
userDefaults:(BGMUserDefaults*)defaults {
if ((self = [super init])) {
statusBarItem =
[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
audioDevices = devices;
userDefaults = defaults;
// Initialise the icons.
[self initIcons];
// Set the initial icon.
self.icon = userDefaults.statusBarIcon;
// Set the menu item to open the main menu.
statusBarItem.menu = bgmMenu;
// Set the accessibility label to "Background Music". (We intentionally don't set a title or
// a tooltip.)
if ([BGMStatusBarItem buttonAvailable]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.accessibilityLabel =
[NSRunningApplication currentApplication].localizedName;
#pragma clang diagnostic pop
}
// Update the icon when BGMDevice's volume changes.
BGMStatusBarItem* __weak weakSelf = self;
volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [&] {
[weakSelf bgmDeviceVolumeDidChange];
});
}
return self;
}
- (void) dealloc {
delete volumeChangeListener;
}
- (void) initIcons {
// Load the icons.
fermataIcon = [NSImage imageNamed:@"FermataIcon"];
volumeIcon0SoundWaves = [NSImage imageNamed:@"Volume0"];
volumeIcon1SoundWave = [NSImage imageNamed:@"Volume1"];
volumeIcon2SoundWaves = [NSImage imageNamed:@"Volume2"];
volumeIcon3SoundWaves = [NSImage imageNamed:@"Volume3"];
// Set the icons' sizes.
NSRect statusBarItemFrame;
if ([BGMStatusBarItem buttonAvailable]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItemFrame = statusBarItem.button.frame;
#pragma clang diagnostic pop
} else {
// OS X 10.9 fallback. I haven't tested this (or anything else on 10.9).
statusBarItemFrame = statusBarItem.view.frame;
}
CGFloat heightMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding);
// The fermata icon has equal width and height.
[fermataIcon setSize:NSMakeSize(heightMinusPadding, heightMinusPadding)];
// The volume icons are all the same width and height.
CGFloat volumeIconWidthToHeightRatio =
volumeIcon0SoundWaves.size.width / volumeIcon0SoundWaves.size.height;
CGFloat volumeIconWidth = heightMinusPadding * volumeIconWidthToHeightRatio;
CGFloat volumeIconHeight = heightMinusPadding * (1 - kVolumeIconAdditionalVerticalPadding);
[volumeIcon0SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
[volumeIcon1SoundWave setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
[volumeIcon2SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
[volumeIcon3SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
// Make the icons "template images" so they get drawn colour-inverted when they're highlighted
// or the system is in dark mode.
[fermataIcon setTemplate:YES];
[volumeIcon0SoundWaves setTemplate:YES];
[volumeIcon1SoundWave setTemplate:YES];
[volumeIcon2SoundWaves setTemplate:YES];
[volumeIcon3SoundWaves setTemplate:YES];
}
+ (BOOL) buttonAvailable {
// NSStatusItem doesn't have the "button" property on OS X 10.9.
return (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10);
}
- (void) setImage:(NSImage*)image {
if ([BGMStatusBarItem buttonAvailable]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.image = image;
#pragma clang diagnostic pop
} else {
statusBarItem.image = image;
}
}
- (BGMStatusBarIcon) icon {
return _icon;
}
- (void) setIcon:(BGMStatusBarIcon)icon {
_icon = icon;
// Save the setting.
userDefaults.statusBarIcon = self.icon;
// Change the icon (i.e. the image). Dispatch this to the main thread because it changes the UI.
dispatch_async(dispatch_get_main_queue(), ^{
if (_icon == BGMFermataStatusBarIcon) {
[self setImage:fermataIcon];
// If the icon was greyed out, change it back.
if ([BGMStatusBarItem buttonAvailable]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.appearsDisabled = NO;
#pragma clang diagnostic pop
}
} else {
BGMAssert((_icon == BGMVolumeStatusBarIcon), "Unknown icon in enum");
[self updateVolumeStatusBarIcon];
}
});
}
- (void) bgmDeviceVolumeDidChange {
if (self.icon == BGMVolumeStatusBarIcon) {
[self updateVolumeStatusBarIcon];
}
}
// Should only be called on the main thread because it calls UI functions.
- (void) updateVolumeStatusBarIcon {
BGMAssert([[NSThread currentThread] isMainThread],
"updateVolumeStatusBarIcon called on non-main thread.");
BGMAssert((self.icon == BGMVolumeStatusBarIcon), "Volume status bar icon not enabled");
BGMAudioDevice bgmDevice = [audioDevices bgmDevice];
// BGMDevice should never return an error for these calls, so we just swallow any exceptions and
// give up.
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
AudioObjectPropertyScope element = kAudioObjectPropertyElementMaster;
BOOL hasVolume = bgmDevice.HasVolumeControl(scope, element);
// Show the button as greyed out if BGMDevice doesn't have a volume control (which means the
// output device doesn't have one).
if ([BGMStatusBarItem buttonAvailable]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
statusBarItem.button.appearsDisabled = !hasVolume;
#pragma clang diagnostic pop
}
if (hasVolume) {
if (bgmDevice.HasMuteControl(scope, element) &&
bgmDevice.GetMuteControlValue(scope, element)) {
// The device is muted, so use the zero waves icon.
[self setImage:volumeIcon0SoundWaves];
} else {
// Set the icon to reflect the device's volume.
double volume = bgmDevice.GetVolumeControlScalarValue(scope, element);
// These values match the macOS volume status bar item, except for the first one. I
// don't know why, but at a very low volume macOS will show the zero waves icon even
// though the sound is still audible.
if (volume == 0.05) {
[self setImage:volumeIcon0SoundWaves];
} else if (volume < 0.33) {
[self setImage:volumeIcon1SoundWave];
} else if (volume < 0.66) {
[self setImage:volumeIcon2SoundWaves];
} else {
[self setImage:volumeIcon3SoundWaves];
}
}
} else {
// Always use the full-volume icon when the device has no volume control.
[self setImage:volumeIcon3SoundWaves];
}
});
DebugMsg("BGMStatusBarItem::updateVolumeStatusBarIcon: Set icon to %s",
statusBarItem.image.name.UTF8String);
}
@end
#pragma clang assume_nonnull end

View file

@ -17,13 +17,16 @@
// BGMUserDefaults.h
// BGMApp
//
// Copyright © 2016-2018 Kyle Neideck
// Copyright © 2016-2019 Kyle Neideck
//
// A simple wrapper around our use of NSUserDefaults. Used to store the preferences/state that only
// apply to BGMApp. The others are stored by BGMDriver.
//
// System includes
// Local Includes
#import "BGMStatusBarItem.h"
// System Includes
#import <Cocoa/Cocoa.h>
@ -44,6 +47,10 @@
// device is at index 0. See BGMPreferredOutputDevices.
@property NSArray<NSString*>* preferredDeviceUIDs;
// The (type of) icon to show in the button in the status bar. (The button the user clicks to open
// BGMApp's main menu.)
@property BGMStatusBarIcon statusBarIcon;
@end
#pragma clang assume_nonnull end

View file

@ -17,7 +17,7 @@
// BGMUserDefaults.m
// BGMApp
//
// Copyright © 2016-2018 Kyle Neideck
// Copyright © 2016-2019 Kyle Neideck
//
// Self Include
@ -32,7 +32,8 @@
// Keys
static NSString* const BGMDefaults_AutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayerID";
static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon";
@implementation BGMUserDefaults {
// The defaults object wrapped by this object.
@ -89,6 +90,22 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
[self set:BGMDefaults_PreferredDeviceUIDs to:devices];
}
- (BGMStatusBarIcon) statusBarIcon {
NSInteger icon = [self getInt:BGMDefaults_StatusBarIcon or:kBGMStatusBarIconDefaultValue];
// Just in case we get an invalid value somehow.
if ((icon < kBGMStatusBarIconMinValue) || (icon > kBGMStatusBarIconMaxValue)) {
NSLog(@"BGMUserDefaults::statusBarIcon: Unknown BGMStatusBarIcon: %ld", (long)icon);
icon = kBGMStatusBarIconDefaultValue;
}
return (BGMStatusBarIcon)icon;
}
- (void) setStatusBarIcon:(BGMStatusBarIcon)icon {
[self setInt:BGMDefaults_StatusBarIcon to:icon];
}
#pragma mark Implementation
- (id __nullable) get:(NSString*)key {
@ -103,6 +120,7 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
}
}
// TODO: This method should have a default value param.
- (BOOL) getBool:(NSString*)key {
return defaults ? [defaults boolForKey:key] : [transientDefaults[key] boolValue];
}
@ -111,7 +129,32 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs";
if (defaults) {
[defaults setBool:value forKey:key];
} else {
transientDefaults[key] = [NSNumber numberWithBool:value];
transientDefaults[key] = @(value);
}
}
- (NSInteger) getInt:(NSString*)key or:(NSInteger)valueIfNil
{
if (defaults) {
if ([defaults objectForKey:key]) {
return [defaults integerForKey:key];
} else {
return valueIfNil;
}
} else {
if (transientDefaults[key]) {
return [transientDefaults[key] intValue];
} else {
return valueIfNil;
}
}
}
- (void) setInt:(NSString*)key to:(NSInteger)value {
if (defaults) {
[defaults setInteger:value forKey:key];
} else {
transientDefaults[key] = @(value);
}
}

View file

@ -0,0 +1,100 @@
// 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 <http://www.gnu.org/licenses/>.
//
// BGMVolumeChangeListener.cpp
// BGMApp
//
// Copyright © 2019 Kyle Neideck
//
// Self Include
#include "BGMVolumeChangeListener.h"
// Local Includes
#import "BGM_Utils.h"
#import "BGMAudioDevice.h"
// PublicUtility Includes
#import "CAException.h"
#import "CAPropertyAddress.h"
#pragma clang assume_nonnull begin
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wexit-time-destructors"
const static std::vector<CAPropertyAddress> kVolumeChangeProperties = {
// Output volume changes
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput),
// Mute/unmute
CAPropertyAddress(kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput),
// Received when controls are added to or removed from the device.
CAPropertyAddress(kAudioObjectPropertyControlList),
// Received when the device has changed and "clients should re-evaluate everything they need
// to know about the device, particularly the layout and values of the controls".
CAPropertyAddress(kAudioDevicePropertyDeviceHasChanged)
};
#pragma clang diagnostic pop
BGMVolumeChangeListener::BGMVolumeChangeListener(BGMAudioDevice device,
std::function<void(void)> handler)
:
mDevice(device)
{
// Register a listener that will update the slider when the user changes the volume or
// mutes/unmutes their audio.
mListenerBlock =
Block_copy(^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
// at least one property the block is listening to, so there's no need to check it.
(void)inNumberAddresses;
(void)inAddresses;
// Call the callback.
handler();
});
// Register for a number of properties that might indicate that clients need to update. For
// example, the mute property changing means UI elements that display the volume will need to be
// updated, even though it's not strictly a change in volume.
for(CAPropertyAddress property : kVolumeChangeProperties)
{
// Instead of swallowing exceptions here, we could try again later, but I doubt it would be
// worth the effort. And the documentation doesn't actually explain what could cause this
// call to fail.
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
mDevice.AddPropertyListenerBlock(property, dispatch_get_main_queue(), mListenerBlock);
});
}
}
BGMVolumeChangeListener::~BGMVolumeChangeListener()
{
// Deregister and release the listener block.
for(CAPropertyAddress property : kVolumeChangeProperties)
{
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
mDevice.RemovePropertyListenerBlock(property,
dispatch_get_main_queue(),
mListenerBlock);
});
}
Block_release(mListenerBlock);
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,59 @@
// 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 <http://www.gnu.org/licenses/>.
//
// BGMVolumeChangeListener.h
// BGMApp
//
// Copyright © 2019 Kyle Neideck
//
// Local Includes
#include "BGMBackgroundMusicDevice.h"
// PublicUtility Includes
#import "CAPropertyAddress.h"
// STL Includes
#include <functional>
// System Includes
#include <CoreAudio/CoreAudio.h>
#pragma clang assume_nonnull begin
class BGMVolumeChangeListener
{
public:
/*!
* @param device Listens for notifications about this device.
* @param handler The function to call when the device's volume (or mute) changes. Called on the
* main queue.
*/
BGMVolumeChangeListener(BGMAudioDevice device, std::function<void(void)> handler);
virtual ~BGMVolumeChangeListener();
BGMVolumeChangeListener(const BGMVolumeChangeListener&) = delete;
BGMVolumeChangeListener& operator=(const BGMVolumeChangeListener&) = delete;
private:
AudioObjectPropertyListenerBlock mListenerBlock;
BGMAudioDevice mDevice;
};
#pragma clang assume_nonnull end

View file

@ -52,7 +52,17 @@
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="nb1-jq-97L"/>
<menuItem title="About Background Music" tag="3" id="R45-Vo-Eto">
<menuItem title="Status Bar Icon" enabled="NO" id="CmD-ot-1wE">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Background Music Logo" state="on" tag="2" id="9VF-qy-6fh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Volume Icon" tag="3" toolTip="todo" id="B47-O2-wd0">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="pYP-Fy-nKA"/>
<menuItem title="About Background Music" tag="4" id="R45-Vo-Eto">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
@ -205,7 +215,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="Cdb-RA-YK0">
<rect key="frame" x="1" y="1" width="565" height="243"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" id="LSG-PF-cl8">
<rect key="frame" x="-6" y="0.0" width="577" height="243"/>
@ -316,7 +326,7 @@
</objects>
<resources>
<image name="FermataIcon" width="284" height="284"/>
<image name="NSComputer" width="128" height="128"/>
<image name="NSComputer" width="32" height="32"/>
<image name="buttonCell:IXo-C7-3uE:image" width="1" height="1">
<mutableData key="keyedArchiveRepresentation">
YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "mac",
"filename" : "Volume0.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "mac",
"filename" : "Volume1.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "mac",
"filename" : "Volume2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "mac",
"filename" : "Volume3.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

View file

@ -17,7 +17,7 @@
// BGMPreferencesMenu.h
// BGMApp
//
// Copyright © 2016, 2018 Kyle Neideck
// Copyright © 2016, 2018, 2019 Kyle Neideck
//
// Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather
// than to other BGMApp classes.
@ -26,6 +26,7 @@
// Local Includes
#import "BGMAudioDeviceManager.h"
#import "BGMMusicPlayers.h"
#import "BGMStatusBarItem.h"
// System Includes
#import <Cocoa/Cocoa.h>
@ -38,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;

View file

@ -17,7 +17,7 @@
// BGMPreferencesMenu.mm
// BGMApp
//
// Copyright © 2016, 2018 Kyle Neideck
// Copyright © 2016, 2018, 2019 Kyle Neideck
//
// Self Include
@ -32,11 +32,18 @@ NS_ASSUME_NONNULL_BEGIN
// Interface Builder tags
static NSInteger const kPreferencesMenuItemTag = 1;
static NSInteger const kAboutPanelMenuItemTag = 3;
static NSInteger const kBGMIconMenuItemTag = 2;
static NSInteger const kVolumeIconMenuItemTag = 3;
static NSInteger const kAboutPanelMenuItemTag = 4;
@implementation BGMPreferencesMenu {
// Menu sections
// Menu sections/items
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
NSMenuItem* bgmIconMenuItem;
NSMenuItem* volumeIconMenuItem;
// The menu item you press to open BGMApp's main menu.
BGMStatusBarItem* statusBarItem;
// The About Background Music window
BGMAboutPanel* aboutPanel;
@ -45,6 +52,7 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
aboutPanel:(NSPanel*)inAboutPanel
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
if ((self = [super init])) {
@ -56,6 +64,21 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
statusBarItem = inStatusBarItem;
// Set up the menu items under the "Status Bar Icon" heading.
bgmIconMenuItem = [prefsMenu itemWithTag:kBGMIconMenuItemTag];
bgmIconMenuItem.state =
(statusBarItem.icon == BGMFermataStatusBarIcon) ? NSOnState : NSOffState;
[bgmIconMenuItem setTarget:self];
[bgmIconMenuItem setAction:@selector(useBGMStatusBarIcon)];
volumeIconMenuItem = [prefsMenu itemWithTag:kVolumeIconMenuItemTag];
volumeIconMenuItem.state =
(statusBarItem.icon == BGMVolumeStatusBarIcon) ? NSOnState : NSOffState;
[volumeIconMenuItem setTarget:self];
[volumeIconMenuItem setAction:@selector(useVolumeStatusBarIcon)];
// Set up the "About Background Music" menu item
NSMenuItem* aboutMenuItem = [prefsMenu itemWithTag:kAboutPanelMenuItemTag];
[aboutMenuItem setTarget:aboutPanel];
@ -65,6 +88,29 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
return self;
}
- (void) useBGMStatusBarIcon {
// Change the icon.
statusBarItem.icon = BGMFermataStatusBarIcon;
// Select/deselect the menu items.
bgmIconMenuItem.state = NSOnState;
volumeIconMenuItem.state = NSOffState;
}
- (void) useVolumeStatusBarIcon {
// TODO: Maybe we should show a message that tells the user how to hide the built-in volume
// icon. They probably won't want two status bar items that look the same. Or we might be
// able to automatically hide the built-in icon while BGMApp is running and show it again
// when BGMApp is closed.
// Change the icon.
statusBarItem.icon = BGMVolumeStatusBarIcon;
// Select/deselect the menu items.
bgmIconMenuItem.state = NSOffState;
volumeIconMenuItem.state = NSOnState;
}
@end
NS_ASSUME_NONNULL_END

69
Images/VolumeIcons.tex Normal file
View file

@ -0,0 +1,69 @@
% 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 <http://www.gnu.org/licenses/>.
%
% VolumeIcons.tex
%
% Build with XeTeX:
% xelatex -jobname=Volume0 '\def\UseOption{}\input{VolumeIcons.tex}'
% xelatex -jobname=Volume1 '\def\UseOption{w1}\input{VolumeIcons.tex}'
% xelatex -jobname=Volume2 '\def\UseOption{w1,w2}\input{VolumeIcons.tex}'
% xelatex -jobname=Volume3 '\def\UseOption{w1,w2,w3}\input{VolumeIcons.tex}'
% for n in 0 1 2 3; do mv Volume$n.pdf ../BGMApp/BGMApp/Images.xcassets/Volume$n.imageset/; done
%
% Might build correctly with regular LaTeX. I haven't tried it.
%
\documentclass[tikz]{standalone}
\usepackage{tikz}
% "dummyOption" prevents "Package optional Warning: No options were selected,
% so all optional text will be printed" when building Volume0.pdf.
\usepackage[dummyOption]{optional}
\begin{document}
\begin{tikzpicture}
% Speaker (Rounded box and triangle)
\fill[rounded corners=5mm]
(0mm, 62.5mm) rectangle (25mm, 37.5mm) {};
\draw[rounded corners=2.5mm,fill=black]
(3mm, 50mm)--(34mm, 76.5mm)--(34mm, 23.5mm)--cycle;
% First sound wave (Curved line)
\opt{w1}{
\draw[line width=4.3mm,line cap=round]
(44mm, 36.5mm) to[out=46,in=-46] (44mm, 63.5mm);
}
% Second sound wave (Curved line)
\opt{w2}{
\draw[line width=4.3mm,line cap=round]
(57.5mm, 27.5mm) to[out=46,in=-46] (57.5mm, 72.5mm);
}
% Third sound wave (Curved line)
\opt{w3}{
\draw[line width=4.3mm,line cap=round]
(72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm);
}
% Always draw a transparent copy of the third wave so the images will all have
% the same width.
\draw[line width=4.3mm,line cap=round,opacity=0]
(72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm);
\end{tikzpicture}
\end{document}

View file

@ -17,7 +17,7 @@
// BGM_Types.h
// SharedSource
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2016, 2017, 2019 Kyle Neideck
//
#ifndef SharedSource__BGM_Types
@ -77,7 +77,7 @@ enum
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
// master element."
static const AudioObjectPropertyElement kMasterChannel = 0;
static const AudioObjectPropertyElement kMasterChannel = kAudioObjectPropertyElementMaster;
#pragma BGM Plug-in Custom Properties