Merge pull request #98 from rakslice/pan

This commit is contained in:
Kyle Neideck 2017-02-14 23:32:21 +11:00
commit 8257f49b46
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
11 changed files with 563 additions and 104 deletions

View file

@ -35,23 +35,32 @@
// Protocol for the UI custom classes
@protocol BGMAppVolumeSubview <NSObject>
@protocol BGMAppVolumeMenuItemSubview <NSObject>
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx;
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)item;
@end
// Custom classes for the UI elements in the app volume menu items
@interface BGMAVM_AppIcon : NSImageView <BGMAppVolumeSubview>
@interface BGMAVM_AppIcon : NSImageView <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_AppNameLabel : NSTextField <BGMAppVolumeSubview>
@interface BGMAVM_AppNameLabel : NSTextField <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_VolumeSlider : NSSlider <BGMAppVolumeSubview>
@interface BGMAVM_ShowMoreControlsButton : NSButton <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_VolumeSlider : NSSlider <BGMAppVolumeMenuItemSubview>
- (void) setRelativeVolume:(NSNumber*)relativeVolume;
@end
@interface BGMAVM_PanSlider : NSSlider <BGMAppVolumeMenuItemSubview>
- (void) setPanPosition:(NSNumber*)panPosition;
@end

View file

@ -17,7 +17,8 @@
// BGMAppVolumes.m
// BGMApp
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2017 Andrew Tonner
//
// Self Include
@ -25,6 +26,7 @@
// BGM Includes
#include "BGM_Types.h"
#include "BGM_Utils.h"
// PublicUtility Includes
#include "CACFDictionary.h"
@ -32,14 +34,20 @@
#include "CACFString.h"
static NSInteger const kAppVolumesMenuItemTag = 3;
// Tags for UI elements in MainMenu.xib
static NSInteger const kAppVolumesHeadingMenuItemTag = 3;
static NSInteger const kSeparatorBelowAppVolumesMenuItemTag = 4;
static float const kSlidersSnapWithin = 5;
static CGFloat const kAppVolumeViewInitialHeight = 20;
@implementation BGMAppVolumes {
NSMenu* bgmMenu;
NSView* appVolumeView;
CGFloat appVolumeViewFullHeight;
BGMAudioDeviceManager* audioDevices;
}
@ -47,6 +55,7 @@ static float const kSlidersSnapWithin = 5;
if ((self = [super init])) {
bgmMenu = menu;
appVolumeView = view;
appVolumeViewFullHeight = appVolumeView.frame.size.height;
audioDevices = devices;
// Create the menu items for controlling app volumes
@ -66,23 +75,26 @@ static float const kSlidersSnapWithin = 5;
[[NSWorkspace sharedWorkspace] removeObserver:self forKeyPath:@"runningApplications" context:nil];
}
#pragma mark UI Modifications
- (void) insertMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
NSAssert([NSThread isMainThread], @"insertMenuItemsForApps is not thread safe");
#ifndef NS_BLOCK_ASSERTIONS // If assertions are enabled
NSInteger numMenuItemsBeforeInsert =
[bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag] - [bgmMenu indexOfItemWithTag:kAppVolumesMenuItemTag] - 1;
auto numMenuItems = [&self]() {
NSInteger headingIdx = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag];
NSInteger separatorIdx = [bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag];
return separatorIdx - headingIdx - 1;
};
NSInteger numMenuItemsBeforeInsert = numMenuItems();
NSUInteger numApps = 0;
#endif
// Create a blank menu item to copy as a template
NSMenuItem* blankItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
blankItem.view = appVolumeView;
// Get the app volumes currently set on the device
CACFArray appVolumesOnDevice((CFArrayRef)[audioDevices bgmDevice].GetPropertyData_CFType(kBGMAppVolumesAddress), false);
NSInteger index = [bgmMenu indexOfItemWithTag:kAppVolumesMenuItemTag] + 1;
NSInteger index = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag] + 1;
// Add a volume-control menu item for each app
for (NSRunningApplication* app in apps) {
@ -95,12 +107,12 @@ static float const kSlidersSnapWithin = 5;
numApps++;
#endif
NSMenuItem* appVolItem = [blankItem copy];
NSMenuItem* appVolItem = [self createBlankAppVolumeMenuItem];
// Look through the menu item's subviews for the ones we want to set up
for (NSView* subview in appVolItem.view.subviews) {
if ([subview conformsToProtocol:@protocol(BGMAppVolumeSubview)]) {
[subview performSelector:@selector(setUpWithApp:context:) withObject:app withObject:self];
if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) {
[(NSView<BGMAppVolumeMenuItemSubview>*)subview setUpWithApp:app context:self menuItem:appVolItem];
}
}
@ -113,21 +125,27 @@ static float const kSlidersSnapWithin = 5;
[bgmMenu insertItem:appVolItem atIndex:index];
}
#ifndef NS_BLOCK_ASSERTIONS // If assertions are enabled
NSInteger numMenuItemsAfterInsert =
[bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag] - [bgmMenu indexOfItemWithTag:kAppVolumesMenuItemTag] - 1;
NSAssert3(numMenuItemsAfterInsert == (numMenuItemsBeforeInsert + numApps),
@"Did not add the expected number of menu items. numMenuItemsBeforeInsert=%ld numMenuItemsAfterInsert=%ld numAppsToAdd=%lu",
NSAssert3(numMenuItems() == (numMenuItemsBeforeInsert + numApps),
@"Added more/fewer menu items than there were apps. Items before: %ld, items after: %ld, apps: %lu",
(long)numMenuItemsBeforeInsert,
(long)numMenuItemsAfterInsert,
(long)numMenuItems(),
(unsigned long)numApps);
#endif
}
// Create a blank menu item to copy as a template.
- (NSMenuItem*) createBlankAppVolumeMenuItem {
NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
menuItem.view = appVolumeView;
menuItem = [menuItem copy]; // So we can modify a copy of the view, rather than the template itself.
return menuItem;
}
- (void) removeMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
NSAssert([NSThread isMainThread], @"removeMenuItemsForApps is not thread safe");
NSInteger firstItemIndex = [bgmMenu indexOfItemWithTag:kAppVolumesMenuItemTag] + 1;
NSInteger firstItemIndex = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag] + 1;
NSInteger lastItemIndex = [bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag] - 1;
// Check each app volume menu item, removing the items that control one of the given apps
@ -169,16 +187,71 @@ static float const kSlidersSnapWithin = 5;
CFTypeRef relativeVolume;
appVolume.GetCFType(CFSTR(kBGMAppVolumesKey_RelativeVolume), relativeVolume);
CFTypeRef panPosition;
appVolume.GetCFType(CFSTR(kBGMAppVolumesKey_PanPosition), panPosition);
// Update the slider
for (NSView* subview in menuItem.view.subviews) {
if ([subview respondsToSelector:@selector(setRelativeVolume:)]) {
[subview performSelector:@selector(setRelativeVolume:) withObject:(__bridge NSNumber*)relativeVolume];
}
if ([subview respondsToSelector:@selector(setPanPosition:)]) {
[subview performSelector:@selector(setPanPosition:) withObject:(__bridge NSNumber*)panPosition];
}
}
}
}
}
- (void) showHideExtraControls:(BGMAVM_ShowMoreControlsButton*)button {
// Show or hide an app's extra controls, currently only pan, in its App Volumes menu item.
NSMenuItem* menuItem = button.cell.representedObject;
BGMAssert(button, "!button");
BGMAssert(menuItem, "!menuItem");
CGFloat width = menuItem.view.frame.size.width;
CGFloat height = menuItem.view.frame.size.height;
#if DEBUG
const char* appName = [((NSRunningApplication*)menuItem.representedObject).localizedName UTF8String];
#endif
auto nearEnough = [](CGFloat x, CGFloat y) { // Shouldn't be necessary, but just in case.
return fabs(x - y) < 0.01; // We don't need much precision.
};
if (nearEnough(button.frameCenterRotation, 0.0)) {
// Hide extra controls
DebugMsg("BGMAppVolumes::showHideExtraControls: Hiding extra controls (%s)", appName);
BGMAssert(nearEnough(height, appVolumeViewFullHeight), "Extra controls were already hidden");
// Make the menu item shorter to hide the extra controls. Keep the width unchanged.
menuItem.view.frameSize = { width, kAppVolumeViewInitialHeight };
// Turn the button upside down so the arrowhead points down.
button.frameCenterRotation = 180.0;
// Move the button up slightly so it aligns with the volume slider.
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y - 1)];
} else {
// Show extra controls
DebugMsg("BGMAppVolumes::showHideExtraControls: Showing extra controls (%s)", appName);
BGMAssert(nearEnough(button.frameCenterRotation, 180.0), "Unexpected button rotation");
BGMAssert(nearEnough(height, kAppVolumeViewInitialHeight), "Extra controls were already shown");
// Make the menu item taller to show the extra controls. Keep the width unchanged.
menuItem.view.frameSize = { width, appVolumeViewFullHeight };
// Turn the button rightside up so the arrowhead points up.
button.frameCenterRotation = 0.0;
// Move the button down slightly, back to it's original position.
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y + 1)];
}
}
#pragma mark KVO
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
#pragma unused (object, context)
@ -211,6 +284,8 @@ static float const kSlidersSnapWithin = 5;
}
}
#pragma mark BGMDevice Communication
- (void) sendVolumeChangeToBGMDevice:(SInt32)newVolume appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
CACFDictionary appVolumeChange(true);
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
@ -224,14 +299,30 @@ static float const kSlidersSnapWithin = 5;
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
}
- (void) sendPanPositionChangeToBGMDevice:(SInt32)newPanPosition appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
CACFDictionary appVolumeChange(true);
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), (__bridge CFStringRef)appBundleID);
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_PanPosition), newPanPosition);
CACFArray appVolumeChanges(true);
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
}
@end
#pragma mark Custom Classes (IB)
// Custom classes for the UI elements in the app volume menu items
@implementation BGMAVM_AppIcon
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx {
#pragma unused (ctx)
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
#pragma unused (ctx, menuItem)
self.image = app.icon;
}
@ -240,8 +331,8 @@ static float const kSlidersSnapWithin = 5;
@implementation BGMAVM_AppNameLabel
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx {
#pragma unused (ctx)
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
#pragma unused (ctx, menuItem)
NSString* name = app.localizedName ? (NSString*)app.localizedName : @"";
self.stringValue = name;
@ -249,6 +340,26 @@ static float const kSlidersSnapWithin = 5;
@end
@implementation BGMAVM_ShowMoreControlsButton
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
#pragma unused (app)
// Set up the button that show/hide the extra controls (currently only a pan slider) for the app.
self.cell.representedObject = menuItem;
self.target = ctx;
self.action = @selector(showHideExtraControls:);
// The menu item starts out with the extra controls visible, so we hide them here.
//
// TODO: Leave them visible if any of the controls are set to non-default values. The user has no way to
// tell otherwise. Maybe we should also make this button look different if the controls are hidden
// when they have non-default values.
[ctx showHideExtraControls:self];
}
@end
@implementation BGMAVM_VolumeSlider {
// Will be set to -1 for apps without a pid
pid_t appProcessID;
@ -256,7 +367,9 @@ static float const kSlidersSnapWithin = 5;
BGMAppVolumes* context;
}
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx {
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
#pragma unused (menuItem)
context = ctx;
self.target = self;
@ -269,9 +382,11 @@ static float const kSlidersSnapWithin = 5;
self.minValue = kAppRelativeVolumeMinRawValue;
}
// We have to handle snapping for volume sliders ourselves because adding a tick mark (snap point) in Interface Builder
// changes how the slider looks.
- (void) snap {
// Snap to the 50% point
float midPoint = static_cast<float>((self.maxValue - self.minValue) / 2);
// Snap to the 50% point.
float midPoint = static_cast<float>((self.maxValue + self.minValue) / 2);
if (self.floatValue > (midPoint - kSlidersSnapWithin) && self.floatValue < (midPoint + kSlidersSnapWithin)) {
self.floatValue = midPoint;
}
@ -294,3 +409,39 @@ static float const kSlidersSnapWithin = 5;
@end
@implementation BGMAVM_PanSlider {
// Will be set to -1 for apps without a pid
pid_t appProcessID;
NSString* appBundleID;
BGMAppVolumes* context;
}
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
#pragma unused (menuItem)
context = ctx;
self.target = self;
self.action = @selector(appPanPositionChanged);
appProcessID = app.processIdentifier;
appBundleID = app.bundleIdentifier;
self.minValue = kAppPanLeftRawValue;
self.maxValue = kAppPanRightRawValue;
}
- (void) setPanPosition:(NSNumber *)panPosition {
self.intValue = panPosition.intValue;
}
- (void) appPanPositionChanged {
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue);
[context sendPanPositionChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
}
@end

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D17a" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<development version="7000" identifier="xcode"/>
@ -60,12 +60,12 @@
</items>
<point key="canvasLocation" x="-184" y="-69.5"/>
</menu>
<customView id="MWB-XH-kFI">
<rect key="frame" x="0.0" y="0.0" width="264" height="20"/>
<customView wantsLayer="YES" id="MWB-XH-kFI">
<rect key="frame" x="0.0" y="0.0" width="269" height="47"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField identifier="AppName" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xmd-bg-huG" customClass="BGMAVM_AppNameLabel">
<rect key="frame" x="58" y="4" width="115" height="14"/>
<rect key="frame" x="42" y="28" width="115" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="left" title="App name here" usesSingleLineMode="YES" id="ZHF-ZW-Oqg">
<font key="font" metaFont="smallSystem"/>
@ -74,23 +74,66 @@
</textFieldCell>
</textField>
<imageView identifier="AppIcon" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="W04-iT-IUw" customClass="BGMAVM_AppIcon">
<rect key="frame" x="36" y="3" width="16" height="16"/>
<rect key="frame" x="20" y="27" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="6QQ-oO-HxF"/>
</imageView>
<slider verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="I1l-Ci-4md" customClass="BGMAVM_VolumeSlider">
<rect key="frame" x="179" y="2" width="74" height="15"/>
<rect key="frame" x="163" y="27" width="74" height="15"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" controlSize="small" continuous="YES" state="on" alignment="left" maxValue="100" doubleValue="50" tickMarkPosition="above" sliderType="linear" id="Jmg-df-9Xl"/>
<accessibility description="Volume"/>
</slider>
<slider toolTip="Pan" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2mh-uO-kOV" customClass="BGMAVM_PanSlider">
<rect key="frame" x="163" y="7" width="74" height="15"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" controlSize="mini" continuous="YES" state="on" alignment="left" minValue="-100" maxValue="100" tickMarkPosition="below" numberOfTickMarks="1" sliderType="linear" id="ccM-Mt-93g"/>
<accessibility description="Pan"/>
</slider>
<button verticalHuggingPriority="750" fixedFrame="YES" tag="1" springLoaded="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vTG-n6-GxY" customClass="BGMAVM_ShowMoreControlsButton">
<rect key="frame" x="243" y="27" width="15" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<contentFilters>
<ciFilter name="CIAffineTransform">
<configuration>
<null key="inputImage"/>
<affineTransform key="inputTransform" m11="1" m12="0.0" m21="0.0" m22="1" tX="0.0" tY="2"/>
</configuration>
</ciFilter>
</contentFilters>
<buttonCell key="cell" type="square" title="⌃" bezelStyle="shadowlessSquare" image="F928CF85-B22A-4634-8160-BC84F45043E3" alignment="center" lineBreakMode="truncatingTail" imageScaling="proportionallyDown" inset="2" id="IXo-C7-3uE">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<textField toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9jc-9i-jw2">
<rect key="frame" x="162" y="-1" width="12" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="L" id="hgE-7A-bez">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<accessibility description="Pan left"/>
</textField>
<textField toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1lZ-hX-6Kl">
<rect key="frame" x="228" y="-1" width="12" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="R" id="lzr-NO-0Na">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<accessibility description="Pan right"/>
</textField>
</subviews>
<point key="canvasLocation" x="81" y="-111"/>
<point key="canvasLocation" x="117.5" y="-117.5"/>
</customView>
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="Cf4-3V-gl1" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
<windowPositionMask key="initialPositionMask" topStrut="YES"/>
<rect key="contentRect" x="248" y="350" width="980" height="335"/>
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="778"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="HlB-hX-Y0Y">
<rect key="frame" x="0.0" y="0.0" width="980" height="335"/>
<autoresizingMask key="autoresizingMask"/>
@ -165,7 +208,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
<rect key="frame" x="1" y="1" width="565" height="243"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" id="LSG-PF-cl8">
<rect key="frame" x="-4" y="0.0" width="572" height="243"/>
@ -186,14 +229,14 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
<rect key="frame" x="-15" y="1" width="16" height="0.0"/>
<rect key="frame" x="550" y="1" width="16" height="243"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6qu-yI-r00">
<rect key="frame" x="391" y="20" width="203" height="11"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="The AirPlay Logo is a trademark of Apple Inc." id="lx7-k3-q16">
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="The AirPlay Logo is a trademark of Apple Inc." id="lx7-k3-q16">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -213,10 +256,61 @@
<color key="textColor" red="0.11543657067200695" green="0.4338699494949495" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<color key="backgroundColor" red="0.99215692281723022" green="0.9960784912109375" blue="0.9960784912109375" alpha="1" colorSpace="deviceRGB"/>
</textFieldCell>
<point key="canvasLocation" x="101.5" y="496"/>
<point key="canvasLocation" x="-559" y="-118"/>
</textField>
</objects>
<resources>
<image name="F928CF85-B22A-4634-8160-BC84F45043E3" width="1" height="1">
<mutableData key="keyedArchiveRepresentation">
YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU
GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw
c1dOU0NvbG9ygAKADRIgwwAAgAOAC1Z7MSwgMX3SFQoWGFpOUy5vYmplY3RzoReABIAK0hUKGh2iGxyA
BYAGgAkQANIgCiEiXxAUTlNUSUZGUmVwcmVzZW50YXRpb26AB4AITxEIrE1NACoAAAAKAAAADgEAAAMA
AAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEGAAMAAAABAAEAAAERAAQA
AAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEWAAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMA
AAABAAEAAAFSAAMAAAABAAEAAAFTAAMAAAACAAEAAYdzAAcAAAf0AAAAuAAAAAAAAAf0YXBwbAIgAABt
bnRyR1JBWVhZWiAH0AACAA4ADAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYA
AQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVk
ZXNjAAAAwAAAAG9kc2NtAAABMAAABmZjcHJ0AAAHmAAAADh3dHB0AAAH0AAAABRrVFJDAAAH5AAAAA5k
ZXNjAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9m
aWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAA
AAAfAAAADHNrU0sAAAAqAAABhGVuVVMAAAAoAAABrmNhRVMAAAAsAAAB1nZpVk4AAAAsAAACAnB0QlIA
AAAqAAACLnVrVUEAAAAsAAACWGZyRlUAAAAqAAAChGh1SFUAAAAuAAACrnpoVFcAAAAQAAAC3G5iTk8A
AAAsAAAC7GtvS1IAAAAYAAADGGNzQ1oAAAAkAAADMGhlSUwAAAAgAAADVHJvUk8AAAAkAAADdGRlREUA
AAA6AAADmGl0SVQAAAAuAAAD0nN2U0UAAAAuAAAEAHpoQ04AAAAQAAAELmphSlAAAAAWAAAEPmVsR1IA
AAAkAAAEVHB0UE8AAAA4AAAEeG5sTkwAAAAqAAAEsGVzRVMAAAAoAAAE2nRoVEgAAAAkAAAFAnRyVFIA
AAAiAAAFJmZpRkkAAAAsAAAFSGhySFIAAAA6AAAFdHBsUEwAAAA2AAAFrnJ1UlUAAAAmAAAF5GFyRUcA
AAAoAAAGCmRhREsAAAA0AAAGMgBWAWEAZQBvAGIAZQBjAG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkA
bABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAFAAcgBvAGYAaQBsAGUAUABlAHIAZgBpAGwAIABkAGUA
IABnAHIAaQBzACAAZwBlAG4A6AByAGkAYwBDHqUAdQAgAGgA7ABuAGgAIABNAOAAdQAgAHgA4QBtACAA
QwBoAHUAbgBnAFAAZQByAGYAaQBsACAAQwBpAG4AegBhACAARwBlAG4A6QByAGkAYwBvBBcEMAQzBDAE
OwRMBD0EOAQ5ACAEPwRABD4ERAQwBDkEOwAgAEcAcgBhAHkAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIA
aQBxAHUAZQAgAGcAcgBpAHMAwQBsAHQAYQBsAOEAbgBvAHMAIABzAHoA/AByAGsAZQAgAHAAcgBvAGYA
aQBskBp1KHBwlo6Ccl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QB0AG8AbgBlAHAAcgBvAGYA
aQBsx3y8GAAgAEcAcgBhAHkAINUEuFzTDMd8AE8AYgBlAGMAbgD9ACABYQBlAGQA/QAgAHAAcgBvAGYA
aQBsBeQF6AXVBeQF2QXcACAARwByAGEAeQAgBdsF3AXcBdkAUAByAG8AZgBpAGwAIABnAHIAaQAgAGcA
ZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAARwByAGEAdQBzAHQAdQBmAGUAbgAtAFAA
cgBvAGYAaQBsAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwBHAGUA
bgBlAHIAaQBzAGsAIABnAHIA5QBzAGsAYQBsAGUAcAByAG8AZgBpAGxmbpAacHBepmPPj/Blh072TgCC
LDCwMOwwpDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA8ADwQO/A8YDrwO7ACADswO6A8EDuQBQAGUA
cgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMAaQBuAHoAZQBuAHQAbwBzAEEAbABnAGUA
bQBlAGUAbgAgAGcAcgBpAGoAcwBwAHIAbwBmAGkAZQBsAFAAZQByAGYAaQBsACAAZwByAGkAcwAgAGcA
ZQBuAOkAcgBpAGMAbw5CDhsOIw5EDh8OJQ5MDioONQ5ADhcOMg4XDjEOSA4nDkQOGwBHAGUAbgBlAGwA
IABHAHIAaQAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAHAAcgBvAGYA
aQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAHAAcgBvAGYAaQBsACAAcwBpAHYAaQBoACAAdABvAG4A
bwB2AGEAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABzAHoAYQByAG8BWwBjAGkE
HgQxBEkEOAQ5ACAEQQQ1BEAESwQ5ACAEPwRABD4ERAQ4BDsETAZFBkQGQQAgBioGOQYxBkoGQQAgAEcA
cgBhAHkAIAYnBkQGOQYnBkUARwBlAG4AZQByAGUAbAAgAGcAcgDlAHQAbwBuAGUAYgBlAHMAawByAGkA
dgBlAGwAcwBlAAB0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdodHMgcmVz
ZXJ2ZWQuAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAAAAABAc0AANIlJicoWiRjbGFzc25hbWVY
JGNsYXNzZXNfEBBOU0JpdG1hcEltYWdlUmVwoycpKlpOU0ltYWdlUmVwWE5TT2JqZWN00iUmLC1XTlNB
cnJheaIsKtIlJi8wXk5TTXV0YWJsZUFycmF5oy8sKtMyMwo0NTZXTlNXaGl0ZVxOU0NvbG9yU3BhY2VE
MCAwABADgAzSJSY4OVdOU0NvbG9yojgq0iUmOzxXTlNJbWFnZaI7Kl8QD05TS2V5ZWRBcmNoaXZlctE/
QFRyb290gAEACAARABoAIwAtADIANwBGAEwAVwBeAGUAcgB5AIEAgwCFAIoAjACOAJUAmgClAKcAqQCr
ALAAswC1ALcAuQC7AMAA1wDZANsJiwmQCZsJpAm3CbsJxgnPCdQJ3AnfCeQJ8wn3Cf4KBgoTChgKGgoc
CiEKKQosCjEKOQo8Ck4KUQpWAAAAAAAAAgEAAAAAAAAAQQAAAAAAAAAAAAAAAAAAClg
</mutableData>
</image>
<image name="FermataIcon" width="284" height="284"/>
</resources>
</document>

