mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-09-20 14:31:56 +00:00
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:
parent
31b501e832
commit
7992a5708c
13 changed files with 511 additions and 88 deletions
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newOutputDevice.GetObjectID()
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : 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 {
|
||||
|
|
|
@ -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"/>
|
||||
|
|
98
BGMApp/BGMApp/Images.xcassets/AirPlayIcon.imageset/AirPlay.pdf
vendored
Normal file
98
BGMApp/BGMApp/Images.xcassets/AirPlayIcon.imageset/AirPlay.pdf
vendored
Normal file
File diff suppressed because one or more lines are too long
21
BGMApp/BGMApp/Images.xcassets/AirPlayIcon.imageset/Contents.json
vendored
Normal file
21
BGMApp/BGMApp/Images.xcassets/AirPlayIcon.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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) */
|
||||
|
|
Loading…
Reference in a new issue