Use data source names instead of device names in the output device menu.

The names of the data source(s) for a device are generally the names
intended to be shown to the user, since the OS X volume menu, System
Preferences, etc. use them.

A menu item is now added for each data source of each output device,
rather than one per device.

Also adds some macros/functions for casting values to __nonnull.

Resolves #59.
This commit is contained in:
Kyle Neideck 2016-12-23 01:46:27 +11:00
parent 31b501e832
commit 7992a5708c
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
13 changed files with 511 additions and 88 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
LastUpgradeVersion = "0810"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
LastUpgradeVersion = "0810"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -36,6 +36,8 @@
#import "SystemPreferences.h"
#pragma clang assume_nonnull begin
static float const kStatusBarIconPadding = 0.25;
@implementation AppDelegate {
@ -81,6 +83,15 @@ static float const kStatusBarIconPadding = 0.25;
- (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
#pragma unused (aNotification)
// Log the version/build number.
//
// TODO: NSLog should only be used for logging errors.
// TODO: Automatically add the commit ID to the end of the build number for unreleased builds. (In the
// Info.plist or something -- not here.)
NSLog(@"BGMApp version: %@, BGMApp build number: %@",
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]);
// Set up the rest of the UI and other external interfaces.
@ -140,8 +151,10 @@ static float const kStatusBarIconPadding = 0.25;
}
- (void) applicationWillTerminate:(NSNotification*)aNotification {
#pragma unused (aNotification)
#pragma unused (aNotification)
DebugMsg("AppDelegate::applicationWillTerminate");
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
if (error) {
@ -271,3 +284,5 @@ static float const kStatusBarIconPadding = 0.25;
@end
#pragma clang assume_nonnull end

View file

@ -26,12 +26,12 @@
// PublicUtility Includes
#ifdef __cplusplus
#include "CAHALAudioDevice.h"
#import "CAHALAudioDevice.h"
#endif
// System Includes
#import <Foundation/Foundation.h>
#include <CoreAudio/AudioHardwareBase.h>
#import <CoreAudio/AudioHardwareBase.h>
#pragma clang assume_nonnull begin
@ -53,6 +53,8 @@ extern int const kBGMErrorCode_OutputDeviceNotFound;
#endif
- (BOOL) isOutputDevice:(AudioObjectID)deviceID;
- (BOOL) isOutputDataSource:(UInt32)dataSourceID;
// Set the audio output device that BGMApp uses.
//
// Returns an error if the output device couldn't be changed. If revertOnFailure is true in that case,
@ -61,9 +63,17 @@ extern int const kBGMErrorCode_OutputDeviceNotFound;
//
// Both errors' codes will be the code of the exception that caused the failure, if any, generally one
// of the error constants from AudioHardwareBase.h.
//
// Blocks while the old device stops IO (if there was one).
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
revertOnFailure:(BOOL)revertOnFailure;
// As above, but also sets the new output device's data source. See kAudioDevicePropertyDataSource in
// AudioHardware.h.
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
dataSourceID:(UInt32)dataSourceID
revertOnFailure:(BOOL)revertOnFailure;
// Blocks until IO has started running on the output device (for playthrough).
- (OSStatus) waitForOutputDeviceToStart;

View file

@ -265,40 +265,90 @@ public:
}
}
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID revertOnFailure:(BOOL)revertOnFailure {
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting output device. deviceID=%u", deviceID);
- (BOOL) isOutputDataSource:(UInt32)dataSourceID {
@synchronized (self) {
try {
AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
UInt32 channel = 0;
return outputDevice.HasDataSourceControl(scope, channel) &&
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
} catch (CAException e) {
BGMLogException(e);
return false;
}
}
}
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
revertOnFailure:(BOOL)revertOnFailure {
return [self setOutputDeviceWithIDImpl:deviceID
dataSourceID:nil
revertOnFailure:revertOnFailure];
}
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
dataSourceID:(UInt32)dataSourceID
revertOnFailure:(BOOL)revertOnFailure {
return [self setOutputDeviceWithIDImpl:deviceID
dataSourceID:&dataSourceID
revertOnFailure:revertOnFailure];
}
- (NSError* __nullable) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
dataSourceID:(UInt32* __nullable)dataSourceID
revertOnFailure:(BOOL)revertOnFailure {
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting output device. newDeviceID=%u",
newDeviceID);
AudioDeviceID currentDeviceID = outputDevice.GetObjectID();
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (GetObjectID doesn't throw.)
// Set up playthrough and control sync
BGMAudioDevice newOutputDevice(deviceID);
BGMAudioDevice newOutputDevice(newDeviceID);
try {
@synchronized (self) {
// Mirror changes in BGMDevice's controls to the new output device's.
deviceControlSync = BGMDeviceControlSync(bgmDevice, newOutputDevice);
// Re-read the device ID after entering the monitor. (The initial read is because
// currentDeviceID's used in the catch blocks.)
currentDeviceID = outputDevice.GetObjectID();
// Stream audio from BGMDevice to the output device.
//
// TODO: Should this be done async? Some output devices take a long time to start IO (e.g. AirPlay) and I
// assume this blocks the main thread. Haven't tried it to check, though.
playThrough = BGMPlayThrough(bgmDevice, newOutputDevice);
if (newDeviceID != currentDeviceID) {
// Mirror changes in BGMDevice's controls to the new output device's.
deviceControlSync = BGMDeviceControlSync(bgmDevice, newOutputDevice);
// Stream audio from BGMDevice to the new output device. This blocks while the old device
// stops IO.
playThrough = BGMPlayThrough(bgmDevice, newOutputDevice);
outputDevice = BGMAudioDevice(deviceID);
outputDevice = newOutputDevice;
}
// Set the output device to use the new data source.
if (dataSourceID) {
// TODO: If this fails, ideally we'd still start playthrough and return an error, but not
// revert the device. It would probably be a bit awkward, though.
[self setDataSource:*dataSourceID device:outputDevice];
}
if (newDeviceID != currentDeviceID) {
// We successfully changed to the new device. Start playthrough on it, since audio might be
// playing. (If we only changed the data source, playthrough will already be running if it
// needs to be.)
playThrough.Start();
// But stop playthrough if audio isn't playing, since it uses CPU.
playThrough.StopIfIdle();
}
}
// Start playthrough because audio might be playing.
//
// TODO: If audio isn't playing, this makes playthrough run until the user plays audio and then stops it again,
// which wastes CPU. I think we could just have Start() call StopIfIdle(), but I haven't tried it yet.
playThrough.Start();
playThrough.StopIfIdle();
} catch (CAException e) {
return [self failedToSetOutputDevice:newOutputDevice.GetObjectID()
BGMAssert(e.GetError() != kAudioHardwareNoError,
"CAException with kAudioHardwareNoError");
return [self failedToSetOutputDevice:newDeviceID
errorCode:e.GetError()
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
} catch (...) {
return [self failedToSetOutputDevice:newOutputDevice.GetObjectID()
return [self failedToSetOutputDevice:newDeviceID
errorCode:kAudioHardwareUnspecifiedError
revertTo:(revertOnFailure ? &currentDeviceID : nullptr)];
}
@ -306,6 +356,20 @@ public:
return nil;
}
- (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice)device {
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", [&]() {
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
UInt32 channel = 0;
if (device.DataSourceControlIsSettable(scope, channel)) {
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting dataSourceID=%u",
dataSourceID);
device.SetCurrentDataSourceByID(scope, channel, dataSourceID);
}
});
}
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID
errorCode:(OSStatus)errorCode
revertTo:(AudioDeviceID*)revertTo {

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11542" systemVersion="16B2333a" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11542" systemVersion="16C48b" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<development version="7000" identifier="xcode"/>
@ -86,17 +86,17 @@
</subviews>
<point key="canvasLocation" x="81" y="-111"/>
</customView>
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="Cf4-3V-gl1" customClass="NSPanel">
<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="816" height="310"/>
<rect key="contentRect" x="248" y="350" width="980" height="335"/>
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="778"/>
<view key="contentView" id="HlB-hX-Y0Y">
<rect key="frame" x="0.0" y="0.0" width="816" height="310"/>
<rect key="frame" x="0.0" y="0.0" width="980" height="335"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r51-dd-LGP">
<rect key="frame" x="53" y="95" width="240" height="22"/>
<rect key="frame" x="61" y="125" width="240" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Background Music" id="Dw2-nu-eBQ">
<font key="font" size="18" name=".HelveticaNeueDeskInterface-Regular"/>
@ -105,7 +105,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="ekc-h0-I43">
<rect key="frame" x="53" y="70" width="240" height="17"/>
<rect key="frame" x="61" y="100" width="240" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Version 0.1.0" id="FDH-7l-wFf">
<font key="font" metaFont="system"/>
@ -114,7 +114,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L5P-Lw-aCd">
<rect key="frame" x="367" y="273" width="270" height="17"/>
<rect key="frame" x="391" y="298" width="270" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Licensed under GPLv2 or any later version." id="ETh-En-bzX">
<font key="font" metaFont="system"/>
@ -123,11 +123,11 @@
</textFieldCell>
</textField>
<box horizontalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Zc9-gs-X8C">
<rect key="frame" x="332" y="88" width="5" height="150"/>
<rect key="frame" x="361" y="93" width="5" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
<rect key="frame" x="18" y="45" width="310" height="17"/>
<rect key="frame" x="26" y="75" width="310" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016 Background Music contributors" placeholderString="" id="ctF-95-uVu">
<font key="font" metaFont="system"/>
@ -136,7 +136,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" translatesAutoresizingMaskIntoConstraints="NO" id="nx6-kQ-N8Z" customClass="BGMLinkField">
<rect key="frame" x="18" y="20" width="310" height="17"/>
<rect key="frame" x="26" y="50" width="310" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" tag="3" title="https://github.com/kyleneideck/BackgroundMusic" placeholderString="" id="VOb-5X-o3R">
<font key="font" metaFont="system"/>
@ -145,7 +145,7 @@
</textFieldCell>
</textField>
<imageView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Tui-Hf-FLv">
<rect key="frame" x="98" y="125" width="150" height="150"/>
<rect key="frame" x="106" y="155" width="150" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<shadow key="shadow">
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
@ -153,7 +153,7 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="FermataIcon" id="dBU-ZS-ZzA"/>
</imageView>
<imageView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="R1R-Rd-xPC">
<rect key="frame" x="98" y="125" width="150" height="150"/>
<rect key="frame" x="106" y="155" width="150" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<shadow key="shadow">
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
@ -161,19 +161,22 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="FermataIcon" id="1VP-dU-RCe"/>
</imageView>
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eqz-ap-PAC">
<rect key="frame" x="369" y="20" width="427" height="245"/>
<rect key="frame" x="393" y="45" width="567" height="245"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
<rect key="frame" x="1" y="1" width="425" height="243"/>
<rect key="frame" x="1" y="1" width="565" height="243"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" usesFontPanel="YES" findStyle="panel" continuousSpellChecking="YES" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" quoteSubstitution="YES" dashSubstitution="YES" smartInsertDelete="YES" id="LSG-PF-cl8">
<rect key="frame" x="0.0" y="0.0" width="425" height="243"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="minSize" width="425" height="243"/>
<size key="maxSize" width="477" height="10000000"/>
<size key="minSize" width="565" height="243"/>
<size key="maxSize" width="594" height="10000000"/>
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
</allowedInputSourceLocales>
</textView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
@ -183,13 +186,22 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
<rect key="frame" x="410" y="1" width="16" height="243"/>
<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">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<point key="canvasLocation" x="-31" y="220"/>
<point key="canvasLocation" x="-177" y="232.5"/>
</window>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" id="IoN-sN-cCx">
<rect key="frame" x="0.0" y="0.0" width="471" height="180"/>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "AirPlay.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -113,6 +113,11 @@ static NSInteger const kProjectWebsiteLabelTag = 3;
}
licenseView.string = licenseStr;
NSFont* __nullable font = [NSFont fontWithName:@"Andale Mono" size:0.0];
if (font) {
licenseView.textStorage.font = font;
}
}
}