View file

@ -19,6 +19,7 @@
//
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2016 Josh Junon
// Copyright © 2017 Andrew Tonner
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Based largely on SA_Device.cpp from Apple's SimpleAudioDriver Plug-In sample code. Also uses a few sections from Apple's
@ -2102,17 +2103,44 @@ void BGM_Device::ApplyClientRelativeVolume(UInt32 inClientID, UInt32 inIOBufferF
Float32* theBuffer = reinterpret_cast<Float32*>(ioBuffer);
Float32 theRelativeVolume = mClients.GetClientRelativeVolumeRT(inClientID);
if(theRelativeVolume != 1.)
auto thePanPositionInt = mClients.GetClientPanPositionRT(inClientID);
Float32 thePanPosition = static_cast<Float32>(thePanPositionInt) / 100.0f;
// TODO When we get around to supporting devices with more than two channels, it would be worth looking into
// kAudioFormatProperty_PanningMatrix and kAudioFormatProperty_BalanceFade in AudioFormat.h.
// TODO precompute matrix coefficients w/ volume and do everything in one pass
// Apply balance w/ crossfeed to the frames in the buffer.
// Expect samples interleaved, starting with left
if (thePanPosition > 0.0f) {
for (UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2) {
auto L = i;
auto R = i + 1;
theBuffer[R] = theBuffer[R] + theBuffer[L] * thePanPosition;
theBuffer[L] = theBuffer[L] * (1 - thePanPosition);
}
} else if (thePanPosition < 0.0f) {
for (UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2) {
auto L = i;
auto R = i + 1;
theBuffer[L] = theBuffer[L] + theBuffer[R] * (-thePanPosition);
theBuffer[R] = theBuffer[R] * (1 + thePanPosition);
}
}
if(theRelativeVolume != 1.0f)
{
for(UInt32 i = 0; i < inIOBufferFrameSize * 2; i++)
{
Float32 theAdjustedSample = theBuffer[i] * theRelativeVolume;
// Clamp to [-1.0, 1.0]
// Clamp to [-1, 1].
// (This way is roughly 6 times faster than using std::min and std::max because the compiler can vectorize the loop.)
const Float32 theAdjustedSampleClippedBelow = theAdjustedSample < -1. ? -1. : theAdjustedSample;
theBuffer[i] = theAdjustedSampleClippedBelow > 1. ? 1. : theAdjustedSampleClippedBelow;
const Float32 theAdjustedSampleClippedBelow = theAdjustedSample < -1.0f ? -1.0f : theAdjustedSample;
theBuffer[i] = theAdjustedSampleClippedBelow > 1.0f ? 1.0f : theAdjustedSampleClippedBelow;
}
}
}
@ -2122,7 +2150,7 @@ bool BGM_Device::BufferIsAudible(UInt32 inIOBufferFrameSize, const void* inBuffe
// Check each frame to see if any are audible
for(UInt32 i = 0; i < inIOBufferFrameSize * 2; i++)
{
if (0. != reinterpret_cast<const Float32*>(inBuffer)[i]) {
if (0.0f != reinterpret_cast<const Float32*>(inBuffer)[i]) {
return true;
}
}

View file

@ -48,5 +48,6 @@ void BGM_Client::Copy(const BGM_Client& inClient)
mDoingIO = inClient.mDoingIO;
mIsMusicPlayer = inClient.mIsMusicPlayer;
mRelativeVolume = inClient.mRelativeVolume;
mPanPosition = inClient.mPanPosition;
}

View file

@ -72,6 +72,9 @@ public:
// mRelativeVolumeCurve is applied this this value when it's set.
Float32 mRelativeVolume = 1.0;
// The client's pan position, in the range [-100, 100] where -100 is left and 100 is right
SInt32 mPanPosition = 0;
};
#pragma clang assume_nonnull end

View file

@ -18,6 +18,7 @@
// BGMDriver
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2017 Andrew Tonner
//
// Self Include
@ -37,14 +38,16 @@ void BGM_ClientMap::AddClient(BGM_Client inClient)
{
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);
// If this client has been a client in the past (and has a bundle ID), copy its previous relative volume
// If this client has been a client in the past (and has a bundle ID), copy its previous audio settings
auto pastClientItr = inClient.mBundleID.IsValid() ? mPastClientMap.find(inClient.mBundleID) : mPastClientMap.end();
if(pastClientItr != mPastClientMap.end())
{
DebugMsg("BGM_ClientMap::AddClient: Found previous volume %f for client %u",
DebugMsg("BGM_ClientMap::AddClient: Found previous volume %f and pan %d for client %u",
pastClientItr->second.mRelativeVolume,
pastClientItr->second.mPanPosition,
inClient.mClientID);
inClient.mRelativeVolume = pastClientItr->second.mRelativeVolume;
inClient.mPanPosition = pastClientItr->second.mPanPosition;
}
// Add the new client to the shadow maps
@ -234,8 +237,8 @@ CACFArray BGM_ClientMap::CopyClientRelativeVolumesAsAppVolumes(CAVolumeCurve i
void BGM_ClientMap::CopyClientIntoAppVolumesArray(BGM_Client inClient, CAVolumeCurve inVolumeCurve, CACFArray& ioAppVolumes) const
{
// Only include clients set to a non-default volume
if(inClient.mRelativeVolume != 1.0)
// Only include clients set to a non-default volume or pan
if(inClient.mRelativeVolume != 1.0 || inClient.mPanPosition != 0)
{
CACFDictionary theAppVolume(false);
@ -244,6 +247,8 @@ void BGM_ClientMap::CopyClientIntoAppVolumesArray(BGM_Client inClient, CAVolu
// Reverse the volume conversion from SetClientsRelativeVolumes
theAppVolume.AddSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume),
inVolumeCurve.ConvertScalarToRaw(inClient.mRelativeVolume / 4));
theAppVolume.AddSInt32(CFSTR(kBGMAppVolumesKey_PanPosition),
inClient.mPanPosition);
ioAppVolumes.AppendDictionary(theAppVolume.GetDict());
}
@ -251,28 +256,75 @@ void BGM_ClientMap::CopyClientIntoAppVolumesArray(BGM_Client inClient, CAVolu
// TODO: Combine the SetClientsRelativeVolume methods? Their code is very similar.
bool BGM_ClientMap::SetClientsRelativeVolume(pid_t inAppPID, Float32 inRelativeVolume)
template <typename T>
std::vector<BGM_Client*> * _Nullable GetClientsFromMap(std::map<T, std::vector<BGM_Client*>> & map, T key) {
auto theClientItr = map.find(key);
if(theClientItr != map.end()) {
return &theClientItr->second;
}
return nullptr;
}
std::vector<BGM_Client*> * _Nullable BGM_ClientMap::GetClients(pid_t inAppPid) {
return GetClientsFromMap(mClientMapByPIDShadow, inAppPid);
}
std::vector<BGM_Client*> * _Nullable BGM_ClientMap::GetClients(CACFString inAppBundleID) {
return GetClientsFromMap(mClientMapByBundleIDShadow, inAppBundleID);
}
void ShowSetRelativeVolumeMessage(pid_t inAppPID, BGM_Client* theClient);
void ShowSetRelativeVolumeMessage(CACFString inAppBundleID, BGM_Client* theClient);
void ShowSetRelativeVolumeMessage(pid_t inAppPID, BGM_Client* theClient) {
(void)inAppPID;
(void)theClient;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by pid (%d)",
theClient->mRelativeVolume,
theClient->mClientID,
inAppPID);
}
void ShowSetRelativeVolumeMessage(CACFString inAppBundleID, BGM_Client* theClient) {
(void)inAppBundleID;
(void)theClient;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by bundle ID (%s)",
theClient->mRelativeVolume,
theClient->mClientID,
CFStringGetCStringPtr(inAppBundleID.GetCFString(), kCFStringEncodingUTF8));
}
// Template method declarations are running into LLVM bug 23987
// TODO: template these.
//bool BGM_ClientMap::SetClientsRelativeVolume(pid_t inAppPID, Float32 inRelativeVolume) {
// return SetClientsRelativeVolumeT<pid_t>(inAppPID, inRelativeVolume);
//}
//bool BGM_ClientMap::SetClientsRelativeVolume(CACFString inAppBundleID, Float32 inRelativeVolume) {
// return SetClientsRelativeVolumeT<CACFString>(inAppBundleID, inRelativeVolume)
//}
//template <typename T>
//bool BGM_ClientMap::SetClientsRelativeVolume(T searchKey, Float32 inRelativeVolume)
bool BGM_ClientMap::SetClientsRelativeVolume(pid_t searchKey, Float32 inRelativeVolume)
{
bool didChangeVolume = false;
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);
auto theSetVolumesInShadowMapsFunc = [&] {
// Look up the clients for the PID and update their volumes
auto theClientItr = mClientMapByPIDShadow.find(inAppPID);
// Look up the clients for the key and update their volumes
if(theClientItr != mClientMapByPIDShadow.end())
auto theClients = GetClients(searchKey);
if(theClients != nullptr)
{
std::vector<BGM_Client*> theClients = theClientItr->second;
for(BGM_Client* theClient : theClients)
for(BGM_Client* theClient : *theClients)
{
theClient->mRelativeVolume = inRelativeVolume;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by pid (%d)",
theClient->mRelativeVolume,
theClient->mClientID,
theClient->mProcessID);
ShowSetRelativeVolumeMessage(searchKey, theClient);
didChangeVolume = true;
}
@ -286,29 +338,23 @@ bool BGM_ClientMap::SetClientsRelativeVolume(pid_t inAppPID, Float32 inRelati
return didChangeVolume;
}
bool BGM_ClientMap::SetClientsRelativeVolume(CACFString inAppBundleID, Float32 inRelativeVolume)
bool BGM_ClientMap::SetClientsRelativeVolume(CACFString searchKey, Float32 inRelativeVolume)
{
bool didChangeVolume = false;
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);
auto theSetVolumesInShadowMapsFunc = [&] {
// Look up the clients for the bundle ID and update their volumes
auto theClientItr = mClientMapByBundleIDShadow.find(inAppBundleID);
// Look up the clients for the key and update their volumes
if(theClientItr != mClientMapByBundleIDShadow.end())
auto theClients = GetClients(searchKey);
if(theClients != nullptr)
{
std::vector<BGM_Client*> theClients = theClientItr->second;
for(BGM_Client* theClient : theClients)
for(BGM_Client* theClient : *theClients)
{
theClient->mRelativeVolume = inRelativeVolume;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by bundle ID (%s)",
theClient->mRelativeVolume,
theClient->mClientID,
CFStringGetCStringPtr(inAppBundleID.GetCFString(), kCFStringEncodingUTF8));
ShowSetRelativeVolumeMessage(searchKey, theClient);
didChangeVolume = true;
}
@ -322,6 +368,62 @@ bool BGM_ClientMap::SetClientsRelativeVolume(CACFString inAppBundleID, Float3
return didChangeVolume;
}
bool BGM_ClientMap::SetClientsPanPosition(pid_t searchKey, SInt32 inPanPosition)
{
bool didChangePanPosition = false;
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);
auto theSetPansInShadowMapsFunc = [&] {
// Look up the clients for the key and update their pan positions
auto theClients = GetClients(searchKey);
if(theClients != nullptr) {
for(auto theClient: *theClients) {
theClient->mPanPosition = inPanPosition;
// ShowSetPanPositionsMessage(searchKey, theClient)
didChangePanPosition = true;
}
}
};
theSetPansInShadowMapsFunc();
SwapInShadowMaps();
theSetPansInShadowMapsFunc();
return didChangePanPosition;
}
bool BGM_ClientMap::SetClientsPanPosition(CACFString searchKey, SInt32 inPanPosition)
{
bool didChangePanPosition = false;
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);
auto theSetPansInShadowMapsFunc = [&] {
// Look up the clients for the key and update their pan positions
auto theClients = GetClients(searchKey);
if(theClients != nullptr) {
for(auto theClient: *theClients) {
theClient->mPanPosition = inPanPosition;
// ShowSetPanPositionsMessage(searchKey, theClient)
didChangePanPosition = true;
}
}
};
theSetPansInShadowMapsFunc();
SwapInShadowMaps();
theSetPansInShadowMapsFunc();
return didChangePanPosition;
}
void BGM_ClientMap::UpdateClientIOStateNonRT(UInt32 inClientID, bool inDoingIO)
{
CAMutex::Locker theShadowMapsLocker(mShadowMapsMutex);

View file

@ -118,11 +118,26 @@ private:
void CopyClientIntoAppVolumesArray(BGM_Client inClient, CAVolumeCurve inVolumeCurve, CACFArray& ioAppVolumes) const;
public:
// Returns true if a client with PID inAppPID was found and its relative volume changed.
// Using the template function hits LLVM Bug 23987
// TODO Switch to template function
// Returns true if a client for the key was found and its relative volume changed.
//template <typename T>
//bool SetClientsRelativeVolume(T _Null_unspecified searchKey, Float32 inRelativeVolume);
//
//template <typename T>
//bool SetClientsPanPosition(T _Null_unspecified searchKey, SInt32 inPanPosition);
// Returns true if a client for PID inAppPID was found and its relative volume changed.
bool SetClientsRelativeVolume(pid_t inAppPID, Float32 inRelativeVolume);
// Returns true if a client with bundle ID inAppBundleID was found and its relative volume changed.
// Returns true if a client for bundle ID inAppBundleID was found and its relative volume changed.
bool SetClientsRelativeVolume(CACFString inAppBundleID, Float32 inRelativeVolume);
// Returns true if a client for PID inAppPID was found and its pan position changed.
bool SetClientsPanPosition(pid_t inAppPID, SInt32 inPanPosition);
// Returns true if a client for bundle ID inAppBundleID was found and its pan position changed.
bool SetClientsPanPosition(CACFString inAppBundleID, SInt32 inPanPosition);
void StartIONonRT(UInt32 inClientID) { UpdateClientIOStateNonRT(inClientID, true); }
void StopIONonRT(UInt32 inClientID) { UpdateClientIOStateNonRT(inClientID, false); }
@ -136,6 +151,11 @@ private:
// mutex must be locked when calling this method.
void SwapInShadowMapsRT();
// Client lookup for PID inAppPID
std::vector<BGM_Client*> * _Nullable GetClients(pid_t inAppPid);
// Client lookup for bundle ID inAppBundleID
std::vector<BGM_Client*> * _Nullable GetClients(CACFString inAppBundleID);
private:
BGM_TaskQueue* mTaskQueue;

View file

@ -18,6 +18,7 @@
// BGMDriver
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2017 Andrew Tonner
//
// Self Include
@ -296,6 +297,13 @@ Float32 BGM_Clients::GetClientRelativeVolumeRT(UInt32 inClientID) const
return (didGetClient ? theClient.mRelativeVolume : 1.0);
}
SInt32 BGM_Clients::GetClientPanPositionRT(UInt32 inClientID) const
{
BGM_Client theClient;
bool didGetClient = mClientMap.GetClientRT(inClientID, &theClient);
return (didGetClient ? theClient.mPanPosition : kAppPanCenterRawValue);
}
bool BGM_Clients::SetClientsRelativeVolumes(const CACFArray inAppVolumes)
{
bool didChangeAppVolumes = false;
@ -320,38 +328,70 @@ bool BGM_Clients::SetClientsRelativeVolumes(const CACFArray inAppVolumes)
BGM_InvalidClientRelativeVolumeException(),
"BGM_Clients::SetClientsRelativeVolumes: App volume was sent without PID or bundle ID for app");
Float32 theRelativeVolume;
bool didGetVolume;
{
SInt32 theRawRelativeVolume;
bool didGetVolume = theAppVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume), theRawRelativeVolume);
didGetVolume = theAppVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume), theRawRelativeVolume);
ThrowIf(!didGetVolume || theRawRelativeVolume < kAppRelativeVolumeMinRawValue || theRawRelativeVolume > kAppRelativeVolumeMaxRawValue,
BGM_InvalidClientRelativeVolumeException(),
"BGM_Clients::SetClientsRelativeVolumes: Relative volume for app missing or out of valid range");
if (didGetVolume) {
ThrowIf(didGetVolume && (theRawRelativeVolume < kAppRelativeVolumeMinRawValue || theRawRelativeVolume > kAppRelativeVolumeMaxRawValue),
BGM_InvalidClientRelativeVolumeException(),
"BGM_Clients::SetClientsRelativeVolumes: Relative volume for app out of valid range");
// Apply the volume curve to the raw volume
//
// mRelativeVolumeCurve uses the default kPow2Over1Curve transfer function, so we also multiply by 4 to
// keep the middle volume equal to 1 (meaning apps' volumes are unchanged by default).
Float32 theRelativeVolume = mRelativeVolumeCurve.ConvertRawToScalar(theRawRelativeVolume) * 4;
// Try to update the client's volume, first by PID and then, if that fails, by bundle ID
//
// TODO: Should we always try both in case an app has multiple clients?
if(mClientMap.SetClientsRelativeVolume(theAppPID, theRelativeVolume))
{
didChangeAppVolumes = true;
}
else if(mClientMap.SetClientsRelativeVolume(theAppBundleID, theRelativeVolume))
{
didChangeAppVolumes = true;
}
else
{
// TODO: The app isn't currently a client, so we should add it to the past clients map, or update its
// past volume if it's already in there.
}
// Apply the volume curve to the raw volume
//
// mRelativeVolumeCurve uses the default kPow2Over1Curve transfer function, so we also multiply by 4 to
// keep the middle volume equal to 1 (meaning apps' volumes are unchanged by default).
theRelativeVolume = mRelativeVolumeCurve.ConvertRawToScalar(theRawRelativeVolume) * 4;
}
}
// Try to update the client's volume, first by PID and then, if that fails, by bundle ID
//
// TODO: Should we always try both in case an app has multiple clients?
if(mClientMap.SetClientsRelativeVolume(theAppPID, theRelativeVolume))
bool didGetPanPosition;
{
didChangeAppVolumes = true;
}
else if(mClientMap.SetClientsRelativeVolume(theAppBundleID, theRelativeVolume))
{
didChangeAppVolumes = true;
}
else
{
// TODO: The app isn't currently a client, so we should add it to the past clients map, or update its
// past volume if it's already in there.
SInt32 thePanPosition;
didGetPanPosition = theAppVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_PanPosition), thePanPosition);
if (didGetPanPosition) {
ThrowIf(didGetPanPosition && (thePanPosition < kAppPanLeftRawValue || thePanPosition > kAppPanRightRawValue),
BGM_InvalidClientPanPositionException(),
"BGM_Clients::SetClientsRelativeVolumes: Pan position for app out of valid range");
if(mClientMap.SetClientsPanPosition(theAppPID, thePanPosition))
{
didChangeAppVolumes = true;
}
else if(mClientMap.SetClientsPanPosition(theAppBundleID, thePanPosition))
{
didChangeAppVolumes = true;
}
else
{
// TODO: The app isn't currently a client, so we should add it to the past clients map, or update its
// past pan position if it's already in there.
}
}
}
ThrowIf(!didGetVolume && !didGetPanPosition,
BGM_InvalidClientRelativeVolumeException(),
"BGM_Clients::SetClientsRelativeVolumes: No volume or pan position in request");
}
return didChangeAppVolumes;