View file

@ -55,46 +55,145 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
for (NSMenuItem* item in outputDeviceMenuItems) {
[prefsMenu removeItem:item];
}
[outputDeviceMenuItems removeAllObjects];
// Insert menu items after the item for the "Output Device" heading
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
[outputDeviceMenuItems removeAllObjects];
// Add a menu item for each output device
CAHALAudioSystemObject audioSystem;
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
if (numDevices > 0) {
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
audioSystem.GetAudioDevices(numDevices, devices);
for (UInt32 i = 0; i < numDevices; i++) {
CAHALAudioDevice device(devices[i]);
// TODO: Handle C++ exceptions. (And the ones above that we don't swallow.)
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::populatePreferencesMenu", [&]() {
BOOL hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
if (device.GetObjectID() != [audioDevices bgmDevice].GetObjectID() && hasOutputChannels) {
NSString* deviceName = CFBridgingRelease(device.CopyName());
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:deviceName
action:@selector(outputDeviceWasChanged:)
keyEquivalent:@""];
BOOL selected = [audioDevices isOutputDevice:device.GetObjectID()];
item.state = (selected ? NSOnState : NSOffState);
item.target = self;
item.indentationLevel = 1;
item.representedObject = [NSNumber numberWithUnsignedInt:device.GetObjectID()];
[prefsMenu insertItem:item atIndex:menuItemsIdx];
[outputDeviceMenuItems addObject:item];
}
});
[self insertMenuItemsForDevice:device preferencesMenu:prefsMenu];
}
}
}
- (void) insertMenuItemsForDevice:(CAHALAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
// Insert menu items after the item for the "Output Device" heading.
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", [&]() {
BOOL isBGMDevice = (device.GetObjectID() == [audioDevices bgmDevice].GetObjectID());
BOOL isHidden = device.IsHidden();
BOOL hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
BOOL canBeDefault = device.CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
if (!isBGMDevice && !isHidden && hasOutputChannels && canBeDefault) {
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
[prefsMenu insertItem:item atIndex:menuItemsIdx];
[outputDeviceMenuItems addObject:item];
}
}
});
}
- (NSArray<NSMenuItem*>*) createMenuItemsForDevice:(CAHALAudioDevice)device {
// We fill this array with a menu item for each output device (or each data source for each device) on
// the system.
NSMutableArray<NSMenuItem*>* items = [NSMutableArray new];
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
UInt32 channel = 0; // 0 is the master channel.
// 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
// than "Built-in Output".
//
// TODO: Handle data destinations as well? I don't have (or know of) any hardware with them.
// TODO: Use the current data source's name when the control isn't settable, but only add one menu item.
UInt32 numDataSources = 0;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&]() {
if (device.HasDataSourceControl(scope, channel) &&
device.DataSourceControlIsSettable(scope, channel)) {
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
}
});
if (numDataSources > 0) {
UInt32 dataSourceIDs[numDataSources];
// This call updates numDataSources to the real number of IDs it added to our array.
device.GetAvailableDataSources(scope, channel, numDataSources, dataSourceIDs);
for (UInt32 i = 0; i < numDataSources; i++) {
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u %s%u",
"Device ID:", device.GetObjectID(),
", Data source ID:", dataSourceIDs[i]);
BGMLogAndSwallowExceptionsMsg("BGMOutputDevicePrefs::createMenuItemsForDevice", "(DS)", [&]() {
NSNumber* dataSourceID = [NSNumber numberWithUnsignedInt:dataSourceIDs[i]];
NSString* dataSourceName =
CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i]));
NSString* deviceName = CFBridgingRelease(device.CopyName());
[items addObject:[self createMenuItemForDevice:device
dataSourceID:dataSourceID
title:dataSourceName
toolTip:deviceName]];
});
}
} else {
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
"Device ID:", device.GetObjectID());
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&]() {
[items addObject:[self createMenuItemForDevice:device
dataSourceID:nil
title:CFBridgingRelease(device.CopyName())
toolTip:nil]];
});
}
return items;
}
- (NSMenuItem*) createMenuItemForDevice:(CAHALAudioDevice)device
dataSourceID:(NSNumber* __nullable)dataSourceID
title:(NSString* __nullable)title
toolTip:(NSString* __nullable)toolTip {
// If we don't have a title, use the tool-tip text instead.
if (!title) {
title = (toolTip ? toolTip : @"");
}
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title)
action:@selector(outputDeviceWasChanged:)
keyEquivalent:@""];
// Add the AirPlay icon to the labels of AirPlay devices.
//
// TODO: Test this with real hardware that supports AirPlay. (I don't have any.)
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemForDevice", [&]() {
if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) {
item.image = [NSImage imageNamed:@"AirPlayIcon"];
// Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or
// OS X is in dark mode.
[item.image setTemplate:YES];
}
});
// The menu item should be selected if it's the menu item for the current output device. If the device
// has data sources, only the menu item for the current data source should be selected.
BOOL isSelected =
[audioDevices isOutputDevice:device.GetObjectID()] &&
(!dataSourceID || [audioDevices isOutputDataSource:[dataSourceID unsignedIntValue]]);
item.state = (isSelected ? NSOnState : NSOffState);
item.toolTip = toolTip;
item.target = self;
item.indentationLevel = 1;
item.representedObject = @{ @"deviceID": [NSNumber numberWithUnsignedInt:device.GetObjectID()],
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
return item;
}
- (void) outputDeviceWasChanged:(NSMenuItem*)menuItem {
DebugMsg("BGMOutputDevicePrefs::outputDeviceWasChanged: '%s' menu item selected",
[menuItem.title UTF8String]);
@ -105,22 +204,59 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
}
// Change to the new output device.
AudioDeviceID newDeviceID = [[menuItem representedObject] unsignedIntValue];
NSError* __nullable error = [audioDevices setOutputDeviceWithID:newDeviceID revertOnFailure:YES];
AudioDeviceID newDeviceID = [[menuItem representedObject][@"deviceID"] unsignedIntValue];
id newDataSourceID = [menuItem representedObject][@"dataSourceID"];
BOOL changingDevice = ![audioDevices isOutputDevice:newDeviceID];
BOOL changingDataSource =
(newDataSourceID != [NSNull null]) &&
![audioDevices isOutputDataSource:[newDataSourceID unsignedIntValue]];
if (changingDevice || changingDataSource) {
NSString* deviceName =
menuItem.toolTip ?
[NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] :
menuItem.title;
// Dispatched because it usually blocks. (Note that we're using QOS_CLASS_USER_INITIATED
// rather than QOS_CLASS_USER_INTERACTIVE.)
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
[self changeToOutputDevice:newDeviceID
newDataSource:newDataSourceID
deviceName:deviceName];
});
}
}
- (void) changeToOutputDevice:(AudioDeviceID)deviceID
newDataSource:(id)dataSourceID
deviceName:(NSString*)deviceName {
NSError* __nullable error;
if (dataSourceID == [NSNull null]) {
error = [audioDevices setOutputDeviceWithID:deviceID revertOnFailure:YES];
} else {
error = [audioDevices setOutputDeviceWithID:deviceID
dataSourceID:[dataSourceID unsignedIntValue]
revertOnFailure:YES];
}
if (error) {
// Couldn't change the output device, so show a warning. (No need to change the menu selection back
// because it's repopulated every time it's opened.)
NSLog(@"Failed to set output device: %@", menuItem);
// Couldn't change the output device, so show a warning. (No need to change the menu
// selection back because it gets repopulated every time it's opened.)
NSAlert* alert = [NSAlert new];
NSString* deviceName = [menuItem title];
alert.messageText =
[NSString stringWithFormat:@"Failed to set %@ as the output device", deviceName];
alert.informativeText = @"This is probably a bug. Feel free to report it.";
[alert runModal];
// NSAlerts should only be shown on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Failed to set output device: %@", deviceName);
NSAlert* alert = [NSAlert new];
alert.messageText =
[NSString stringWithFormat:@"Failed to set %@ as the output device.", deviceName];
alert.informativeText = @"This is probably a bug. Feel free to report it.";
[alert runModal];
});
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
LastUpgradeVersion = "0810"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0810"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -37,6 +37,7 @@
// System Includes
#include <mach/error.h>
#pragma mark Macros
// The Assert macro from CADebugMacros with support for format strings added.
#define BGMAssert(inCondition, inMessage, ...) \
@ -46,8 +47,54 @@
__ASSERT_STOP; \
}
#define BGMAssertNonNull(expression) \
BGMAssertNonNull2((expression), #expression)
#pragma clang assume_nonnull begin
#define BGMAssertNonNull2(expression, expressionStr) \
BGMAssert((expression), \
"%s:%d:%s: '%s' is null", \
__FILE__, \
__LINE__, \
__FUNCTION__, \
expressionStr);
#pragma mark Objective-C Macros
#if defined(__OBJC__)
#if __has_feature(objc_generics)
// This trick is from https://gist.github.com/robb/d55b72d62d32deaee5fa
@interface BGMNonNullCastHelper<__covariant T>
- (nonnull T) asNonNull;
@end
// Explicitly casts expressions from nullable to non-null. Only works with expressions that
// evaluate to Objective-C objects. Use BGM_Utils::NN for other types.
//
// TODO: Replace existing non-null casts with this.
#define BGMNN(expression) ({ \
__typeof((expression)) value = (expression); \
BGMAssertNonNull2(value, #expression); \
BGMNonNullCastHelper<__typeof((expression))>* helper; \
(__typeof(helper.asNonNull))value; \
})
#else /* __has_feature(objc_generics) */
#define BGMNN(expression) ({ \
id value = (expression); \
BGMAssertNonNull2(value, #expression); \
value; \
})
#endif /* __has_feature(objc_generics) */
#endif /* defined(__OBJC__) */
#pragma mark C++ Macros
#if defined(__cplusplus)
@ -75,9 +122,25 @@
#define BGMLogUnexpectedExceptionsMsg(callerName, message, function) \
BGM_Utils::LogUnexpectedExceptions(__FILE__, __LINE__, callerName, message, function)
#endif /* defined(__cplusplus) */
#pragma clang assume_nonnull begin
#if defined(__cplusplus)
#pragma mark C++ Utility Functions
namespace BGM_Utils
{
// Used to explicitly cast from nullable to non-null. For Objective-C objects, use the BGMNN
// macro (above).
template <typename T>
inline T __nonnull NN(T __nullable v) {
BGMAssertNonNull(v);
return v;
}
// Log (and swallow) errors returned by Mach functions. Returns false if there was an error.
bool LogIfMachError(const char* callerName,
const char* errorReturnedBy,
@ -136,7 +199,6 @@ namespace BGM_Utils
const char* callerName,
const char* __nullable message,
const std::function<void(void)>& function);
}
#endif /* defined(__cplusplus) */