View file

@ -95,6 +95,7 @@ public:
bool IsMusicPlayerRT(const UInt32 inClientID) const;
Float32 GetClientRelativeVolumeRT(UInt32 inClientID) const;
SInt32 GetClientPanPositionRT(UInt32 inClientID) const;
// Copies the current and past clients into an array in the format expected for
// kAudioDeviceCustomPropertyAppVolumes. (Except that CACFArray and CACFDictionary are used instead
@ -102,7 +103,8 @@ public:
CACFArray CopyClientRelativeVolumesAsAppVolumes() const { return mClientMap.CopyClientRelativeVolumesAsAppVolumes(mRelativeVolumeCurve); };
// inAppVolumes is an array of dicts with the keys kBGMAppVolumesKey_ProcessID,
// kBGMAppVolumesKey_BundleID and kBGMAppVolumesKey_RelativeVolume. This method finds the client for
// kBGMAppVolumesKey_BundleID and optionally kBGMAppVolumesKey_RelativeVolume and
// kBGMAppVolumesKey_PanPosition. This method finds the client for
// each app by PID or bundle ID, sets the volume and applies mRelativeVolumeCurve to it.
//
// Returns true if any clients' relative volumes were changed.

View file

@ -110,6 +110,9 @@ enum
// applied to kBGMAppVolumesKey_RelativeVolume when it's first set and then each of the app's samples are multiplied
// by it.
#define kBGMAppVolumesKey_RelativeVolume "rvol"
// A CFNumber<SInt32> between kAppPanLeftRawValue and kAppPanRightRawValue. A negative value has a higher proportion
// of left channel, and a positive value has a higher proportion of right channel.
#define kBGMAppVolumesKey_PanPosition "ppos"
// The app's pid as a CFNumber. May be omitted if kBGMAppVolumesKey_BundleID is present.
#define kBGMAppVolumesKey_ProcessID "pid"
// The app's bundle ID as a CFString. May be omitted if kBGMAppVolumesKey_ProcessID is present.
@ -121,6 +124,11 @@ enum
#define kAppRelativeVolumeMinDbValue -96.0f
#define kAppRelativeVolumeMaxDbValue 0.0f
// Pan position values
#define kAppPanLeftRawValue -100
#define kAppPanCenterRawValue 0
#define kAppPanRightRawValue 100
#pragma mark BGMDevice Custom Property Addresses
// For convenience.
@ -174,6 +182,7 @@ enum {
class BGM_InvalidClientException { };
class BGM_InvalidClientPIDException { };
class BGM_InvalidClientRelativeVolumeException { };
class BGM_InvalidClientPanPositionException { };
class BGM_DeviceNotSetException { };
class BGM_RuntimeException { };