Compare commits
110 Commits
10.9Support
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d322c3ac9b | |||
| 45519a4d52 | |||
| ed2f356570 | |||
| 4d80e1f38f | |||
| ca56d8d0b2 | |||
| 5e4556b49d | |||
| 8d1adf25bb | |||
| c33941a22b | |||
| ec7128495f | |||
| 6fc46c7943 | |||
| b986b687ea | |||
| c617d98f9d | |||
| 4839ea8a4b | |||
| 612e249e1b | |||
| d4df6107bd | |||
| f5628e78a9 | |||
| 728a3a7331 | |||
| f254e8c58d | |||
| 5f9487deb0 | |||
| 6a26afe47a | |||
| 78e2813af1 | |||
| 87af15d290 | |||
| 7b32b6ef66 | |||
| 32723ff04b | |||
| 60e1b3564b | |||
| d49ff20820 | |||
| 9b5d5bf921 | |||
| 07c1c2320b | |||
| 523ad02761 | |||
| 3ee563e4c5 | |||
| e5c406da16 | |||
| b5cf6de2ac | |||
| 8257f49b46 | |||
| cdea147010 | |||
| a91615fc5e | |||
| 467b072a9d | |||
| a62fae6fd1 | |||
| 129c21a180 | |||
| 2d135838fa | |||
| acf3976b9b | |||
| 61ce9ef165 | |||
| f6ac51a334 | |||
| 94c594b342 | |||
| d7e3980af8 | |||
| 2cb572ecf4 | |||
| 8d5ea9c67c | |||
| 651b91aeee | |||
| 12840cbf31 | |||
| f15708461d | |||
| a4160d370d | |||
| 847313a174 | |||
| 7992a5708c | |||
| 31b501e832 | |||
| ab9d4cdc2b | |||
| e0acb34f29 | |||
| ec87adb6e9 | |||
| 810b2ed462 | |||
| 035faa615f | |||
| c8f6790274 | |||
| b0bfebedc4 | |||
| da74e5ea1d | |||
| a5fb68ad2b | |||
| 61a9d89ef9 | |||
| b2a12b3e37 | |||
| d38ea256cd | |||
| e31f2b1c29 | |||
| 59aa04c9bc | |||
| cbbd48dcee | |||
| c91af08d54 | |||
| 484ffa16f3 | |||
| 4e091c7398 | |||
| 808fe1b6b3 | |||
| 758fe02c7a | |||
| 23fd57713d | |||
| dad87b57b6 | |||
| 679d624860 | |||
| 55e9f60774 | |||
| e3fcbdb37e | |||
| 1ee9fa348e | |||
| 3684483543 | |||
| 30a1735346 | |||
| 4dba9412fb | |||
| 90bceb9887 | |||
| 6c9ca6e85c | |||
| 82ea2e4803 | |||
| bbe65a2431 | |||
| a229791ade | |||
| be4135523e | |||
| b58ad2a1f8 | |||
| 960fe0d28d | |||
| d827e7e0d8 | |||
| 34071e633f | |||
| d021dff7a6 | |||
| e95f371305 | |||
| 6fb281c3ce | |||
| 3f62b012c3 | |||
| c70a22dd24 | |||
| ccb709fc02 | |||
| 480d769c26 | |||
| 3eac1f5dab | |||
| eda3505f1a | |||
| 44082ac920 | |||
| f2a0898590 | |||
| b707513e49 | |||
| 8acc5d4c9e | |||
| ad5fe4ecbb | |||
| 7cc0d19182 | |||
| 9a5ed7c2b5 | |||
| 5981e05bb1 | |||
| e87c43dc52 |
@@ -2,6 +2,10 @@
|
||||
.*.swp
|
||||
/BGMDriver/BGMDriver/quick_install.conf
|
||||
/build_and_install.log
|
||||
.idea/
|
||||
tags
|
||||
cmake-build-debug/
|
||||
/Background-Music-*/
|
||||
|
||||
# Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
language: objective-c
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
xcode_sdk: macosx10.12
|
||||
sudo: required
|
||||
env: DEPLOY=true
|
||||
- os: osx
|
||||
osx_image: xcode8.2
|
||||
xcode_sdk: macosx10.12
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8.1
|
||||
xcode_sdk: macosx10.12
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8
|
||||
xcode_sdk: macosx10.11
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode7.3
|
||||
xcode_sdk: macosx10.11
|
||||
sudo: required
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
install:
|
||||
# Install Apple's NullAudio device. Travis' VMs don't have any audio devices installed.
|
||||
- sudo xcodebuild -project BGMApp/BGMAppTests/NullAudio/AudioDriverExamples.xcodeproj -target NullAudio DSTROOT="/" install
|
||||
- sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod || sudo killall coreaudiod
|
||||
script:
|
||||
# Build in a case-sensitive disk image to catch failures that only happen on case-sensitive filesystems.
|
||||
- hdiutil create -type SPARSEBUNDLE -fs 'Case-sensitive Journaled HFS+' -volname bgmbuild -nospotlight -verbose -attach -size 50m bgmbuild.dmg
|
||||
- sudo cp -r . /Volumes/bgmbuild
|
||||
- cd /Volumes/bgmbuild
|
||||
# Install Background Music.
|
||||
- yes | ./build_and_install.sh
|
||||
- cat build_and_install.log
|
||||
- find */build/Release/*/ -type f -exec md5 {} \;
|
||||
# Log the installed audio devices...
|
||||
- system_profiler SPAudioDataType
|
||||
# ...and their IDs.
|
||||
- say -a '?'
|
||||
# Check the BGM dirs and files were installed. (These fail if the dir/file isn't found.)
|
||||
- ls -la "/Applications/Background Music.app"
|
||||
- ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver"
|
||||
- ls -la "/usr/local/libexec/BGMXPCHelper.xpc" || ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"
|
||||
- ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"
|
||||
# Close BGMApp (which the install script opened).
|
||||
- osascript -e 'tell application "Background Music" to quit'
|
||||
# Skip the UI tests until Travis has support for them.
|
||||
- BGMApp/BGMAppTests/UITests/travis-skip.py
|
||||
# Run the tests.
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'Background Music Device' test
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'Background Music' test
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'BGMXPCHelper' test
|
||||
# Uninstall Background Music.
|
||||
- yes | ./uninstall.sh
|
||||
# Check the BGM dirs and files were removed.
|
||||
- if ls -la "/Applications/Background Music.app"; then false; fi
|
||||
- if ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver"; then false; fi
|
||||
- if ls -la "/usr/local/libexec/BGMXPCHelper.xpc"; then false; fi
|
||||
- if ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"; then false; fi
|
||||
- if ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"; then false; fi
|
||||
# Build the .pkg installer.
|
||||
- ./package.sh
|
||||
# Install the .pkg.
|
||||
- sudo installer -pkg Background-Music-*/BackgroundMusic-*.pkg -target / -verbose -dumplog
|
||||
# Check the BGM dirs and files were installed again.
|
||||
- ls -la "/Applications/Background Music.app"
|
||||
- ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver"
|
||||
- ls -la "/usr/local/libexec/BGMXPCHelper.xpc" || ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"
|
||||
- ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"
|
||||
# Post on IRC when Travis builds finish.
|
||||
notifications:
|
||||
irc: "irc.freenode.org#backgroundmusic"
|
||||
# Upload the .pkg and dSYM zip to GitHub.
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: j5GdMTkJI/9lfGMcAW4dnBnfNSW0EUGSuaKSXw49FfjfcshLL2RFxIbQkyA7QqjoJm6ohstU3tOCo7c9FrqIWjE/+5itGJpq7NXDRxFtd2qzcli1u+1IRvQUZJ4VYC9982pSS0IUynK9/f0rhbdkWsCuXWIjoClYPBRscc8soDBJvkDbfilPFfFgkc8TuSmtGDCdu9coGVi6b9HuTLNQU0g5DZkjmv71Vj3SwJ2CmvOk3GFfV1SjvG2SRgBDwyP1g9MRGRiNYkmK9lJRgsq2KLluzb04lt22x8RIcZ+kZYOQVmgDlCeWlOcXi0iz1wU/QzdoYFEAnJdG4q0hqKeqIi+p8Tc31nHPuc1ZlYpifzMQ6KuOoOP19eceJwriAT133t2RSB3Rl3nxh9bymNPNyQ2dJwGNFtO68f3aZsuE5L92lVgW/ipZ6e5Sw1ovXldR04mxNtyY4WvFXFlkn/776tKV0vgAubsHfceGM/aRoBj+E2gDvqkFqIR8wrZAZEeSM2reMHPMx5ICFppIZ8dCIVjF5bsxZQsbojY+LXV8BUU5kLAou0yD7Q+lHi9r3HYdN90+cC02HKGFYzsIiMAyf4IAngnLhwmmrLOwr3wWdACjYTJhznAZGNJh4lCeB4dx85iyj3EexJ6J/DL1k2+ZNKyMN3+i/215t+AvSsXuw5U=
|
||||
file_glob: true
|
||||
file: Background-Music-*/*
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: kyleneideck/BackgroundMusic
|
||||
condition: $DEPLOY = true
|
||||
|
||||
|
||||
Generated
+18
@@ -10,15 +10,33 @@
|
||||
<FileRef
|
||||
location = "group:README.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:MANUAL-INSTALL.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:MANUAL-UNINSTALL.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:TODO.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:DEVELOPING.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:CONTRIBUTING.md">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:LICENSE">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:LICENSE-Apple-Sample-Code">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:build_and_install.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:uninstall.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Images">
|
||||
</FileRef>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -32,9 +32,19 @@
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CB8B3481BBA75F0000E2DD1"
|
||||
BuildableName = "BGMAppTests.xctest"
|
||||
BlueprintName = "BGMAppTests"
|
||||
BlueprintIdentifier = "2743C9F51D86CFF90089613B"
|
||||
BuildableName = "BGMAppUnitTests.xctest"
|
||||
BlueprintName = "BGMAppUnitTests"
|
||||
ReferencedContainer = "container:BGMApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CCC4F531E584081008053E4"
|
||||
BuildableName = "BGMAppUITests.xctest"
|
||||
BlueprintName = "BGMAppUITests"
|
||||
ReferencedContainer = "container:BGMApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
@@ -59,6 +69,7 @@
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
enableAddressSanitizer = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
@@ -74,7 +85,7 @@
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "ASAN_OPTIONS"
|
||||
value = "detect_odr_violation=0"
|
||||
value = "detect_odr_violation=0,use_odr_indicator=1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// AppDelegate.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
#import "AppDelegate.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMAppVolumes.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMXPCListener.h"
|
||||
|
||||
|
||||
static float const kStatusBarIconPadding = 0.25;
|
||||
|
||||
@implementation AppDelegate {
|
||||
// The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main menu
|
||||
// for the app. These are called "menu bar extras" in the Human Interface Guidelines.
|
||||
NSStatusItem* statusBarItem;
|
||||
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMAppVolumes* appVolumes;
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
BGMPreferencesMenu* prefsMenu;
|
||||
BGMXPCListener* xpcListener;
|
||||
}
|
||||
|
||||
- (void) awakeFromNib {
|
||||
// Set up the status bar item
|
||||
statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||
|
||||
// Set the icon
|
||||
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
|
||||
if (icon != nil) {
|
||||
CGFloat lengthMinusPadding = [[statusBarItem button] frame].size.height * (1 - kStatusBarIconPadding);
|
||||
[icon setSize:NSMakeSize(lengthMinusPadding, lengthMinusPadding)];
|
||||
// Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or the status
|
||||
// bar's in dark mode
|
||||
[icon setTemplate:YES];
|
||||
statusBarItem.button.image = icon;
|
||||
} else {
|
||||
// If our icon is missing for some reason, fallback to a fermata character (1D110)
|
||||
statusBarItem.button.title = @"𝄐";
|
||||
}
|
||||
|
||||
// Set the main menu
|
||||
statusBarItem.menu = self.bgmMenu;
|
||||
}
|
||||
|
||||
- (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
|
||||
#pragma unused (aNotification)
|
||||
|
||||
// Set up the GUI and other external interfaces.
|
||||
|
||||
// Coordinates the audio devices (BGMDevice and the output device): manages playthrough, volume/mute controls, etc.
|
||||
NSError* err;
|
||||
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&err];
|
||||
if (audioDevices == nil) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:err.code];
|
||||
}
|
||||
[audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices];
|
||||
|
||||
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
|
||||
helperConnectionErrorHandler:^(NSError* error) {
|
||||
[self showXPCHelperErrorMessageAndExit:error];
|
||||
}];
|
||||
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithMenu:[self bgmMenu]
|
||||
appVolumeView:[self appVolumeView]
|
||||
audioDevices:audioDevices];
|
||||
|
||||
prefsMenu = [[BGMPreferencesMenu alloc] initWithbgmMenu:[self bgmMenu]
|
||||
audioDevices:audioDevices
|
||||
aboutPanel:[self aboutPanel]
|
||||
aboutPanelLicenseView:[self aboutPanelLicenseView]];
|
||||
|
||||
[self loadUserDefaults];
|
||||
}
|
||||
|
||||
- (void) loadUserDefaults {
|
||||
// Register the preference defaults. These are the preferences/state that only apply to BGMApp. The others are
|
||||
// persisted on BGMDriver.
|
||||
NSDictionary* appDefaults = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
|
||||
forKey:@"AutoPauseMusicEnabled"];
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
|
||||
|
||||
// Enable auto-pausing music if it's enabled in the user's preferences (which it is by default).
|
||||
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AutoPauseMusicEnabled"]) {
|
||||
[self toggleAutoPauseMusic:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showDeviceNotFoundErrorMessageAndExit:(NSInteger)code {
|
||||
// Show an error dialog and exit if either BGMDevice wasn't found on the system or we couldn't find any output devices
|
||||
|
||||
// NSAlert should only be used on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert new];
|
||||
|
||||
if (code == kBGMErrorCode_BGMDeviceNotFound) {
|
||||
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL and offer to install them if not. Also,
|
||||
// it would be nice if we could restart coreaudiod automatically (using launchd).
|
||||
[alert setMessageText:@"Could not find the Background Music virtual audio device."];
|
||||
[alert setInformativeText:@"Make sure you've installed Background Music.driver to /Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo killall coreaudiod\")."];
|
||||
} else if (code == kBGMErrorCode_OutputDeviceNotFound) {
|
||||
[alert setMessageText:@"Could not find an audio output device."];
|
||||
[alert setInformativeText:@"If you do have one installed, this is probably a bug. Sorry about that. Feel free to file an issue on GitHub."];
|
||||
}
|
||||
|
||||
[alert runModal];
|
||||
[NSApp terminate:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showXPCHelperErrorMessageAndExit:(NSError*)error {
|
||||
// NSAlert should only be used on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert new];
|
||||
|
||||
// TODO: Offer to install BGMXPCHelper if it's missing.
|
||||
[alert setMessageText:@"Error connecting to BGMXPCHelper."];
|
||||
[alert setInformativeText:[NSString stringWithFormat:@"%s%s%@ (%lu)",
|
||||
"Make sure you have BGMXPCHelper installed. There are instructions in the README.md file.",
|
||||
"\n\nDetails:\n",
|
||||
[error localizedDescription],
|
||||
[error code]]];
|
||||
|
||||
[alert runModal];
|
||||
[NSApp terminate:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) applicationWillTerminate:(NSNotification*)aNotification {
|
||||
#pragma unused (aNotification)
|
||||
[audioDevices unsetBGMDeviceAsOSDefault];
|
||||
}
|
||||
|
||||
- (IBAction) toggleAutoPauseMusic:(id)sender {
|
||||
#pragma unused (sender)
|
||||
|
||||
if (self.autoPauseMenuItem.state == NSOnState) {
|
||||
self.autoPauseMenuItem.state = NSOffState;
|
||||
[autoPauseMusic disable];
|
||||
} else {
|
||||
self.autoPauseMenuItem.state = NSOnState;
|
||||
[autoPauseMusic enable];
|
||||
}
|
||||
|
||||
// Persist the change in the user's preferences
|
||||
[[NSUserDefaults standardUserDefaults] setBool:(self.autoPauseMenuItem.state == NSOnState)
|
||||
forKey:@"AutoPauseMusicEnabled"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// AppDelegate.h
|
||||
// BGMAppDelegate.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
@@ -22,16 +22,22 @@
|
||||
// Sets up and tears down the app.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
@interface BGMAppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate>
|
||||
|
||||
@property (weak) IBOutlet NSMenu* bgmMenu;
|
||||
@property (weak) IBOutlet NSMenuItem* autoPauseMenuItem;
|
||||
@property (weak) IBOutlet NSView* appVolumeView;
|
||||
@property (weak) IBOutlet NSPanel* aboutPanel;
|
||||
@property (unsafe_unretained) IBOutlet NSTextView *aboutPanelLicenseView;
|
||||
@property (unsafe_unretained) IBOutlet NSTextView* aboutPanelLicenseView;
|
||||
@property (weak) IBOutlet NSMenuItem* autoPauseMenuItemUnwrapped;
|
||||
|
||||
@property (readonly) BGMAudioDeviceManager* audioDevices;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppDelegate.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
#import "BGMAppDelegate.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
#import "BGMAppVolumes.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "SystemPreferences.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static float const kStatusBarIconPadding = 0.25;
|
||||
|
||||
@implementation BGMAppDelegate {
|
||||
// The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main menu
|
||||
// for the app. These are called "menu bar extras" in the Human Interface Guidelines.
|
||||
NSStatusItem* statusBarItem;
|
||||
|
||||
// Only show the 'BGMXPCHelper is missing' error dialog once.
|
||||
BOOL haveShownXPCHelperErrorMessage;
|
||||
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMAutoPauseMenuItem* autoPauseMenuItem;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
BGMAppVolumes* appVolumes;
|
||||
BGMPreferencesMenu* prefsMenu;
|
||||
BGMXPCListener* xpcListener;
|
||||
}
|
||||
|
||||
@synthesize audioDevices = audioDevices;
|
||||
|
||||
- (void) awakeFromNib {
|
||||
// Show BGMApp in the dock, if the command-line option for that was passed. This is used by the UI tests.
|
||||
if ([NSProcessInfo.processInfo.arguments indexOfObject:@"--show-dock-icon"] != NSNotFound) {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
}
|
||||
|
||||
haveShownXPCHelperErrorMessage = NO;
|
||||
|
||||
[self initStatusBarItem];
|
||||
}
|
||||
|
||||
// Set up the status bar item. (The thing you click to show BGMApp's UI.)
|
||||
- (void) initStatusBarItem {
|
||||
statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||
|
||||
// Set the icon
|
||||
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
|
||||
|
||||
// NSStatusItem doesn't have the "button" property on OS X 10.9.
|
||||
BOOL buttonAvailable = (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10);
|
||||
|
||||
if (icon != nil) {
|
||||
NSRect statusBarItemFrame;
|
||||
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItemFrame = statusBarItem.button.frame;
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
// OS X 10.9 fallback. I haven't tested this (or anything else on 10.9).
|
||||
statusBarItemFrame = statusBarItem.view.frame;
|
||||
}
|
||||
|
||||
CGFloat lengthMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding);
|
||||
[icon setSize:NSMakeSize(lengthMinusPadding, lengthMinusPadding)];
|
||||
|
||||
// Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or the status
|
||||
// bar's in dark mode
|
||||
[icon setTemplate:YES];
|
||||
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.image = icon;
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
statusBarItem.image = icon;
|
||||
}
|
||||
} else {
|
||||
// If our icon is missing for some reason, fallback to a fermata character (1D110)
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.title = @"𝄐";
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
statusBarItem.title = @"𝄐";
|
||||
}
|
||||
}
|
||||
|
||||
// Set the main menu
|
||||
statusBarItem.menu = self.bgmMenu;
|
||||
}
|
||||
|
||||
- (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.
|
||||
|
||||
// audioDevices coordinates BGMDevice and the output device. It manages playthrough, volume/mute controls, etc.
|
||||
{
|
||||
NSError* error;
|
||||
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&error];
|
||||
if (audioDevices == nil) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:error.code];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set Background Music Device as your default audio device."
|
||||
informativeText:@"You might be able to set it yourself."];
|
||||
}
|
||||
}
|
||||
|
||||
BGMUserDefaults* userDefaults = [self createUserDefaults];
|
||||
|
||||
musicPlayers = [[BGMMusicPlayers alloc] initWithAudioDevices:audioDevices
|
||||
userDefaults:userDefaults];
|
||||
|
||||
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
|
||||
musicPlayers:musicPlayers];
|
||||
|
||||
autoPauseMenuItem = [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
autoPauseMusic:autoPauseMusic
|
||||
musicPlayers:musicPlayers
|
||||
userDefaults:userDefaults];
|
||||
|
||||
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
|
||||
helperConnectionErrorHandler:^(NSError* error) {
|
||||
NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) "
|
||||
"BGMXPCHelper connection error: %@",
|
||||
error);
|
||||
|
||||
[self showXPCHelperErrorMessage:error];
|
||||
}];
|
||||
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithMenu:self.bgmMenu
|
||||
appVolumeView:self.appVolumeView
|
||||
audioDevices:audioDevices];
|
||||
|
||||
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
|
||||
audioDevices:audioDevices
|
||||
musicPlayers:musicPlayers
|
||||
aboutPanel:self.aboutPanel
|
||||
aboutPanelLicenseView:self.aboutPanelLicenseView];
|
||||
|
||||
// Handle events about the main menu. (See the NSMenuDelegate methods below.)
|
||||
self.bgmMenu.delegate = self;
|
||||
}
|
||||
|
||||
- (BGMUserDefaults*) createUserDefaults {
|
||||
BOOL persistentDefaults = [NSProcessInfo.processInfo.arguments indexOfObject:@"--no-persistent-data"] == NSNotFound;
|
||||
NSUserDefaults* wrappedDefaults = persistentDefaults ? [NSUserDefaults standardUserDefaults] : nil;
|
||||
return [[BGMUserDefaults alloc] initWithDefaults:wrappedDefaults];
|
||||
}
|
||||
|
||||
- (void) applicationWillTerminate:(NSNotification*)aNotification {
|
||||
#pragma unused (aNotification)
|
||||
|
||||
DebugMsg("BGMAppDelegate::applicationWillTerminate");
|
||||
|
||||
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Failed to reset your system's audio output device."
|
||||
informativeText:@"You'll have to change it yourself to get audio working again."];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Error messages
|
||||
|
||||
- (void) showDeviceNotFoundErrorMessageAndExit:(NSInteger)code {
|
||||
// Show an error dialog and exit if either BGMDevice wasn't found on the system or we couldn't find any output devices
|
||||
|
||||
// NSAlert should only be used on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert new];
|
||||
|
||||
if (code == kBGMErrorCode_BGMDeviceNotFound) {
|
||||
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL and offer to install them if not. Also,
|
||||
// it would be nice if we could restart coreaudiod automatically (using launchd).
|
||||
[alert setMessageText:@"Could not find the Background Music virtual audio device."];
|
||||
[alert setInformativeText:@"Make sure you've installed Background Music.driver to /Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo killall coreaudiod\")."];
|
||||
} else if (code == kBGMErrorCode_OutputDeviceNotFound) {
|
||||
[alert setMessageText:@"Could not find an audio output device."];
|
||||
[alert setInformativeText:@"If you do have one installed, this is probably a bug. Sorry about that. Feel free to file an issue on GitHub."];
|
||||
}
|
||||
|
||||
[alert runModal];
|
||||
[NSApp terminate:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showXPCHelperErrorMessage:(NSError*)error {
|
||||
if (!haveShownXPCHelperErrorMessage) {
|
||||
haveShownXPCHelperErrorMessage = YES;
|
||||
|
||||
// NSAlert should only be used on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert new];
|
||||
|
||||
// TODO: Offer to install BGMXPCHelper if it's missing.
|
||||
// TODO: Show suppression button?
|
||||
[alert setMessageText:@"Error connecting to BGMXPCHelper."];
|
||||
[alert setInformativeText:[NSString stringWithFormat:@"%s%s%@ (%lu)",
|
||||
"Make sure you have BGMXPCHelper installed. There are instructions in the "
|
||||
"README.md file.\n\n"
|
||||
"Background Music might still work, but it won't work as well as it could.",
|
||||
"\n\nDetails:\n",
|
||||
[error localizedDescription],
|
||||
[error code]]];
|
||||
[alert runModal];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showSetDeviceAsDefaultError:(NSError*)error
|
||||
message:(NSString*)msg
|
||||
informativeText:(NSString*)info {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSLog(@"%@ %@ Error: %@", msg, info, error);
|
||||
|
||||
NSAlert* alert = [NSAlert alertWithError:error];
|
||||
alert.messageText = msg;
|
||||
alert.informativeText = info;
|
||||
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert addButtonWithTitle:@"Open Sound in System Preferences"];
|
||||
|
||||
NSModalResponse buttonClicked = [alert runModal];
|
||||
|
||||
if (buttonClicked != NSAlertFirstButtonReturn) { // 'OK' is the first button.
|
||||
[self openSysPrefsSoundOutput];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void) openSysPrefsSoundOutput {
|
||||
SystemPreferencesApplication* __nullable sysPrefs =
|
||||
[SBApplication applicationWithBundleIdentifier:@"com.apple.systempreferences"];
|
||||
|
||||
if (!sysPrefs) {
|
||||
NSLog(@"Could not open System Preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
// In System Preferences, go to the "Output" tab on the "Sound" pane.
|
||||
for (SystemPreferencesPane* pane : [sysPrefs panes]) {
|
||||
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: pane = %s", [pane.name UTF8String]);
|
||||
|
||||
if ([pane.id isEqualToString:@"com.apple.preference.sound"]) {
|
||||
sysPrefs.currentPane = pane;
|
||||
|
||||
for (SystemPreferencesAnchor* anchor : [pane anchors]) {
|
||||
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: anchor = %s", [anchor.name UTF8String]);
|
||||
|
||||
if ([[anchor.name lowercaseString] isEqualToString:@"output"]) {
|
||||
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: Showing Output in Sound pane.");
|
||||
|
||||
[anchor reveal];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bring System Preferences to the foreground.
|
||||
[sysPrefs activate];
|
||||
}
|
||||
|
||||
#pragma mark NSMenuDelegate
|
||||
|
||||
- (void) menuNeedsUpdate:(NSMenu*)menu {
|
||||
if ([menu isEqual:self.bgmMenu]) {
|
||||
[autoPauseMenuItem parentMenuNeedsUpdate];
|
||||
} else {
|
||||
DebugMsg("BGMAppDelegate::menuNeedsUpdate: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) menu:(NSMenu*)menu willHighlightItem:(NSMenuItem* __nullable)item {
|
||||
if ([menu isEqual:self.bgmMenu]) {
|
||||
[autoPauseMenuItem parentMenuItemWillHighlight:item];
|
||||
} else {
|
||||
DebugMsg("BGMAppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+233
-52
@@ -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,71 +75,88 @@ 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 setView: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) {
|
||||
// Only show apps that appear in the dock (at first)
|
||||
// TODO: Would it be better to only show apps that are registered as HAL clients?
|
||||
if ([app activationPolicy] != NSApplicationActivationPolicyRegular) continue;
|
||||
|
||||
// Don't show Finder
|
||||
if ([[app bundleIdentifier] isEqualTo:@"com.apple.finder"]) continue;
|
||||
if (app.activationPolicy != NSApplicationActivationPolicyRegular) continue;
|
||||
|
||||
#ifndef NS_BLOCK_ASSERTIONS // If assertions are enabled
|
||||
// Count how many apps we should add menu items for so we can check it at the end of the method
|
||||
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];
|
||||
for (NSView* subview in appVolItem.view.subviews) {
|
||||
if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) {
|
||||
[(NSView<BGMAppVolumeMenuItemSubview>*)subview setUpWithApp:app context:self menuItem:appVolItem];
|
||||
}
|
||||
}
|
||||
|
||||
// Store the NSRunningApplication object with the menu item so when the app closes we can find the item to remove it
|
||||
[appVolItem setRepresentedObject:app];
|
||||
appVolItem.representedObject = app;
|
||||
|
||||
// Set the slider to the volume for this app if we got one from the driver
|
||||
[self setVolumeOfMenuItem:appVolItem fromAppVolumes:appVolumesOnDevice];
|
||||
|
||||
|
||||
// NSMenuItem didn't implement NSAccessibility before OS X SDK 10.12.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101200 // MAC_OS_X_VERSION_10_12
|
||||
if ([appVolItem respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
// TODO: This doesn't show up in Accessibility Inspector for me. Not sure why.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
appVolItem.accessibilityTitle = [NSString stringWithFormat:@"%@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
#endif
|
||||
|
||||
[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
|
||||
@@ -138,7 +164,7 @@ static float const kSlidersSnapWithin = 5;
|
||||
NSMenuItem* item = [bgmMenu itemAtIndex:i];
|
||||
|
||||
for (NSRunningApplication* appToBeRemoved in apps) {
|
||||
NSRunningApplication* itemApp = [item representedObject];
|
||||
NSRunningApplication* itemApp = item.representedObject;
|
||||
|
||||
if ([itemApp isEqual:appToBeRemoved]) {
|
||||
[bgmMenu removeItem:item];
|
||||
@@ -153,7 +179,7 @@ static float const kSlidersSnapWithin = 5;
|
||||
- (void) setVolumeOfMenuItem:(NSMenuItem*)menuItem fromAppVolumes:(CACFArray&)appVolumes {
|
||||
// Set menuItem's volume slider to the volume of the app in appVolumes that menuItem represents
|
||||
// Leaves menuItem unchanged if it doesn't match any of the apps in appVolumes
|
||||
NSRunningApplication* representedApp = [menuItem representedObject];
|
||||
NSRunningApplication* representedApp = menuItem.representedObject;
|
||||
|
||||
for (UInt32 i = 0; i < appVolumes.GetNumberItems(); i++) {
|
||||
CACFDictionary appVolume(false);
|
||||
@@ -167,21 +193,76 @@ static float const kSlidersSnapWithin = 5;
|
||||
pid_t pid;
|
||||
appVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), pid);
|
||||
|
||||
if ([representedApp processIdentifier] == pid ||
|
||||
[[representedApp bundleIdentifier] isEqualToString:(__bridge NSString*)bundleID.GetCFString()]) {
|
||||
if ((representedApp.processIdentifier == pid) ||
|
||||
[representedApp.bundleIdentifier isEqualToString:(__bridge NSString*)bundleID.GetCFString()]) {
|
||||
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]) {
|
||||
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)
|
||||
@@ -214,6 +295,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);
|
||||
@@ -227,26 +310,70 @@ 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 setImage:[app icon]];
|
||||
self.image = app.icon;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@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)
|
||||
|
||||
[self setStringValue:[app localizedName]];
|
||||
NSString* name = app.localizedName ? (NSString*)app.localizedName : @"";
|
||||
self.stringValue = name;
|
||||
}
|
||||
|
||||
@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];
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = @"More options";
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -258,40 +385,94 @@ 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 setTarget:self];
|
||||
[self setAction:@selector(appVolumeChanged)];
|
||||
self.target = self;
|
||||
self.action = @selector(appVolumeChanged);
|
||||
|
||||
appProcessID = [app processIdentifier];
|
||||
appBundleID = [app bundleIdentifier];
|
||||
appProcessID = app.processIdentifier;
|
||||
appBundleID = app.bundleIdentifier;
|
||||
|
||||
[self setMaxValue:kAppRelativeVolumeMaxRawValue];
|
||||
[self setMinValue:kAppRelativeVolumeMinRawValue];
|
||||
self.maxValue = kAppRelativeVolumeMaxRawValue;
|
||||
self.minValue = kAppRelativeVolumeMinRawValue;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Volume for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if ([self floatValue] > (midPoint - kSlidersSnapWithin) && [self floatValue] < (midPoint + kSlidersSnapWithin)) {
|
||||
[self setFloatValue:midPoint];
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setRelativeVolume:(NSNumber*)relativeVolume {
|
||||
[self setIntValue:[relativeVolume intValue]];
|
||||
self.intValue = relativeVolume.intValue;
|
||||
[self snap];
|
||||
}
|
||||
|
||||
- (void) appVolumeChanged {
|
||||
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
|
||||
|
||||
DebugMsg("BGMAppVolumes::appVolumeChanged: App volume for %s changed to %d", [appBundleID UTF8String], [self intValue]);
|
||||
DebugMsg("BGMAppVolumes::appVolumeChanged: App volume for %s changed to %d", appBundleID.UTF8String, self.intValue);
|
||||
|
||||
[self snap];
|
||||
|
||||
[context sendVolumeChangeToBGMDevice:[self intValue] appProcessID:appProcessID appBundleID:appBundleID];
|
||||
[context sendVolumeChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Pan for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
- (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
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAudioDevice.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGMAudioDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
|
||||
// System Includes
|
||||
#include <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
|
||||
// master element."
|
||||
static const AudioObjectPropertyElement kMasterChannel = 0;
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMAudioDevice::BGMAudioDevice(AudioObjectID inAudioDevice)
|
||||
:
|
||||
CAHALAudioDevice(inAudioDevice)
|
||||
{
|
||||
}
|
||||
|
||||
BGMAudioDevice::BGMAudioDevice(CFStringRef inUID)
|
||||
:
|
||||
CAHALAudioDevice(inUID)
|
||||
{
|
||||
}
|
||||
|
||||
BGMAudioDevice::BGMAudioDevice(const CAHALAudioDevice& inDevice)
|
||||
:
|
||||
BGMAudioDevice(inDevice.GetObjectID())
|
||||
{
|
||||
};
|
||||
|
||||
BGMAudioDevice::~BGMAudioDevice()
|
||||
{
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::CanBeOutputDeviceInBGMApp() const
|
||||
{
|
||||
CFStringRef uid = CopyDeviceUID();
|
||||
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
|
||||
bool isNullDevice = CFEqual(uid, CFSTR(kBGMNullDeviceUID));
|
||||
CFRelease(uid);
|
||||
|
||||
bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0;
|
||||
bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
|
||||
|
||||
return !isBGMDevice && !isNullDevice && !IsHidden() && hasOutputChannels && canBeDefault;
|
||||
}
|
||||
|
||||
#pragma mark Available Controls
|
||||
|
||||
bool BGMAudioDevice::HasSettableMasterVolume(AudioObjectPropertyScope inScope) const
|
||||
{
|
||||
return HasVolumeControl(inScope, kMasterChannel) &&
|
||||
VolumeControlIsSettable(inScope, kMasterChannel);
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::HasSettableVirtualMasterVolume(AudioObjectPropertyScope inScope) const
|
||||
{
|
||||
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
|
||||
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
||||
inScope,
|
||||
kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
// TODO: Replace these calls deprecated AudioToolbox functions. There are more below.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
Boolean virtualMasterVolumeIsSettable;
|
||||
OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(),
|
||||
&virtualMasterVolumeAddress,
|
||||
&virtualMasterVolumeIsSettable);
|
||||
virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError);
|
||||
|
||||
bool hasVirtualMasterVolume =
|
||||
AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
return hasVirtualMasterVolume && virtualMasterVolumeIsSettable;
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::HasSettableMasterMute(AudioObjectPropertyScope inScope) const
|
||||
{
|
||||
return HasMuteControl(inScope, kMasterChannel) &&
|
||||
MuteControlIsSettable(inScope, kMasterChannel);
|
||||
}
|
||||
|
||||
#pragma mark Control Values Accessors
|
||||
|
||||
void BGMAudioDevice::CopyMuteFrom(const BGMAudioDevice inDevice,
|
||||
AudioObjectPropertyScope inScope)
|
||||
{
|
||||
// TODO: Support for devices that have per-channel mute controls but no master mute control
|
||||
if(HasSettableMasterMute(inScope) && inDevice.HasMuteControl(inScope, kMasterChannel))
|
||||
{
|
||||
SetMuteControlValue(inScope,
|
||||
kMasterChannel,
|
||||
inDevice.GetMuteControlValue(inScope, kMasterChannel));
|
||||
}
|
||||
}
|
||||
|
||||
void BGMAudioDevice::CopyVolumeFrom(const BGMAudioDevice inDevice,
|
||||
AudioObjectPropertyScope inScope)
|
||||
{
|
||||
// Get the volume of the other device.
|
||||
bool didGetVolume = false;
|
||||
Float32 volume = FLT_MIN;
|
||||
|
||||
if(inDevice.HasVolumeControl(inScope, kMasterChannel))
|
||||
{
|
||||
volume = inDevice.GetVolumeControlScalarValue(inScope, kMasterChannel);
|
||||
didGetVolume = true;
|
||||
}
|
||||
|
||||
// Use the average channel volume of the other device if it has no master volume.
|
||||
if(!didGetVolume)
|
||||
{
|
||||
UInt32 numChannels =
|
||||
inDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
|
||||
volume = 0;
|
||||
|
||||
for(UInt32 channel = 1; channel <= numChannels; channel++)
|
||||
{
|
||||
if(inDevice.HasVolumeControl(inScope, channel))
|
||||
{
|
||||
volume += inDevice.GetVolumeControlScalarValue(inScope, channel);
|
||||
didGetVolume = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(numChannels > 0) // Avoid divide by zero.
|
||||
{
|
||||
volume /= numChannels;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the volume of this device.
|
||||
if(didGetVolume && volume != FLT_MIN)
|
||||
{
|
||||
bool didSetVolume = false;
|
||||
|
||||
try
|
||||
{
|
||||
didSetVolume = SetMasterVolumeScalar(inScope, volume);
|
||||
}
|
||||
catch(CAException e)
|
||||
{
|
||||
OSStatus err = e.GetError();
|
||||
char err4CC[5] = CA4CCToCString(err);
|
||||
CFStringRef uid = CopyDeviceUID();
|
||||
LogWarning("BGMAudioDevice::CopyVolumeFrom: CAException '%s' trying to set master "
|
||||
"volume of %s",
|
||||
err4CC,
|
||||
CFStringGetCStringPtr(uid, kCFStringEncodingUTF8));
|
||||
CFRelease(uid);
|
||||
}
|
||||
|
||||
if(!didSetVolume)
|
||||
{
|
||||
// Couldn't find a master volume control to set, so try to find a virtual one
|
||||
Float32 virtualMasterVolume;
|
||||
bool success = inDevice.GetVirtualMasterVolumeScalar(inScope, virtualMasterVolume);
|
||||
if(success)
|
||||
{
|
||||
didSetVolume = SetVirtualMasterVolumeScalar(inScope, virtualMasterVolume);
|
||||
}
|
||||
}
|
||||
|
||||
if(!didSetVolume)
|
||||
{
|
||||
// Couldn't set a master or virtual master volume, so as a fallback try to set each
|
||||
// channel individually.
|
||||
UInt32 numChannels = GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
|
||||
for(UInt32 channel = 1; channel <= numChannels; channel++)
|
||||
{
|
||||
if(HasVolumeControl(inScope, channel) && VolumeControlIsSettable(inScope, channel))
|
||||
{
|
||||
SetVolumeControlScalarValue(inScope, channel, volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::SetMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume)
|
||||
{
|
||||
if(HasSettableMasterVolume(inScope))
|
||||
{
|
||||
SetVolumeControlScalarValue(inScope, kMasterChannel, inVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::GetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
|
||||
Float32& outVirtualMasterVolume) const
|
||||
{
|
||||
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
|
||||
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
||||
inScope,
|
||||
kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
|
||||
return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(),
|
||||
&virtualMasterVolumeAddress,
|
||||
&virtualMasterVolumePropertySize,
|
||||
&outVirtualMasterVolume);
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::SetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
|
||||
Float32 inVolume)
|
||||
{
|
||||
// TODO: For me, setting the virtual master volume sets all the device's channels to the same volume, meaning you can't
|
||||
// keep any channels quieter than the others. The expected behaviour is to scale the channel volumes
|
||||
// proportionally. So to do this properly I think we'd have to store BGMDevice's previous volume and calculate
|
||||
// each channel's new volume from its current volume and the distance between BGMDevice's old and new volumes.
|
||||
//
|
||||
// The docs kAudioHardwareServiceDeviceProperty_VirtualMasterVolume for say
|
||||
// "If the device has individual channel volume controls, this property will apply to those identified by the
|
||||
// device's preferred multi-channel layout (or preferred stereo pair if the device is stereo only). Note that
|
||||
// this control maintains the relative balance between all the channels it affects.
|
||||
// so I'm not sure why that's not working here. As a workaround we take the to device's (virtual master) balance
|
||||
// before changing the volume and set it back after, but of course that'll only work for stereo devices.
|
||||
|
||||
bool didSetVolume = false;
|
||||
|
||||
if(HasSettableVirtualMasterVolume(inScope))
|
||||
{
|
||||
// Not sure why, but setting the virtual master volume sets all channels to the same volume. As a workaround, we store
|
||||
// the current balance here so we can reset it after setting the volume.
|
||||
Float32 virtualMasterBalance;
|
||||
bool didGetVirtualMasterBalance = GetVirtualMasterBalance(inScope, virtualMasterBalance);
|
||||
|
||||
AudioObjectPropertyAddress virtualMasterVolumeAddress = {
|
||||
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
||||
inScope,
|
||||
kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
didSetVolume = (kAudioServicesNoError == AHSSetPropertyData(GetObjectID(),
|
||||
&virtualMasterVolumeAddress,
|
||||
sizeof(Float32),
|
||||
&inVolume));
|
||||
|
||||
// Reset the balance
|
||||
AudioObjectPropertyAddress virtualMasterBalanceAddress = {
|
||||
kAudioHardwareServiceDeviceProperty_VirtualMasterBalance,
|
||||
inScope,
|
||||
kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if(didSetVolume &&
|
||||
didGetVirtualMasterBalance &&
|
||||
AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress))
|
||||
{
|
||||
Boolean balanceIsSettable;
|
||||
OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(),
|
||||
&virtualMasterBalanceAddress,
|
||||
&balanceIsSettable);
|
||||
if(err == kAudioServicesNoError && balanceIsSettable)
|
||||
{
|
||||
AHSSetPropertyData(GetObjectID(),
|
||||
&virtualMasterBalanceAddress,
|
||||
sizeof(Float32),
|
||||
&virtualMasterBalance);
|
||||
}
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
return didSetVolume;
|
||||
}
|
||||
|
||||
bool BGMAudioDevice::GetVirtualMasterBalance(AudioObjectPropertyScope inScope,
|
||||
Float32& outVirtualMasterBalance) const
|
||||
{
|
||||
AudioObjectPropertyAddress virtualMasterBalanceAddress = {
|
||||
kAudioHardwareServiceDeviceProperty_VirtualMasterBalance,
|
||||
inScope,
|
||||
kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
|
||||
return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(),
|
||||
&virtualMasterBalanceAddress,
|
||||
&virtualMasterVolumePropertySize,
|
||||
&outVirtualMasterBalance);
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus BGMAudioDevice::AHSGetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32* ioDataSize,
|
||||
void* outData)
|
||||
{
|
||||
// The docs for AudioHardwareServiceGetPropertyData specifically allow passing NULL for
|
||||
// inQualifierData as we do here, but it's declared in an assume_nonnull section so we have to
|
||||
// disable the warning here. I'm not sure why inQualifierData isn't __nullable. I'm assuming
|
||||
// it's either a backwards compatibility thing or just a bug.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
// The non-depreciated version of this (and the setter below) doesn't seem to support devices
|
||||
// other than the default
|
||||
return AudioHardwareServiceGetPropertyData(inObjectID, inAddress, 0, NULL, ioDataSize, outData);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus BGMAudioDevice::AHSSetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32 inDataSize,
|
||||
const void* inData)
|
||||
{
|
||||
// See the explanation about these pragmas in AHSGetPropertyData
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return AudioHardwareServiceSetPropertyData(inObjectID, inAddress, 0, NULL, inDataSize, inData);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// BGMAudioDevice.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// A HAL audio device. Note that this class's only state is the AudioObjectID of the device.
|
||||
//
|
||||
|
||||
#ifndef BGMApp__BGMAudioDevice
|
||||
#define BGMApp__BGMAudioDevice
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioDevice.h"
|
||||
|
||||
|
||||
class BGMAudioDevice
|
||||
:
|
||||
public CAHALAudioDevice
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
BGMAudioDevice(AudioObjectID inAudioDevice);
|
||||
BGMAudioDevice(CFStringRef inUID);
|
||||
BGMAudioDevice(const CAHALAudioDevice& inDevice);
|
||||
virtual ~BGMAudioDevice();
|
||||
|
||||
#if defined(__OBJC__)
|
||||
|
||||
// Hack/workaround for Objective-C classes so we don't have to use pointers for instance
|
||||
// variables.
|
||||
BGMAudioDevice() : BGMAudioDevice(kAudioObjectUnknown) { }
|
||||
|
||||
#endif /* defined(__OBJC__) */
|
||||
|
||||
operator AudioObjectID() const { return GetObjectID(); }
|
||||
|
||||
/*! @throws CAException */
|
||||
bool CanBeOutputDeviceInBGMApp() const;
|
||||
|
||||
#pragma mark Available Controls
|
||||
|
||||
bool HasSettableMasterVolume(AudioObjectPropertyScope inScope) const;
|
||||
bool HasSettableVirtualMasterVolume(AudioObjectPropertyScope inScope) const;
|
||||
bool HasSettableMasterMute(AudioObjectPropertyScope inScope) const;
|
||||
|
||||
#pragma mark Control Values Accessors
|
||||
|
||||
void CopyMuteFrom(const BGMAudioDevice inDevice,
|
||||
AudioObjectPropertyScope inScope);
|
||||
void CopyVolumeFrom(const BGMAudioDevice inDevice,
|
||||
AudioObjectPropertyScope inScope);
|
||||
|
||||
bool SetMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume);
|
||||
|
||||
bool GetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
|
||||
Float32& outVirtualMasterVolume) const;
|
||||
bool SetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope,
|
||||
Float32 inVolume);
|
||||
|
||||
bool GetVirtualMasterBalance(AudioObjectPropertyScope inScope,
|
||||
Float32& outVirtualMasterBalance) const;
|
||||
|
||||
private:
|
||||
static OSStatus AHSGetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32* ioDataSize,
|
||||
void* outData);
|
||||
static OSStatus AHSSetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32 inDataSize,
|
||||
const void* inData);
|
||||
|
||||
};
|
||||
|
||||
#endif /* BGMApp__BGMAudioDevice */
|
||||
|
||||
@@ -19,38 +19,70 @@
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
// Manages the BGMDevice and the output device. Sets the system's current default device as the output device on init, then
|
||||
// starts playthrough and mirroring the devices' controls. The output device can be changed but the BGMDevice is fixed.
|
||||
// Manages the BGMDevice and the output device. Sets the system's current default device as the
|
||||
// output device on init, then starts playthrough and mirroring the devices' controls. The output
|
||||
// device can be changed but the BGMDevice is fixed.
|
||||
//
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioDevice.h"
|
||||
#ifdef __cplusplus
|
||||
#import "CAHALAudioDevice.h"
|
||||
#endif
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <CoreAudio/AudioHardwareBase.h>
|
||||
#import <CoreAudio/AudioHardwareBase.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
extern int const kBGMErrorCode_BGMDeviceNotFound;
|
||||
extern int const kBGMErrorCode_OutputDeviceNotFound;
|
||||
|
||||
@interface BGMAudioDeviceManager : NSObject
|
||||
|
||||
- (id) initWithError:(NSError**)error;
|
||||
- (instancetype) initWithError:(NSError**)error;
|
||||
|
||||
// Set BGMDevice as the default audio device for all processes
|
||||
- (void) setBGMDeviceAsOSDefault;
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
||||
// Replace BGMDevice as the default device with the output device
|
||||
- (void) unsetBGMDeviceAsOSDefault;
|
||||
- (NSError* __nullable) unsetBGMDeviceAsOSDefault;
|
||||
|
||||
#ifdef __cplusplus
|
||||
// The virtual device published by BGMDriver.
|
||||
- (CAHALAudioDevice) bgmDevice;
|
||||
|
||||
- (BOOL) isOutputDevice:(AudioObjectID)deviceID;
|
||||
// Returns NO if the output device couldn't be changed and has been reverted
|
||||
- (BOOL) setOutputDeviceWithID:(AudioObjectID)deviceID revertOnFailure:(BOOL)revertOnFailure;
|
||||
// The device BGMApp will play audio through, making it, from the user's perspective, the system's
|
||||
// default output device.
|
||||
- (CAHALAudioDevice) outputDevice;
|
||||
#endif
|
||||
|
||||
// Returns when IO has started running on the output device (for playthrough).
|
||||
- (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,
|
||||
// this method will attempt to set the output device back to the original device. If it fails to
|
||||
// revert, an additional error will be included in the error's userInfo with the key "revertError".
|
||||
//
|
||||
// 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;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAudioDeviceManager.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -25,8 +25,10 @@
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
#include "BGMDeviceControlSync.h"
|
||||
#include "BGMPlayThrough.h"
|
||||
#include "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
@@ -36,43 +38,44 @@
|
||||
int const kBGMErrorCode_BGMDeviceNotFound = 0;
|
||||
int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
|
||||
// Hack/workaround that adds a default constructor to CAHALAudioDevice so we don't have to use pointers for the instance variables
|
||||
class BGMAudioDevice : public CAHALAudioDevice {
|
||||
using CAHALAudioDevice::CAHALAudioDevice;
|
||||
public:
|
||||
BGMAudioDevice() : CAHALAudioDevice(kAudioDeviceUnknown) { }
|
||||
};
|
||||
|
||||
@implementation BGMAudioDeviceManager {
|
||||
BGMAudioDevice bgmDevice;
|
||||
BGMAudioDevice outputDevice;
|
||||
|
||||
BGMDeviceControlSync deviceControlSync;
|
||||
BGMPlayThrough playThrough;
|
||||
|
||||
NSRecursiveLock* stateLock;
|
||||
}
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
- (id) initWithError:(NSError**)error {
|
||||
if ((self = [super init])) {
|
||||
stateLock = [NSRecursiveLock new];
|
||||
|
||||
bgmDevice = BGMAudioDevice(CFSTR(kBGMDeviceUID));
|
||||
|
||||
if (bgmDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
DebugMsg("BGMAudioDeviceManager::initWithError: BGMDevice not found");
|
||||
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found");
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
[self initOutputDevice];
|
||||
|
||||
|
||||
if (outputDevice.GetObjectID() == kAudioDeviceUnknown) {
|
||||
DebugMsg("BGMAudioDeviceManager::initWithError: output device not found");
|
||||
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: output device not found");
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
@@ -85,6 +88,7 @@ public:
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
|
||||
AudioObjectID defaultDeviceID = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
|
||||
if (defaultDeviceID == bgmDevice.GetObjectID()) {
|
||||
// TODO: If BGMDevice is already the default (because BGMApp didn't shutdown properly or it was set manually)
|
||||
// we should temporarily disable BGMDevice so we can find out what the previous default was.
|
||||
@@ -94,6 +98,7 @@ public:
|
||||
if (numDevices > 0) {
|
||||
SInt32 minLatencyDeviceIdx = -1;
|
||||
UInt32 minLatency = UINT32_MAX;
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
@@ -115,16 +120,24 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
[self setOutputDeviceWithID:devices[minLatencyDeviceIdx] revertOnFailure:NO];
|
||||
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
|
||||
"setOutputDeviceWithID:devices[minLatencyDeviceIdx]", [&]() {
|
||||
// TODO: On error, try a different output device.
|
||||
[self setOutputDeviceWithID:devices[minLatencyDeviceIdx] revertOnFailure:NO];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
[self setOutputDeviceWithID:defaultDeviceID revertOnFailure:NO];
|
||||
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
|
||||
"setOutputDeviceWithID:defaultDeviceID", [&]() {
|
||||
// TODO: Return the error from setOutputDeviceWithID so it can be returned by initWithError.
|
||||
[self setOutputDeviceWithID:defaultDeviceID revertOnFailure:NO];
|
||||
});
|
||||
}
|
||||
|
||||
assert(outputDevice.GetObjectID() != bgmDevice.GetObjectID());
|
||||
|
||||
// Log message
|
||||
if (outputDevice.GetObjectID() == kAudioDeviceUnknown) {
|
||||
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
|
||||
DebugMsg("BGMAudioDeviceManager::initDevices: Set output device to %s",
|
||||
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
|
||||
@@ -134,33 +147,120 @@ public:
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
- (void) setBGMDeviceAsOSDefault {
|
||||
// Note that there are two different "default" output devices on OS X: "output" and "system output". See
|
||||
// AudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h.
|
||||
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault {
|
||||
DebugMsg("BGMAudioDeviceManager::setBGMDeviceAsOSDefault: Setting the system's default audio "
|
||||
"device to BGMDevice");
|
||||
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
@synchronized (self) {
|
||||
if (audioSystem.GetDefaultAudioDevice(false, true) == outputDevice.GetObjectID()) {
|
||||
// The default system device was the same as the default device, so change that as well
|
||||
audioSystem.SetDefaultAudioDevice(false, true, bgmDevice.GetObjectID());
|
||||
}
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, false, bgmDevice.GetObjectID());
|
||||
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
if (bgmDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
try {
|
||||
AudioDeviceID currentDefault = audioSystem.GetDefaultAudioDevice(false, true);
|
||||
|
||||
try {
|
||||
if (currentDefault == outputDeviceID) {
|
||||
// The default system device was the same as the default device, so change that as well
|
||||
audioSystem.SetDefaultAudioDevice(false, true, bgmDeviceID);
|
||||
}
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, false, bgmDeviceID);
|
||||
} catch (CAException e) {
|
||||
NSLog(@"SetDefaultAudioDevice threw CAException (%d)", e.GetError());
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
}
|
||||
} catch (...) {
|
||||
NSLog(@"Unexpected exception");
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:-1 userInfo:nil];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) unsetBGMDeviceAsOSDefault {
|
||||
- (NSError* __nullable) unsetBGMDeviceAsOSDefault {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
@synchronized (self) {
|
||||
if (audioSystem.GetDefaultAudioDevice(false, true) == bgmDevice.GetObjectID()) {
|
||||
// We changed the system output device to BGMDevice, which we only do if it initially matches the
|
||||
// default output device, so change it back
|
||||
audioSystem.SetDefaultAudioDevice(false, true, outputDevice.GetObjectID());
|
||||
}
|
||||
bool bgmDeviceIsDefault = true;
|
||||
bool bgmDeviceIsSystemDefault = true;
|
||||
|
||||
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
|
||||
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&]() {
|
||||
bgmDeviceIsDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, false) == bgmDeviceID);
|
||||
|
||||
bgmDeviceIsSystemDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, true) == bgmDeviceID);
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
if (bgmDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
if (bgmDeviceIsDefault) {
|
||||
DebugMsg("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault: Setting the system's default output "
|
||||
"device back to device %d", outputDeviceID);
|
||||
|
||||
if (audioSystem.GetDefaultAudioDevice(false, false) == bgmDevice.GetObjectID()) {
|
||||
audioSystem.SetDefaultAudioDevice(false, false, outputDevice.GetObjectID());
|
||||
try {
|
||||
audioSystem.SetDefaultAudioDevice(false, false, outputDeviceID);
|
||||
} catch (CAException e) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
} catch (...) {
|
||||
BGMLogUnexpectedExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault "
|
||||
"SetDefaultAudioDevice (output)");
|
||||
}
|
||||
}
|
||||
|
||||
// If we changed the default system output device to BGMDevice, which we only do if it's set to
|
||||
// the same device as the default output device, change it back to the previous device.
|
||||
if (bgmDeviceIsSystemDefault) {
|
||||
DebugMsg("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault: Setting the system's default system "
|
||||
"output device back to device %d", outputDeviceID);
|
||||
|
||||
try {
|
||||
audioSystem.SetDefaultAudioDevice(false, true, outputDeviceID);
|
||||
} catch (CAException e) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
} catch (...) {
|
||||
BGMLogUnexpectedExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault "
|
||||
"SetDefaultAudioDevice (system output)");
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
@@ -169,62 +269,216 @@ public:
|
||||
return bgmDevice;
|
||||
}
|
||||
|
||||
- (CAHALAudioDevice) outputDevice {
|
||||
return outputDevice;
|
||||
}
|
||||
|
||||
- (BOOL) isOutputDevice:(AudioObjectID)deviceID {
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
return deviceID == outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL) setOutputDeviceWithID:(AudioObjectID)deviceID revertOnFailure:(BOOL)revertOnFailure {
|
||||
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting output device. deviceID=%u", deviceID);
|
||||
- (BOOL) isOutputDataSource:(UInt32)dataSourceID {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
try {
|
||||
AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
|
||||
UInt32 channel = 0;
|
||||
|
||||
return outputDevice.HasDataSourceControl(scope, channel) &&
|
||||
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
|
||||
} catch (CAException e) {
|
||||
BGMLogException(e);
|
||||
return false;
|
||||
}
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (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(); // (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);
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
try {
|
||||
// Re-read the device ID after entering the monitor. (The initial read is because
|
||||
// currentDeviceID is 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) {
|
||||
// Deactivate playthrough rather than stopping it so it can't be started by HAL
|
||||
// notifications while we're updating deviceControlSync.
|
||||
playThrough.Deactivate();
|
||||
|
||||
outputDevice = BGMAudioDevice(deviceID);
|
||||
deviceControlSync.SetDevices(bgmDevice, newOutputDevice);
|
||||
deviceControlSync.Activate();
|
||||
|
||||
// Stream audio from BGMDevice to the new output device. This blocks while the old device
|
||||
// stops IO.
|
||||
playThrough.SetDevices(&bgmDevice, &newOutputDevice);
|
||||
playThrough.Activate();
|
||||
|
||||
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();
|
||||
}
|
||||
} catch (CAException e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:e.GetError()
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
|
||||
// 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();
|
||||
} catch (CAException e) {
|
||||
// Using LogWarning from PublicUtility instead of NSLog here crashes from a bad access. Not sure why.
|
||||
NSLog(@"BGMAudioDeviceManager::setOutputDeviceWithID: Couldn't set device with ID %u as output device. %s %s%d.",
|
||||
newOutputDevice.GetObjectID(),
|
||||
(revertOnFailure ? "Will attempt to revert to the previous device." : ""),
|
||||
"Error: ", e.GetError());
|
||||
|
||||
if (revertOnFailure) {
|
||||
// Try to reactivate the original device listener and playthrough
|
||||
[self setOutputDeviceWithID:outputDevice.GetObjectID() revertOnFailure:NO];
|
||||
return NO;
|
||||
} else {
|
||||
// TODO: Handle in callers. (Maybe show an error dialog and try to set the original default device as the
|
||||
// output device.)
|
||||
Throw(e);
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
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 {
|
||||
// Using LogWarning from PublicUtility instead of NSLog here crashes from a bad access. Not sure why.
|
||||
NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Couldn't set device with ID %u as output device. "
|
||||
"%s%d. %@",
|
||||
deviceID,
|
||||
"Error: ", errorCode,
|
||||
(revertTo ? [NSString stringWithFormat:@"Will attempt to revert to the previous device. "
|
||||
"Previous device ID: %u.", *revertTo] : @""));
|
||||
|
||||
NSDictionary* __nullable info = nil;
|
||||
|
||||
if (revertTo) {
|
||||
// Try to reactivate the original device listener and playthrough. (Sorry about the mutual recursion.)
|
||||
NSError* __nullable revertError = [self setOutputDeviceWithID:*revertTo revertOnFailure:NO];
|
||||
|
||||
if (revertError) {
|
||||
info = @{ @"revertError": (NSError*)revertError };
|
||||
}
|
||||
} else {
|
||||
// TODO: Handle this error better in callers. Maybe show an error dialog and try to set the original
|
||||
// default device as the output device.
|
||||
NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Failed to revert to the previous device.");
|
||||
}
|
||||
|
||||
return YES;
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:errorCode userInfo:info];
|
||||
}
|
||||
|
||||
- (OSStatus) waitForOutputDeviceToStart {
|
||||
@synchronized (self) {
|
||||
return playThrough.WaitForOutputDeviceToStart();
|
||||
// We can only try for stateLock because setOutputDeviceWithID might have already taken it, then made a
|
||||
// HAL request to BGMDevice and is now waiting for the response. Some of the requests setOutputDeviceWithID
|
||||
// makes to BGMDevice block in the HAL if another thread is in BGM_Device::StartIO.
|
||||
//
|
||||
// Since BGM_Device::StartIO calls this method (via XPC), waiting for setOutputDeviceWithID to release
|
||||
// stateLock could cause deadlocks. Instead we return early with an error code that BGMDriver knows to
|
||||
// ignore, since the output device is (almost certainly) being changed and we can't avoid dropping frames
|
||||
// while the output device starts up.
|
||||
OSStatus err;
|
||||
BOOL gotLock;
|
||||
|
||||
@try {
|
||||
gotLock = [stateLock tryLock];
|
||||
|
||||
if (gotLock) {
|
||||
err = playThrough.WaitForOutputDeviceToStart();
|
||||
} else {
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Didn't get state lock. Returning "
|
||||
"early with kDeviceNotStarting.");
|
||||
err = BGMPlayThrough::kDeviceNotStarting;
|
||||
}
|
||||
|
||||
if (err == BGMPlayThrough::kDeviceNotStarting) {
|
||||
// I'm not sure if this block is currently reachable, but BGMDriver only starts waiting on the
|
||||
// output device when IO is starting, so we should start playthrough even if BGMApp hasn't been
|
||||
// notified by the HAL yet.
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Playthrough wasn't starting the "
|
||||
"output device. Will tell it to and then return early with kDeviceNotStarting.");
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::waitForOutputDeviceToStart", [&]() {
|
||||
playThrough.Start();
|
||||
playThrough.StopIfIdle();
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
});
|
||||
}
|
||||
} @finally {
|
||||
if (gotLock) {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAutoPauseMenuItem.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAutoPauseMenuItem : NSObject
|
||||
|
||||
- (instancetype) initWithMenuItem:(NSMenuItem*)item
|
||||
autoPauseMusic:(BGMAutoPauseMusic*)autoPause
|
||||
musicPlayers:(BGMMusicPlayers*)players
|
||||
userDefaults:(BGMUserDefaults*)defaults;
|
||||
|
||||
// Handle events passed along by the delegate (NSMenuDelegate) of the menu containing this menu item.
|
||||
- (void) parentMenuNeedsUpdate;
|
||||
- (void) parentMenuItemWillHighlight:(NSMenuItem* __nullable)item;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAutoPauseMenuItem.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static NSString* const kMenuItemTitleFormat = @"Auto-pause %@";
|
||||
static NSString* const kMenuItemDisabledToolTipFormat = @"%@ doesn't appear to be running.";
|
||||
|
||||
// Wait time to disable/enable the auto-pause menu item, in seconds.
|
||||
static SInt64 const kMenuItemUpdateWaitTime = 1;
|
||||
|
||||
@implementation BGMAutoPauseMenuItem {
|
||||
BGMUserDefaults* userDefaults;
|
||||
NSMenuItem* menuItem;
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
id<NSObject> didLaunchToken, didTerminateToken;
|
||||
}
|
||||
|
||||
- (instancetype) initWithMenuItem:(NSMenuItem*)item
|
||||
autoPauseMusic:(BGMAutoPauseMusic*)autoPause
|
||||
musicPlayers:(BGMMusicPlayers*)players
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
if ((self = [super init])) {
|
||||
menuItem = item;
|
||||
autoPauseMusic = autoPause;
|
||||
musicPlayers = players;
|
||||
userDefaults = defaults;
|
||||
|
||||
// Enable/disable auto-pause to match the user's preferences setting.
|
||||
if (userDefaults.autoPauseMusicEnabled) {
|
||||
menuItem.state = NSOnState;
|
||||
[autoPauseMusic enable];
|
||||
} else {
|
||||
menuItem.state = NSOffState;
|
||||
[autoPauseMusic disable];
|
||||
}
|
||||
|
||||
// Toggle auto-pause when the menu item is clicked.
|
||||
menuItem.target = self;
|
||||
menuItem.action = @selector(toggleAutoPauseMusic);
|
||||
|
||||
[self updateMenuItemTitle];
|
||||
[self initMusicPlayerObservers];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initMusicPlayerObservers {
|
||||
// Add observers that enable/disable the Auto-pause Music menu item when the music player is launched/terminated.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
id<NSObject> (^addObserver)(NSString*) = ^(NSString* name) {
|
||||
return [center addObserverForName:name
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
NSString* appBundleID = [note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier];
|
||||
BOOL isAboutThisMusicPlayer = musicPlayers.selectedMusicPlayer.bundleID &&
|
||||
[appBundleID isEqualToString:(NSString*)musicPlayers.selectedMusicPlayer.bundleID];
|
||||
|
||||
if (isAboutThisMusicPlayer) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
kMenuItemUpdateWaitTime * NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
[self updateMenuItemTitle];
|
||||
});
|
||||
}
|
||||
}];
|
||||
};
|
||||
|
||||
didLaunchToken = addObserver(NSWorkspaceDidLaunchApplicationNotification);
|
||||
didTerminateToken = addObserver(NSWorkspaceDidTerminateApplicationNotification);
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) toggleAutoPauseMusic {
|
||||
// The menu item was clicked.
|
||||
|
||||
if (menuItem.state == NSOnState) {
|
||||
menuItem.state = NSOffState;
|
||||
[autoPauseMusic disable];
|
||||
} else {
|
||||
menuItem.state = NSOnState;
|
||||
[autoPauseMusic enable];
|
||||
}
|
||||
|
||||
// Persist the change in the user's preferences.
|
||||
userDefaults.autoPauseMusicEnabled = (menuItem.state == NSOnState);
|
||||
}
|
||||
|
||||
- (void) updateMenuItemTitle {
|
||||
[self updateMenuItemTitleWithHighlight:menuItem.isHighlighted];
|
||||
}
|
||||
|
||||
- (void) updateMenuItemTitleWithHighlight:(BOOL)highlight {
|
||||
// Set the title of the Auto-pause Music menu item, including the name of the selected music player.
|
||||
NSString* musicPlayerName = musicPlayers.selectedMusicPlayer.name;
|
||||
menuItem.title = [NSString stringWithFormat:kMenuItemTitleFormat, musicPlayerName];
|
||||
|
||||
// Make the Auto-pause Music menu item appear disabled if the application is not running.
|
||||
//
|
||||
// We don't actually disable it just in case the user decides to disable auto-pause and their music player isn't
|
||||
// running. E.g. someone who only recently installed Background Music and doesn't want to use auto-pause at all.
|
||||
if (musicPlayers.selectedMusicPlayer.isRunning) {
|
||||
menuItem.attributedTitle = nil;
|
||||
menuItem.toolTip = nil;
|
||||
} else {
|
||||
// Hardcode the text colour grey to match disabled menu items (unless the menu item is highlighted, in which
|
||||
// case use white).
|
||||
//
|
||||
// I couldn't figure out a way to do this without hardcoding the colours. There's no colour constant for this,
|
||||
// except possibly disabledControlTextColor, which just leaves the text black for me. I also couldn't get the
|
||||
// colours from the built-in NSColorLists.
|
||||
//
|
||||
// TODO: Can we make the tick mark grey as well?
|
||||
NSString* __nullable appleInterfaceStyle =
|
||||
[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
|
||||
BOOL darkMode = [appleInterfaceStyle isEqualToString:@"Dark"];
|
||||
NSColor* textColor = [NSColor colorWithHue:0
|
||||
saturation:0
|
||||
brightness:(highlight ? 1 : (darkMode ? 0.25 : 0.75))
|
||||
alpha:1];
|
||||
|
||||
NSDictionary* attributes = @{ NSFontAttributeName: [NSFont menuBarFontOfSize:0], // Default font size
|
||||
NSForegroundColorAttributeName: textColor };
|
||||
NSAttributedString* pseudoDisabledTitle = [[NSAttributedString alloc] initWithString:menuItem.title
|
||||
attributes:attributes];
|
||||
menuItem.attributedTitle = pseudoDisabledTitle;
|
||||
|
||||
menuItem.toolTip = [NSString stringWithFormat:kMenuItemDisabledToolTipFormat, musicPlayerName];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Parent menu events
|
||||
|
||||
- (void) parentMenuNeedsUpdate {
|
||||
[self updateMenuItemTitle];
|
||||
}
|
||||
|
||||
- (void) parentMenuItemWillHighlight:(NSMenuItem* __nullable)item {
|
||||
// Used to make the auto-pause menu item's text white when it's highlighted and change it back after.
|
||||
//
|
||||
// TODO: If you click the auto-pause menu item while it's disabled, it will initially appear highlighted next time
|
||||
// you open the main menu.
|
||||
|
||||
// If item is nil or any other menu item, the auto-pause menu item will be unhighlighted.
|
||||
BOOL willHighlightMenuItem = [item isEqual:menuItem];
|
||||
|
||||
// Only update the menu item if it's changing (from highlighted to unhighlighted or vice versa) to save a little
|
||||
// CPU.
|
||||
if (willHighlightMenuItem != menuItem.highlighted) {
|
||||
[self updateMenuItemTitleWithHighlight:willHighlightMenuItem];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -25,16 +25,22 @@
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAutoPauseMusic : NSObject
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices;
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers;
|
||||
|
||||
- (void) enable;
|
||||
- (void) disable;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAutoPauseMusic.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -27,6 +27,9 @@
|
||||
#include "BGM_Types.h"
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// STL Includes
|
||||
#import <algorithm> // std::max, std::min
|
||||
|
||||
// System Includes
|
||||
#include <CoreAudio/AudioHardware.h>
|
||||
#include <mach/mach_time.h>
|
||||
@@ -36,34 +39,63 @@
|
||||
// and other audio can have short periods of silence without causing music to play and quickly pause again. Of course, it's a
|
||||
// trade-off against how long the music will overlap the other audio before it gets paused and how long the music will stay paused
|
||||
// after a sound that was only slightly longer than the pause delay.
|
||||
static UInt64 const kPauseDelayNSec = 1500 * NSEC_PER_MSEC;
|
||||
// The delay before unpausing the music player is proportional to how long we paused it for, bounded by these limits. This makes it
|
||||
// a bit less annoying when a sound is just long enough to cause an auto-pause.
|
||||
//
|
||||
// TODO: Make these settable in advanced settings?
|
||||
static int const kPauseDelayMSecs = 1500;
|
||||
static int const kUnpauseDelayMSecs = 3000;
|
||||
// I haven't spent much time experimenting with different values for these constants, so they could probably be improved a fair
|
||||
// bit.
|
||||
//
|
||||
// TODO: Would it be worth listening for kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp so we can unpause
|
||||
// immediately if we haven't been paused for long and the non-music-player client stops IO? That would usually indicate that
|
||||
// it doesn't intend to start playing audio again soon. We'd also have to deal with music players that don't stop IO when
|
||||
// they're paused.
|
||||
static UInt64 const kMaxUnpauseDelayNSec = 3000 * NSEC_PER_MSEC;
|
||||
static UInt64 const kMinUnpauseDelayNSec = kMaxUnpauseDelayNSec / 10;
|
||||
// We multiply the time spent paused by this factor to calculate the delay before we consider unpausing.
|
||||
static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
|
||||
|
||||
@implementation BGMAutoPauseMusic {
|
||||
BOOL enabled;
|
||||
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
|
||||
dispatch_queue_t listenerQueue;
|
||||
// Have to keep track of the listener block we add so we can remove it later
|
||||
// Have to keep track of the listener block we add so we can remove it later.
|
||||
AudioObjectPropertyListenerBlock listenerBlock;
|
||||
// True if BGMApp has paused musicPlayer and hasn't unpaused it yet. (Will be out of sync with the music player app if the user
|
||||
// has unpaused it themselves.)
|
||||
|
||||
dispatch_queue_t pauseUnpauseMusicQueue;
|
||||
|
||||
// True if BGMApp has paused musicPlayer and hasn't unpaused it yet. (Will be out of sync with the music player app if the
|
||||
// user has unpaused it themselves.)
|
||||
BOOL wePaused;
|
||||
// The times, in absolute time, that the BGMDevice last changed its audible state to silent...
|
||||
UInt64 wentSilent;
|
||||
// ...and to audible
|
||||
// ...and to audible.
|
||||
UInt64 wentAudible;
|
||||
dispatch_queue_t pauseUnpauseMusicQueue;
|
||||
}
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices {
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers {
|
||||
if ((self = [super init])) {
|
||||
enabled = NO;
|
||||
audioDevices = inAudioDevices;
|
||||
musicPlayers = inMusicPlayers;
|
||||
|
||||
enabled = NO;
|
||||
wePaused = NO;
|
||||
|
||||
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
|
||||
dispatch_queue_attr_t attr;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
if (&dispatch_queue_attr_make_with_qos_class) {
|
||||
attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
|
||||
} else {
|
||||
// OS X 10.9 fallback
|
||||
attr = DISPATCH_QUEUE_SERIAL;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
listenerQueue = dispatch_queue_create("com.bearisdriving.BGM.AutoPauseMusic.Listener", attr);
|
||||
pauseUnpauseMusicQueue = dispatch_queue_create("com.bearisdriving.BGM.AutoPauseMusic.PauseUnpauseMusic", attr);
|
||||
|
||||
@@ -94,6 +126,8 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
audibleStateStr);
|
||||
#endif
|
||||
|
||||
// TODO: We shouldn't assume this block will only get called when BGMDevice's audible state changes. (Even if
|
||||
// the Core Audio docs did specify that, there's no reason not to be fault tolerant.)
|
||||
if (audibleState == kBGMDeviceIsAudible) {
|
||||
[weakSelf queuePauseBlock];
|
||||
} else if (audibleState == kBGMDeviceIsSilent) {
|
||||
@@ -112,9 +146,12 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
|
||||
- (SInt32) deviceAudibleState {
|
||||
SInt32 audibleState;
|
||||
CFNumberRef audibleStateRef = static_cast<CFNumberRef>([audioDevices bgmDevice].GetPropertyData_CFType(kBGMAudibleStateAddress));
|
||||
CFNumberRef audibleStateRef =
|
||||
static_cast<CFNumberRef>([audioDevices bgmDevice].GetPropertyData_CFType(kBGMAudibleStateAddress));
|
||||
|
||||
CFNumberGetValue(audibleStateRef, kCFNumberSInt32Type, &audibleState);
|
||||
CFRelease(audibleStateRef);
|
||||
|
||||
return audibleState;
|
||||
}
|
||||
|
||||
@@ -124,10 +161,10 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
UInt64 startedPauseDelay = now;
|
||||
|
||||
DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Dispatching pause block at %llu", now);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kPauseDelayMSecs * NSEC_PER_MSEC),
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kPauseDelayNSec),
|
||||
pauseUnpauseMusicQueue,
|
||||
^{
|
||||
BOOL stillAudible = [self deviceAudibleState] == kBGMDeviceIsAudible;
|
||||
BOOL stillAudible = ([self deviceAudibleState] == kBGMDeviceIsAudible);
|
||||
|
||||
DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Running pause block dispatched at %llu.%s wentAudible=%llu",
|
||||
startedPauseDelay,
|
||||
@@ -137,8 +174,8 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
// Pause if this is the most recent pause block and the device is still audible, which means the audible
|
||||
// state hasn't changed since this block was queued. Also set wePaused to true if the player wasn't
|
||||
// already paused.
|
||||
if (!wePaused && startedPauseDelay == wentAudible && stillAudible) {
|
||||
wePaused = [[BGMMusicPlayer selectedMusicPlayer] pause] || wePaused;
|
||||
if (!wePaused && (startedPauseDelay == wentAudible) && stillAudible) {
|
||||
wePaused = ([musicPlayers.selectedMusicPlayer pause] || wePaused);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -148,11 +185,31 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
wentSilent = now;
|
||||
UInt64 startedUnpauseDelay = now;
|
||||
|
||||
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Dispatched unpause block at %llu", now);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kUnpauseDelayMSecs * NSEC_PER_MSEC),
|
||||
// Unpause sooner if we've only been paused for a short time. This is so a notification sound causing an auto-pause is
|
||||
// less of an interruption.
|
||||
//
|
||||
// TODO: Would it help much if we ignored all audio played on the "system default" device rather than the "default"
|
||||
// device? IIRC apps are supposed to use the former for UI sounds.
|
||||
UInt64 unpauseDelayNsec =
|
||||
static_cast<UInt64>((wentSilent - wentAudible) * kUnpauseDelayWeightingFactor);
|
||||
|
||||
// Convert from absolute time to nanos.
|
||||
mach_timebase_info_data_t info;
|
||||
mach_timebase_info(&info);
|
||||
unpauseDelayNsec = unpauseDelayNsec * info.numer / info.denom;
|
||||
|
||||
// Clamp.
|
||||
unpauseDelayNsec = std::min(kMaxUnpauseDelayNSec, unpauseDelayNsec);
|
||||
unpauseDelayNsec = std::max(kMinUnpauseDelayNSec, unpauseDelayNsec);
|
||||
|
||||
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Dispatched unpause block at %llu. unpauseDelayNsec=%llu",
|
||||
now,
|
||||
unpauseDelayNsec);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, unpauseDelayNsec),
|
||||
pauseUnpauseMusicQueue,
|
||||
^{
|
||||
BOOL stillSilent = [self deviceAudibleState] == kBGMDeviceIsSilent;
|
||||
BOOL stillSilent = ([self deviceAudibleState] == kBGMDeviceIsSilent);
|
||||
|
||||
DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu.%s%s wentSilent=%llu",
|
||||
startedUnpauseDelay,
|
||||
@@ -162,9 +219,9 @@ static int const kUnpauseDelayMSecs = 3000;
|
||||
|
||||
// Unpause if we were the one who paused. Also check that this is the most recent unpause block and the
|
||||
// device is still silent, which means the audible state hasn't changed since this block was queued.
|
||||
if (wePaused && startedUnpauseDelay == wentSilent && stillSilent) {
|
||||
if (wePaused && (startedUnpauseDelay == wentSilent) && stillSilent) {
|
||||
wePaused = NO;
|
||||
[[BGMMusicPlayer selectedMusicPlayer] unpause];
|
||||
[musicPlayers.selectedMusicPlayer unpause];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMDeviceControlSync.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -25,13 +25,13 @@
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// System Includes
|
||||
#include <AudioToolbox/AudioServices.h>
|
||||
// PublicUtility Includes
|
||||
#include "CAPropertyAddress.h"
|
||||
|
||||
|
||||
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the master element."
|
||||
static const AudioObjectPropertyElement kMasterChannel = 0;
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static const AudioObjectPropertyAddress kMutePropertyAddress =
|
||||
{ kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster };
|
||||
@@ -41,31 +41,55 @@ static const AudioObjectPropertyAddress kVolumePropertyAddress =
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMDeviceControlSync::BGMDeviceControlSync(CAHALAudioDevice inBGMDevice, CAHALAudioDevice inOutputDevice)
|
||||
BGMDeviceControlSync::BGMDeviceControlSync(AudioObjectID inBGMDevice,
|
||||
AudioObjectID inOutputDevice,
|
||||
CAHALAudioSystemObject inAudioSystem)
|
||||
:
|
||||
mBGMDevice(inBGMDevice),
|
||||
mOutputDevice(inOutputDevice)
|
||||
mOutputDevice(inOutputDevice),
|
||||
mAudioSystem(inAudioSystem),
|
||||
mBGMDeviceControlsList(inBGMDevice)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
|
||||
BGMDeviceControlSync::~BGMDeviceControlSync()
|
||||
{
|
||||
Deactivate();
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlSync::~BGMDeviceControlSync", [&] {
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
Deactivate();
|
||||
});
|
||||
}
|
||||
|
||||
void BGMDeviceControlSync::Activate()
|
||||
{
|
||||
ThrowIf((mBGMDevice.GetObjectID() == kAudioDeviceUnknown || mOutputDevice.GetObjectID() == kAudioDeviceUnknown),
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
ThrowIf((mBGMDevice.GetObjectID() == kAudioObjectUnknown || mOutputDevice.GetObjectID() == kAudioObjectUnknown),
|
||||
BGM_DeviceNotSetException(),
|
||||
"BGMDeviceControlSync::Activate: Both the output device and BGMDevice must be set to start synchronizing their controls");
|
||||
|
||||
// Init BGMDevice controls to match output device
|
||||
CopyVolume(mOutputDevice, mBGMDevice, kAudioObjectPropertyScopeOutput);
|
||||
CopyMute(mOutputDevice, mBGMDevice, kAudioObjectPropertyScopeOutput);
|
||||
|
||||
|
||||
if(!mActive)
|
||||
{
|
||||
DebugMsg("BGMDeviceControlSync::Activate: Activating control sync");
|
||||
|
||||
// Disable BGMDevice controls that the output device doesn't have and reenable any that were
|
||||
// disabled for the previous output device.
|
||||
//
|
||||
// Continue anyway if this fails because it's better to have extra/missing controls than to
|
||||
// be unable to use the device.
|
||||
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlSync::Activate", "Controls list", [&] {
|
||||
bool wasUpdated = mBGMDeviceControlsList.MatchControlsListOf(mOutputDevice);
|
||||
if(wasUpdated)
|
||||
{
|
||||
mBGMDeviceControlsList.PropagateControlListChange();
|
||||
}
|
||||
});
|
||||
|
||||
// Init BGMDevice controls to match output device
|
||||
mBGMDevice.CopyVolumeFrom(mOutputDevice, kAudioObjectPropertyScopeOutput);
|
||||
mBGMDevice.CopyMuteFrom(mOutputDevice, kAudioObjectPropertyScopeOutput);
|
||||
|
||||
// Register listeners for volume and mute values
|
||||
mBGMDevice.AddPropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
|
||||
|
||||
@@ -75,284 +99,140 @@ void BGMDeviceControlSync::Activate()
|
||||
}
|
||||
catch(CAException)
|
||||
{
|
||||
CATry
|
||||
mBGMDevice.RemovePropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
|
||||
CACatch
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
mActive = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugMsg("BGMDeviceControlSync::Activate: Already active");
|
||||
}
|
||||
}
|
||||
|
||||
void BGMDeviceControlSync::Deactivate()
|
||||
{
|
||||
if(mActive && mBGMDevice.GetObjectID() != kAudioDeviceUnknown)
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mActive)
|
||||
{
|
||||
// Unregister listeners
|
||||
mBGMDevice.RemovePropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
|
||||
mBGMDevice.RemovePropertyListener(kMutePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
|
||||
}
|
||||
}
|
||||
DebugMsg("BGMDeviceControlSync::Deactivate: Deactivating control sync");
|
||||
|
||||
|
||||
void BGMDeviceControlSync::Swap(BGMDeviceControlSync& inDeviceControlSync)
|
||||
{
|
||||
mBGMDevice = inDeviceControlSync.mBGMDevice;
|
||||
mOutputDevice = inDeviceControlSync.mOutputDevice;
|
||||
inDeviceControlSync.Deactivate();
|
||||
Activate();
|
||||
}
|
||||
|
||||
#pragma mark Get/Set Control Values
|
||||
|
||||
// static
|
||||
void BGMDeviceControlSync::CopyMute(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope)
|
||||
{
|
||||
// TODO: Support for devices that have per-channel mute controls but no master mute control
|
||||
bool toHasSettableMasterMute = inToDevice.HasMuteControl(inScope, kMasterChannel) && inToDevice.MuteControlIsSettable(inScope, kMasterChannel);
|
||||
if(toHasSettableMasterMute && inFromDevice.HasMuteControl(inScope, kMasterChannel))
|
||||
{
|
||||
inToDevice.SetMuteControlValue(inScope,
|
||||
kMasterChannel,
|
||||
inFromDevice.GetMuteControlValue(inScope, kMasterChannel));
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
void BGMDeviceControlSync::CopyVolume(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope)
|
||||
{
|
||||
// Get the volume of the from device
|
||||
bool didGetFromVolume = false;
|
||||
Float32 fromVolume = FLT_MIN;
|
||||
|
||||
if(inFromDevice.HasVolumeControl(inScope, kMasterChannel))
|
||||
{
|
||||
fromVolume = inFromDevice.GetVolumeControlScalarValue(inScope, kMasterChannel);
|
||||
didGetFromVolume = true;
|
||||
}
|
||||
|
||||
// Use the average channel volume of the from device if it has no master volume
|
||||
if(!didGetFromVolume)
|
||||
{
|
||||
UInt32 fromNumChannels = inFromDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
|
||||
fromVolume = 0;
|
||||
|
||||
for(UInt32 channel = 1; channel <= fromNumChannels; channel++)
|
||||
// Deregister listeners
|
||||
if(mBGMDevice.GetObjectID() != kAudioDeviceUnknown)
|
||||
{
|
||||
if(inFromDevice.HasVolumeControl(inScope, channel))
|
||||
{
|
||||
fromVolume += inFromDevice.GetVolumeControlScalarValue(inScope, channel);
|
||||
didGetFromVolume = true;
|
||||
}
|
||||
}
|
||||
|
||||
fromVolume /= fromNumChannels;
|
||||
}
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] {
|
||||
mBGMDevice.RemovePropertyListener(kVolumePropertyAddress,
|
||||
&BGMDeviceControlSync::BGMDeviceListenerProc,
|
||||
this);
|
||||
});
|
||||
|
||||
// Set the volume of the to device
|
||||
if(didGetFromVolume && fromVolume != FLT_MIN)
|
||||
{
|
||||
bool didSetVolume = false;
|
||||
|
||||
try
|
||||
{
|
||||
didSetVolume = SetMasterVolumeScalar(inToDevice, inScope, fromVolume);
|
||||
}
|
||||
catch(CAException e)
|
||||
{
|
||||
OSStatus err = e.GetError();
|
||||
char err4CC[5] = CA4CCToCString(err);
|
||||
CFStringRef uid = inToDevice.CopyDeviceUID();
|
||||
LogWarning("BGMDeviceControlSync::CopyVolume: CAException '%s' trying to set master volume of %s", err4CC, uid);
|
||||
CFRelease(uid);
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] {
|
||||
mBGMDevice.RemovePropertyListener(kMutePropertyAddress,
|
||||
&BGMDeviceControlSync::BGMDeviceListenerProc,
|
||||
this);
|
||||
});
|
||||
}
|
||||
|
||||
if(!didSetVolume)
|
||||
{
|
||||
// Couldn't find a master volume control to set, so try to find a virtual one
|
||||
Float32 fromVirtualMasterVolume;
|
||||
bool success = GetVirtualMasterVolume(inFromDevice, inScope, fromVirtualMasterVolume);
|
||||
if(success)
|
||||
{
|
||||
didSetVolume = SetVirtualMasterVolume(inToDevice, inScope, fromVirtualMasterVolume);
|
||||
}
|
||||
}
|
||||
|
||||
if(!didSetVolume)
|
||||
{
|
||||
// Couldn't set a master or virtual master volume, so as a fallback try to set each channel individually
|
||||
UInt32 numChannels = inToDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
|
||||
for(UInt32 channel = 1; channel <= numChannels; channel++)
|
||||
{
|
||||
if(inToDevice.HasVolumeControl(inScope, channel) && inToDevice.VolumeControlIsSettable(inScope, channel))
|
||||
{
|
||||
inToDevice.SetVolumeControlScalarValue(inScope, channel, fromVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
mActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGMDeviceControlSync::SetMasterVolumeScalar(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume)
|
||||
{
|
||||
bool hasSettableMasterVolume =
|
||||
inDevice.HasVolumeControl(inScope, kMasterChannel) && inDevice.VolumeControlIsSettable(inScope, kMasterChannel);
|
||||
|
||||
if(hasSettableMasterVolume)
|
||||
else
|
||||
{
|
||||
inDevice.SetVolumeControlScalarValue(inScope, kMasterChannel, inVolume);
|
||||
return true;
|
||||
DebugMsg("BGMDeviceControlSync::Deactivate: Not active");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGMDeviceControlSync::GetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume)
|
||||
#pragma mark Accessors
|
||||
|
||||
void BGMDeviceControlSync::SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice)
|
||||
{
|
||||
AudioObjectPropertyAddress virtualMasterVolumeAddress =
|
||||
{ kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, inScope, kAudioObjectPropertyElementMaster };
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
bool wasActive = mActive;
|
||||
|
||||
Deactivate();
|
||||
|
||||
mBGMDevice = inBGMDevice;
|
||||
mBGMDeviceControlsList.SetBGMDevice(inBGMDevice);
|
||||
mOutputDevice = inOutputDevice;
|
||||
|
||||
if(!AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterVolumeAddress))
|
||||
if(wasActive)
|
||||
{
|
||||
return false;
|
||||
Activate();
|
||||
}
|
||||
|
||||
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
|
||||
return kAudioServicesNoError == AHSGetPropertyData(inDevice.GetObjectID(),
|
||||
&virtualMasterVolumeAddress,
|
||||
&virtualMasterVolumePropertySize,
|
||||
&outVirtualMasterVolume);
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGMDeviceControlSync::SetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume)
|
||||
{
|
||||
// TODO: For me, setting the virtual master volume sets all the device's channels to the same volume, meaning you can't
|
||||
// keep any channels quieter than the others. The expected behaviour is to scale the channel volumes
|
||||
// proportionally. So to do this properly I think we'd have to store BGMDevice's previous volume and calculate
|
||||
// each channel's new volume from its current volume and the distance between BGMDevice's old and new volumes.
|
||||
//
|
||||
// The docs kAudioHardwareServiceDeviceProperty_VirtualMasterVolume for say
|
||||
// "If the device has individual channel volume controls, this property will apply to those identified by the
|
||||
// device's preferred multi-channel layout (or preferred stereo pair if the device is stereo only). Note that
|
||||
// this control maintains the relative balance between all the channels it affects.
|
||||
// so I'm not sure why that's not working here. As a workaround we take the to device's (virtual master) balance
|
||||
// before changing the volume and set it back after, but of course that'll only work for stereo devices.
|
||||
|
||||
bool didSetVolume = false;
|
||||
AudioObjectPropertyAddress virtualMasterVolumeAddress =
|
||||
{ kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, inScope, kAudioObjectPropertyElementMaster };
|
||||
bool hasVirtualMasterVolume = AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterVolumeAddress);
|
||||
|
||||
Boolean virtualMasterVolumeIsSettable;
|
||||
OSStatus err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterVolumeAddress, &virtualMasterVolumeIsSettable);
|
||||
virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError);
|
||||
|
||||
if(hasVirtualMasterVolume && virtualMasterVolumeIsSettable)
|
||||
{
|
||||
// Not sure why, but setting the virtual master volume sets all channels to the same volume. As a workaround, we store
|
||||
// the current balance here so we can reset it after setting the volume.
|
||||
Float32 virtualMasterBalance;
|
||||
bool didGetVirtualMasterBalance = GetVirtualMasterBalance(inDevice, inScope, virtualMasterBalance);
|
||||
|
||||
didSetVolume = kAudioServicesNoError == AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterVolumeAddress, sizeof(Float32), &inVolume);
|
||||
|
||||
// Reset the balance
|
||||
AudioObjectPropertyAddress virtualMasterBalanceAddress =
|
||||
{ kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster };
|
||||
|
||||
if(didSetVolume && didGetVirtualMasterBalance && AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress))
|
||||
{
|
||||
Boolean balanceIsSettable;
|
||||
err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterBalanceAddress, &balanceIsSettable);
|
||||
if(err == kAudioServicesNoError && balanceIsSettable)
|
||||
{
|
||||
AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterBalanceAddress, sizeof(Float32), &virtualMasterBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return didSetVolume;
|
||||
}
|
||||
#pragma mark Listener Procs
|
||||
|
||||
// static
|
||||
bool BGMDeviceControlSync::GetVirtualMasterBalance(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance)
|
||||
OSStatus BGMDeviceControlSync::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData)
|
||||
{
|
||||
AudioObjectPropertyAddress virtualMasterBalanceAddress =
|
||||
{ kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster };
|
||||
|
||||
if(!AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
|
||||
return kAudioServicesNoError == AHSGetPropertyData(inDevice.GetObjectID(),
|
||||
&virtualMasterBalanceAddress,
|
||||
&virtualMasterVolumePropertySize,
|
||||
&outVirtualMasterBalance);
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus BGMDeviceControlSync::AHSGetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32* ioDataSize, void* outData)
|
||||
{
|
||||
// The docs for AudioHardwareServiceGetPropertyData specifically allow passing NULL for inQualifierData as we do here,
|
||||
// but it's declared in an assume_nonnull section so we have to disable the warning here. I'm not sure why inQualifierData
|
||||
// isn't __nullable. I'm assuming it's either a backwards compatibility thing or just a bug.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
// The non-depreciated version of this (and the setter below) doesn't seem to support devices other than the default
|
||||
return AudioHardwareServiceGetPropertyData(inObjectID, inAddress, 0, NULL, ioDataSize, outData);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus BGMDeviceControlSync::AHSSetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inDataSize, const void* inData)
|
||||
{
|
||||
// See the explanation about these pragmas in AHSGetPropertyData
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
return AudioHardwareServiceSetPropertyData(inObjectID, inAddress, 0, NULL, inDataSize, inData);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
#pragma mark Listener
|
||||
|
||||
// static
|
||||
OSStatus BGMDeviceControlSync::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* __nonnull inAddresses, void* __nullable inClientData)
|
||||
{
|
||||
// refCon (reference context) is the instance that registered this listener proc
|
||||
// refCon (reference context) is the instance that registered this listener proc.
|
||||
BGMDeviceControlSync* refCon = static_cast<BGMDeviceControlSync*>(inClientData);
|
||||
|
||||
if(refCon->mActive)
|
||||
{
|
||||
ThrowIf(inObjectID != refCon->mBGMDevice.GetObjectID(),
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGMDeviceControlSync::BGMDeviceListenerProc: notified about audio object other than BGMDevice");
|
||||
|
||||
for(int i = 0; i < inNumberAddresses; i++)
|
||||
|
||||
auto checkState = [&] {
|
||||
if(!refCon)
|
||||
{
|
||||
AudioObjectPropertyScope scope = inAddresses[i].mScope;
|
||||
|
||||
switch(inAddresses[i].mSelector)
|
||||
{
|
||||
case kAudioDevicePropertyVolumeScalar:
|
||||
// Update the output device
|
||||
CopyVolume(refCon->mBGMDevice, refCon->mOutputDevice, scope);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyMute:
|
||||
// Update the output device. Note that this also runs when you change the volume (on BGMDevice)
|
||||
CopyMute(refCon->mBGMDevice, refCon->mOutputDevice, scope);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
LogError("BGMDeviceControlSync::BGMDeviceListenerProc: !refCon");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!refCon->mActive ||
|
||||
(refCon->mBGMDevice.GetObjectID() == kAudioObjectUnknown) ||
|
||||
(refCon->mOutputDevice.GetObjectID() == kAudioObjectUnknown))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(inObjectID != refCon->mBGMDevice.GetObjectID())
|
||||
{
|
||||
LogError("BGMDeviceControlSync::BGMDeviceListenerProc: notified about audio object other than BGMDevice");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for(int i = 0; i < inNumberAddresses; i++)
|
||||
{
|
||||
AudioObjectPropertyScope scope = inAddresses[i].mScope;
|
||||
|
||||
switch(inAddresses[i].mSelector)
|
||||
{
|
||||
case kAudioDevicePropertyVolumeScalar:
|
||||
{
|
||||
CAMutex::Locker locker(refCon->mMutex);
|
||||
|
||||
// Update the output device's volume.
|
||||
if(checkState())
|
||||
{
|
||||
refCon->mOutputDevice.CopyVolumeFrom(refCon->mBGMDevice, scope);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyMute:
|
||||
{
|
||||
CAMutex::Locker locker(refCon->mMutex);
|
||||
|
||||
// Update the output device's mute control. Note that this also runs when you
|
||||
// change the volume (on BGMDevice).
|
||||
if(checkState())
|
||||
{
|
||||
refCon->mOutputDevice.CopyMuteFrom(refCon->mBGMDevice, scope);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// "The return value [of an AudioObjectPropertyListenerProc] is currently unused and should always be 0."
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -17,65 +17,105 @@
|
||||
// BGMDeviceControlSync.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Listens for notifications that BGMDevice's controls (just volume and mute currently) have changed value, and
|
||||
// copies the new values to the output device.
|
||||
// Synchronises BGMDevice's controls (just volume and mute currently) with the output device's
|
||||
// controls. This allows the user to control the output device normally while BGMDevice is set as
|
||||
// the default device.
|
||||
//
|
||||
// BGMDeviceControlSync disables any BGMDevice controls that the output device doesn't also have.
|
||||
// When the value of one of BGMDevice's controls is changed, BGMDeviceControlSync copies the new
|
||||
// value to the output device.
|
||||
//
|
||||
// Thread safe.
|
||||
//
|
||||
|
||||
#ifndef __BGMApp__BGMDeviceControlSync__
|
||||
#define __BGMApp__BGMDeviceControlSync__
|
||||
#ifndef BGMApp__BGMDeviceControlSync
|
||||
#define BGMApp__BGMDeviceControlSync
|
||||
|
||||
// Local Includes
|
||||
#include "BGMAudioDevice.h"
|
||||
#include "BGMDeviceControlsList.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAMutex.h"
|
||||
|
||||
// System Includes
|
||||
#include <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGMDeviceControlSync
|
||||
{
|
||||
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
BGMDeviceControlSync(CAHALAudioDevice inBGMDevice, CAHALAudioDevice inOutputDevice);
|
||||
BGMDeviceControlSync(AudioObjectID inBGMDevice,
|
||||
AudioObjectID inOutputDevice,
|
||||
CAHALAudioSystemObject inAudioSystem
|
||||
= CAHALAudioSystemObject());
|
||||
~BGMDeviceControlSync();
|
||||
// Disallow copying
|
||||
BGMDeviceControlSync(const BGMDeviceControlSync&) = delete;
|
||||
BGMDeviceControlSync& operator=(const BGMDeviceControlSync&) = delete;
|
||||
// Move constructor/assignment
|
||||
BGMDeviceControlSync(BGMDeviceControlSync&& inDeviceControlSync) { Swap(inDeviceControlSync); }
|
||||
BGMDeviceControlSync& operator=(BGMDeviceControlSync&& inDeviceControlSync) { Swap(inDeviceControlSync); return *this; }
|
||||
|
||||
|
||||
#ifdef __OBJC__
|
||||
// Only intended as a convenience for Objective-C instance vars
|
||||
BGMDeviceControlSync() { };
|
||||
BGMDeviceControlSync()
|
||||
: BGMDeviceControlSync(kAudioObjectUnknown, kAudioObjectUnknown) { };
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
/*!
|
||||
Begin synchronising BGMDevice's controls with the output device's.
|
||||
|
||||
@throws BGM_DeviceNotSetException if BGMDevice isn't set.
|
||||
@throws CAException if the HAL or one of the devices returns an error when this function
|
||||
registers for device property notifications or when it copies the current
|
||||
values of the output device's controls to BGMDevice. This
|
||||
BGMDeviceControlSync will remain inactive if this function throws.
|
||||
*/
|
||||
void Activate();
|
||||
/*! Stop synchronising BGMDevice's controls with the output device's. */
|
||||
void Deactivate();
|
||||
void Swap(BGMDeviceControlSync& inDeviceControlSync);
|
||||
|
||||
static void CopyMute(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope);
|
||||
static void CopyVolume(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope);
|
||||
|
||||
static bool SetMasterVolumeScalar(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume);
|
||||
static bool GetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume);
|
||||
static bool SetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume);
|
||||
static bool GetVirtualMasterBalance(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance);
|
||||
|
||||
static OSStatus AHSGetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32* ioDataSize, void* outData);
|
||||
static OSStatus AHSSetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inDataSize, const void* inData);
|
||||
|
||||
static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData);
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
/*!
|
||||
Set the IDs of BGMDevice and the output device to synchronise with.
|
||||
|
||||
@throws BGM_DeviceNotSetException if BGMDevice isn't set.
|
||||
@throws CAException if the HAL or one of the new devices returns an error while restarting
|
||||
synchronisation. This BGMDeviceControlSync will be deactivated if this
|
||||
function throws, but its devices will still be set.
|
||||
*/
|
||||
void SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice);
|
||||
|
||||
#pragma mark Listener Procs
|
||||
|
||||
private:
|
||||
bool mActive = false;
|
||||
CAHALAudioDevice mBGMDevice { kAudioDeviceUnknown };
|
||||
CAHALAudioDevice mOutputDevice { kAudioDeviceUnknown };
|
||||
/*! Receives HAL notifications about the BGMDevice properties this class listens to. */
|
||||
static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID,
|
||||
UInt32 inNumberAddresses,
|
||||
const AudioObjectPropertyAddress* inAddresses,
|
||||
void* __nullable inClientData);
|
||||
|
||||
private:
|
||||
CAMutex mMutex { "Device Control Sync" };
|
||||
bool mActive = false;
|
||||
|
||||
CAHALAudioSystemObject mAudioSystem;
|
||||
|
||||
BGMAudioDevice mBGMDevice { (AudioObjectID)kAudioObjectUnknown };
|
||||
BGMAudioDevice mOutputDevice { (AudioObjectID)kAudioObjectUnknown };
|
||||
|
||||
BGMDeviceControlsList mBGMDeviceControlsList;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* __BGMApp__BGMDeviceControlSync__ */
|
||||
#endif /* BGMApp__BGMDeviceControlSync */
|
||||
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMDeviceControlsList.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGMDeviceControlsList.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAPropertyAddress.h"
|
||||
#include "CACFArray.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static const SInt64 kToggleDeviceInitialDelay = 50 * NSEC_PER_MSEC;
|
||||
static const SInt64 kToggleDeviceBackDelay = 500 * NSEC_PER_MSEC;
|
||||
static const SInt64 kDisableNullDeviceDelay = 500 * NSEC_PER_MSEC;
|
||||
static const SInt64 kDisableNullDeviceTimeout = 5000 * NSEC_PER_MSEC;
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMDeviceControlsList::BGMDeviceControlsList(AudioObjectID inBGMDevice,
|
||||
CAHALAudioSystemObject inAudioSystem)
|
||||
:
|
||||
mBGMDevice(inBGMDevice),
|
||||
mAudioSystem(inAudioSystem)
|
||||
{
|
||||
BGMAssert((mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)) ||
|
||||
mBGMDevice.GetObjectID() == kAudioObjectUnknown),
|
||||
"BGMDeviceControlsList::BGMDeviceControlsList: Given device is not BGMDevice");
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
mCanToggleDeviceOnSystem = (&dispatch_block_wait &&
|
||||
&dispatch_block_cancel &&
|
||||
&dispatch_block_testcancel &&
|
||||
&dispatch_queue_attr_make_with_qos_class);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(!mDeviceTogglingInitialised)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::~BGMDeviceControlsList", [&] {
|
||||
mAudioSystem.RemovePropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
mListenerBlock);
|
||||
});
|
||||
|
||||
// If we're in the middle of toggling the default device, block until we've finished.
|
||||
if(mDisableNullDeviceBlock && mDeviceToggleState != ToggleState::NotToggling)
|
||||
{
|
||||
DebugMsg("BGMDeviceControlsList::~BGMDeviceControlsList: Waiting for device toggle");
|
||||
|
||||
// Copy the reference so we can unlock the mutex and allow any remaining blocks to run.
|
||||
dispatch_block_t disableNullDeviceBlock = mDisableNullDeviceBlock;
|
||||
|
||||
CAMutex::Unlocker unlocker(mMutex);
|
||||
|
||||
// Note that if mDisableNullDeviceBlock is currently running this will return after it
|
||||
// finishes and if it's already run this will return immediately. So we don't have to
|
||||
// worry about ending up waiting for mDisableNullDeviceBlock when it isn't queued.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
bool timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
if(timedOut)
|
||||
{
|
||||
LogWarning("BGMDeviceControlsList::~BGMDeviceControlsList: Device toggle timed out");
|
||||
}
|
||||
}
|
||||
|
||||
mDeviceToggleState = ToggleState::NotToggling;
|
||||
|
||||
DestroyBlock(mDeviceToggleBlock);
|
||||
DestroyBlock(mDeviceToggleBackBlock);
|
||||
DestroyBlock(mDisableNullDeviceBlock);
|
||||
|
||||
Block_release(mListenerBlock);
|
||||
dispatch_release(mListenerQueue);
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
void BGMDeviceControlsList::SetBGMDevice(AudioObjectID inBGMDeviceID)
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
mBGMDevice = inBGMDeviceID;
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
"BGMDeviceControlsList::SetBGMDevice: Given device is not BGMDevice");
|
||||
}
|
||||
|
||||
#pragma mark Update Controls List
|
||||
|
||||
bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID)
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mBGMDevice.GetObjectID() != mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)))
|
||||
{
|
||||
LogWarning("BGMDeviceControlsList::MatchControlsListOf: BGMDevice ID not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the output device doesn't have a control that BGMDevice does, disable it on BGMDevice so
|
||||
// the system's audio UI isn't confusing.
|
||||
|
||||
// No need to change input controls.
|
||||
AudioObjectPropertyScope inScope = kAudioObjectPropertyScopeOutput;
|
||||
|
||||
// Check which of BGMDevice's controls are currently enabled. We need to know whether we're
|
||||
// actually enabling/disabling any controls so we know whether we need to call
|
||||
// PropagateControlListChange afterward.
|
||||
CFTypeRef __nullable enabledControlsRef =
|
||||
mBGMDevice.GetPropertyData_CFType(kBGMEnabledOutputControlsAddress);
|
||||
|
||||
ThrowIf(!enabledControlsRef || (CFGetTypeID(enabledControlsRef) != CFArrayGetTypeID()),
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMDeviceControlsList::MatchControlsListOf: Expected a CFArray for "
|
||||
"kAudioDeviceCustomPropertyEnabledOutputControls");
|
||||
|
||||
CACFArray enabledControls(static_cast<CFArrayRef>(enabledControlsRef), true);
|
||||
|
||||
BGMAssert(enabledControls.GetNumberItems() == 2,
|
||||
"BGMDeviceControlsList::MatchControlsListOf: Expected 2 array elements for "
|
||||
"kAudioDeviceCustomPropertyEnabledOutputControls");
|
||||
|
||||
bool volumeEnabled;
|
||||
bool didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Volume, volumeEnabled);
|
||||
ThrowIf(!didGetBool,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMDeviceControlsList::MatchControlsListOf: Expected volume element of "
|
||||
"kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean");
|
||||
|
||||
bool muteEnabled;
|
||||
didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Mute, muteEnabled);
|
||||
ThrowIf(!didGetBool,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMDeviceControlsList::MatchControlsListOf: Expected mute element of "
|
||||
"kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean");
|
||||
|
||||
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: BGMDevice has volume %s, mute %s",
|
||||
(volumeEnabled ? "enabled" : "disabled"),
|
||||
(muteEnabled ? "enabled" : "disabled"));
|
||||
|
||||
// Check which controls the other device has.
|
||||
BGMAudioDevice device(inDeviceID);
|
||||
bool hasMute = device.HasSettableMasterMute(inScope);
|
||||
|
||||
bool hasVolume =
|
||||
device.HasSettableMasterVolume(inScope) || device.HasSettableVirtualMasterVolume(inScope);
|
||||
|
||||
if(!hasVolume)
|
||||
{
|
||||
// Check for per-channel volume controls.
|
||||
UInt32 numChannels =
|
||||
device.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
|
||||
|
||||
for(UInt32 channel = 1; channel <= numChannels; channel++)
|
||||
{
|
||||
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlsList::MatchControlsListOf",
|
||||
"Checking for channel volume controls",
|
||||
([&] {
|
||||
hasVolume =
|
||||
(device.HasVolumeControl(inScope, channel)
|
||||
&& device.VolumeControlIsSettable(inScope, channel));
|
||||
}));
|
||||
|
||||
if(hasVolume)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell BGMDevice to enable/disable its controls to match the output device.
|
||||
bool deviceUpdated = false;
|
||||
|
||||
CACFArray newEnabledControls;
|
||||
newEnabledControls.SetCFMutableArrayFromCopy(enabledControls.GetCFArray());
|
||||
|
||||
// Update volume.
|
||||
if(volumeEnabled != hasVolume)
|
||||
{
|
||||
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice volume control.",
|
||||
hasVolume ? "Enabling" : "Disabling");
|
||||
|
||||
newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Volume, hasVolume);
|
||||
deviceUpdated = true;
|
||||
}
|
||||
|
||||
// Update mute.
|
||||
if(muteEnabled != hasMute)
|
||||
{
|
||||
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice mute control.",
|
||||
hasMute ? "Enabling" : "Disabling");
|
||||
|
||||
newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Mute, hasMute);
|
||||
deviceUpdated = true;
|
||||
}
|
||||
|
||||
if(deviceUpdated)
|
||||
{
|
||||
mBGMDevice.SetPropertyData_CFType(kBGMEnabledOutputControlsAddress,
|
||||
newEnabledControls.GetCFMutableArray());
|
||||
}
|
||||
|
||||
return deviceUpdated;
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::PropagateControlListChange()
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if((mBGMDevice == kAudioObjectUnknown) || !mCanToggleDeviceOnSystem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InitDeviceToggling();
|
||||
|
||||
// Leave the default device alone if the user has changed it since launching BGMApp.
|
||||
bool bgmDeviceIsDefault = true;
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::PropagateControlListChange", ([&] {
|
||||
bgmDeviceIsDefault =
|
||||
(mBGMDevice.GetObjectID() == mAudioSystem.GetDefaultAudioDevice(false, false));
|
||||
}));
|
||||
|
||||
if(bgmDeviceIsDefault)
|
||||
{
|
||||
mDeviceToggleState = ToggleState::SettingNullDeviceAsDefault;
|
||||
|
||||
// We'll get a notification from the HAL after the Null Device is enabled. Then we can
|
||||
// temporarily make it the default device, which gets other programs to notice that
|
||||
// BGMDevice's controls have changed.
|
||||
try
|
||||
{
|
||||
CAMutex::Unlocker unlocker(mMutex);
|
||||
SetNullDeviceEnabled(true);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
mDeviceToggleState = ToggleState::NotToggling;
|
||||
LogError("BGMDeviceControlsList::PropagateControlListChange: Could not enable the Null "
|
||||
"Device");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
void BGMDeviceControlsList::InitDeviceToggling()
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceTogglingInitialised || !mCanToggleDeviceOnSystem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
"BGMDeviceControlsList::InitDeviceToggling: mBGMDevice device is not set to "
|
||||
"BGMDevice's ID");
|
||||
|
||||
mDeviceToggleBlock = CreateDeviceToggleBlock();
|
||||
mDeviceToggleBackBlock = CreateDeviceToggleBackBlock();
|
||||
mDisableNullDeviceBlock = CreateDisableNullDeviceBlock();
|
||||
|
||||
// Register a listener to find out when the Null Device becomes available/unavailable. See
|
||||
// ToggleDefaultDevice.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_queue_attr_t attr =
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
|
||||
#pragma clang diagnostic pop
|
||||
mListenerQueue = dispatch_queue_create("com.bearisdriving.BGM.BGMDeviceControlsList", attr);
|
||||
|
||||
mListenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// Ignore the notification if we're not toggling the default device, which would just mean
|
||||
// the default device has been changed for an unrelated reason.
|
||||
if(mDeviceToggleState == ToggleState::NotToggling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < inNumberAddresses; i++)
|
||||
{
|
||||
switch(inAddresses[i].mSelector)
|
||||
{
|
||||
case kAudioHardwarePropertyDevices:
|
||||
{
|
||||
CAMutex::Locker innerLocker(mMutex);
|
||||
|
||||
DebugMsg("BGMDeviceControlsList::InitDeviceToggling: Got "
|
||||
"kAudioHardwarePropertyDevices");
|
||||
|
||||
// Cancel the previous block in case it hasn't run yet.
|
||||
DestroyBlock(mDeviceToggleBlock);
|
||||
|
||||
mDeviceToggleBlock = CreateDeviceToggleBlock();
|
||||
|
||||
// Changing the default device too quickly after enabling the Null Device
|
||||
// seems to cause problems with some programs. Not sure why.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceInitialDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBlock);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::InitDeviceToggling", [&] {
|
||||
mAudioSystem.AddPropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
mListenerBlock);
|
||||
});
|
||||
|
||||
mDeviceTogglingInitialised = true;
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::ToggleDefaultDevice()
|
||||
{
|
||||
// Set the Null Device as the OS X default device.
|
||||
AudioObjectID nullDeviceID = mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMNullDeviceUID));
|
||||
|
||||
if(nullDeviceID == kAudioObjectUnknown)
|
||||
{
|
||||
// It's unlikely, but we might have been notified about an unrelated device so just log a
|
||||
// warning.
|
||||
LogWarning("BGMDeviceControlsList::ToggleDefaultDevice: Null Device not found");
|
||||
return;
|
||||
}
|
||||
|
||||
DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting Null Device as default. "
|
||||
"nullDeviceID = %u", nullDeviceID);
|
||||
mAudioSystem.SetDefaultAudioDevice(false, false, nullDeviceID);
|
||||
|
||||
mDeviceToggleState = ToggleState::SettingBGMDeviceAsDefault;
|
||||
|
||||
// A small number of apps (e.g. Firefox) seem to have trouble with the default device being
|
||||
// changed back immediately, so for now we insert a short delay here and before disabling the
|
||||
// Null Device.
|
||||
|
||||
// Cancel the previous block in case it hasn't run yet.
|
||||
DestroyBlock(mDeviceToggleBackBlock);
|
||||
|
||||
mDeviceToggleBackBlock = CreateDeviceToggleBackBlock();
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBackBlock);
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::SetNullDeviceEnabled(bool inEnabled)
|
||||
{
|
||||
DebugMsg("BGMDeviceControlsList::SetNullDeviceEnabled: %s the null device",
|
||||
inEnabled ? "Enabling" : "Disabling");
|
||||
|
||||
// Get the audio object for BGMDriver, which is the object the Null Device belongs to.
|
||||
AudioObjectID bgmDriverID = mAudioSystem.GetAudioPlugInForBundleID(CFSTR(kBGMDriverBundleID));
|
||||
|
||||
if(bgmDriverID == kAudioObjectUnknown)
|
||||
{
|
||||
LogError("BGMDeviceControlsList::SetNullDeviceEnabled: BGMDriver plug-in audio object not "
|
||||
"found");
|
||||
throw CAException(kAudioHardwareUnspecifiedError);
|
||||
}
|
||||
|
||||
CAHALAudioObject bgmDriver(bgmDriverID);
|
||||
bgmDriver.SetPropertyData_CFType(CAPropertyAddress(kAudioPlugInCustomPropertyNullDeviceActive),
|
||||
(inEnabled ? kCFBooleanTrue : kCFBooleanFalse));
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState == ToggleState::SettingNullDeviceAsDefault)
|
||||
{
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBlock",
|
||||
([&] {
|
||||
ToggleDefaultDevice();
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState != ToggleState::SettingBGMDeviceAsDefault)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set BGMDevice back as the default device.
|
||||
DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting BGMDevice as default");
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBackBlock", ([&] {
|
||||
mAudioSystem.SetDefaultAudioDevice(false, false, mBGMDevice.GetObjectID());
|
||||
}));
|
||||
|
||||
mDeviceToggleState = ToggleState::DisablingNullDevice;
|
||||
|
||||
// Cancel the previous block in case it hasn't run yet.
|
||||
DestroyBlock(mDisableNullDeviceBlock);
|
||||
|
||||
mDisableNullDeviceBlock = CreateDisableNullDeviceBlock();
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
mDisableNullDeviceBlock);
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState != ToggleState::DisablingNullDevice)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mDeviceToggleState = ToggleState::NotToggling;
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDisableNullDeviceBlock",
|
||||
([&] {
|
||||
CAMutex::Unlocker unlocker(mMutex);
|
||||
// Hide the null device from the user again.
|
||||
SetNullDeviceEnabled(false);
|
||||
}));
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
"BGMDevice's AudioObjectID changed");
|
||||
});
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
|
||||
{
|
||||
if(!block)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_block_t& blockNN = (dispatch_block_t&)block;
|
||||
if(!dispatch_block_testcancel(blockNN))
|
||||
{
|
||||
// Stop the block from running if it's currently queued.
|
||||
dispatch_block_cancel(blockNN);
|
||||
|
||||
// Make sure the block isn't currently running. That should almost never be the case.
|
||||
while(!dispatch_block_testcancel(blockNN))
|
||||
{
|
||||
CAMutex::Unlocker unlocker(mMutex);
|
||||
usleep(10);
|
||||
}
|
||||
|
||||
Block_release(block);
|
||||
block = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop /* -Wpartial-availability */
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMDeviceControlsList.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef BGMApp__BGMDeviceControlsList
|
||||
#define BGMApp__BGMDeviceControlsList
|
||||
|
||||
// Local Includes
|
||||
#include "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAMutex.h"
|
||||
|
||||
// System Includes
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGMDeviceControlsList
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
|
||||
BGMDeviceControlsList(AudioObjectID inBGMDevice,
|
||||
CAHALAudioSystemObject inAudioSystem
|
||||
= CAHALAudioSystemObject());
|
||||
~BGMDeviceControlsList();
|
||||
// Disallow copying
|
||||
BGMDeviceControlsList(const BGMDeviceControlsList&) = delete;
|
||||
BGMDeviceControlsList& operator=(const BGMDeviceControlsList&) = delete;
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
/*! @param inBGMDeviceID The ID of BGMDevice. */
|
||||
void SetBGMDevice(AudioObjectID inBGMDeviceID);
|
||||
|
||||
#pragma mark Update Controls List
|
||||
|
||||
/*!
|
||||
Enable the BGMDevice controls (volume and mute currently) that can be matched to controls of
|
||||
the given device, and disable the ones that can't.
|
||||
|
||||
@param inDeviceID The ID of the device.
|
||||
@return True if BGMDevice's list of controls was updated.
|
||||
@throws CAException if an error is received from either device.
|
||||
*/
|
||||
bool MatchControlsListOf(AudioObjectID inDeviceID);
|
||||
/*!
|
||||
After updating BGMDevice's controls list, we need to change the default device so programs
|
||||
(including OS X's audio UI) will update themselves. We could just change to the real output
|
||||
device and change back, but that could have side effects the user wouldn't expect. For example,
|
||||
an app the user has muted might be unmuted for a short period.
|
||||
|
||||
Instead we tell BGMDriver to enable the Null Device -- a device that does nothing -- so we can
|
||||
use it to toggle the default device. The Null Device is normally disabled so it can be hidden
|
||||
from the user. OS X won't let us make a hidden device temporarily visible or set a hidden
|
||||
device as the default, so we have to completely remove the Null Device from the system while
|
||||
we're not using it.
|
||||
|
||||
@throws CAException if it fails to enable the Null Device.
|
||||
*/
|
||||
void PropagateControlListChange();
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
private:
|
||||
/*! Lazily initialises the fields used to toggle the default device. */
|
||||
void InitDeviceToggling();
|
||||
/*! Changes the OS X default audio device to the Null Device and then back to BGMDevice. */
|
||||
void ToggleDefaultDevice();
|
||||
/*!
|
||||
Enable or disable the Null Device. See PropagateControlListChange and BGM_NullDevice in
|
||||
BGMDriver.
|
||||
|
||||
@throws CAException if we can't get the BGMDriver plug-in audio object from the HAL or the HAL
|
||||
returns an error when setting kAudioPlugInCustomPropertyNullDeviceActive.
|
||||
*/
|
||||
void SetNullDeviceEnabled(bool inEnabled);
|
||||
|
||||
dispatch_block_t CreateDeviceToggleBlock();
|
||||
dispatch_block_t CreateDeviceToggleBackBlock();
|
||||
dispatch_block_t CreateDisableNullDeviceBlock();
|
||||
|
||||
void DestroyBlock(dispatch_block_t __nullable & block);
|
||||
|
||||
private:
|
||||
CAMutex mMutex { "Device Controls List" };
|
||||
bool mDeviceTogglingInitialised = false;
|
||||
// OS X 10.9 doesn't have the functions we use for PropagateControlListChange.
|
||||
bool mCanToggleDeviceOnSystem;
|
||||
|
||||
BGMAudioDevice mBGMDevice;
|
||||
CAHALAudioSystemObject mAudioSystem; // Not guarded by the mutex.
|
||||
|
||||
enum ToggleState
|
||||
{
|
||||
NotToggling, SettingNullDeviceAsDefault, SettingBGMDeviceAsDefault, DisablingNullDevice
|
||||
};
|
||||
BGMDeviceControlsList::ToggleState mDeviceToggleState = ToggleState::NotToggling;
|
||||
|
||||
dispatch_block_t mDeviceToggleBlock;
|
||||
dispatch_block_t mDeviceToggleBackBlock;
|
||||
dispatch_block_t mDisableNullDeviceBlock;
|
||||
|
||||
dispatch_queue_t mListenerQueue;
|
||||
AudioObjectPropertyListenerBlock mListenerBlock;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMApp__BGMDeviceControlsList */
|
||||
|
||||
+713
-229
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
// BGMPlayThrough.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Reads audio from an input device and immediately writes it to an output device. We currently use this class with the input
|
||||
// device always set to BGMDevice and the output device set to the one selected in the preferences menu.
|
||||
@@ -28,7 +28,7 @@
|
||||
// sample code from 2004. This class's main addition is pausing playthrough when idle to save CPU.
|
||||
//
|
||||
// Playing audio with this class uses more CPU, mostly in the coreaudiod process, than playing audio normally because we need
|
||||
// an input IO proc as well as an output one, and BGMDriver is running in addition to the output device's driver. For me, it
|
||||
// an input IOProc as well as an output one, and BGMDriver is running in addition to the output device's driver. For me, it
|
||||
// usually adds around 1-2% (as a percentage of total usage -- it doesn't seem to be relative to the CPU used when playing
|
||||
// audio normally).
|
||||
//
|
||||
@@ -36,14 +36,18 @@
|
||||
// a future release.
|
||||
//
|
||||
|
||||
#ifndef __BGMApp__BGMPlayThrough__
|
||||
#define __BGMApp__BGMPlayThrough__
|
||||
#ifndef BGMApp__BGMPlayThrough
|
||||
#define BGMApp__BGMPlayThrough
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CARingBuffer.h"
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAMutex.h"
|
||||
|
||||
// STL Includes
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
|
||||
// System Includes
|
||||
#include <mach/semaphore.h>
|
||||
|
||||
@@ -52,6 +56,10 @@
|
||||
|
||||
class BGMPlayThrough
|
||||
{
|
||||
|
||||
public:
|
||||
// Error codes
|
||||
static const OSStatus kDeviceNotStarting = 100;
|
||||
|
||||
public:
|
||||
BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
@@ -59,41 +67,70 @@ public:
|
||||
// Disallow copying
|
||||
BGMPlayThrough(const BGMPlayThrough&) = delete;
|
||||
BGMPlayThrough& operator=(const BGMPlayThrough&) = delete;
|
||||
// Move constructor/assignment
|
||||
BGMPlayThrough(BGMPlayThrough&& inPlayThrough) { Swap(inPlayThrough); }
|
||||
BGMPlayThrough& operator=(BGMPlayThrough&& inPlayThrough) { Swap(inPlayThrough); return *this; }
|
||||
|
||||
#ifdef __OBJC__
|
||||
// Only intended as a convenience for Objective-C instance vars
|
||||
// Only intended as a convenience (hack) for Objective-C instance vars. Call
|
||||
// SetDevices to initialise the instance before using it.
|
||||
BGMPlayThrough() { }
|
||||
#endif
|
||||
|
||||
private:
|
||||
void Swap(BGMPlayThrough& inPlayThrough);
|
||||
|
||||
/*! @throws CAException */
|
||||
void Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
|
||||
public:
|
||||
/*! @throws CAException */
|
||||
void Activate();
|
||||
/*! @throws CAException */
|
||||
void Deactivate();
|
||||
|
||||
|
||||
private:
|
||||
void AllocateBuffer();
|
||||
|
||||
static bool IsBGMDevice(CAHALAudioDevice inDevice);
|
||||
|
||||
void CreateIOProcs();
|
||||
void DestroyIOProcs();
|
||||
/*! @throws CAException */
|
||||
void CreateIOProcIDs();
|
||||
/*! @throws CAException */
|
||||
void DestroyIOProcIDs();
|
||||
/*!
|
||||
@return True if both IOProcs are stopped.
|
||||
@nonthreadsafe
|
||||
*/
|
||||
bool CheckIOProcsAreStopped() const noexcept; // TODO: REQUIRES(mStateMutex);
|
||||
|
||||
public:
|
||||
OSStatus Start();
|
||||
OSStatus WaitForOutputDeviceToStart();
|
||||
/*!
|
||||
Pass null for either param to only change one of the devices.
|
||||
@throws CAException
|
||||
*/
|
||||
void SetDevices(CAHALAudioDevice* __nullable inInputDevice,
|
||||
CAHALAudioDevice* __nullable inOutputDevice);
|
||||
|
||||
/*! @throws CAException */
|
||||
void Start();
|
||||
|
||||
// Blocks until the output device has started our IOProc. Returns one of the error constants
|
||||
// from AudioHardwareBase.h (e.g. kAudioHardwareNoError).
|
||||
OSStatus WaitForOutputDeviceToStart() noexcept;
|
||||
|
||||
private:
|
||||
void ReleaseThreadsWaitingForOutputToStart() const;
|
||||
|
||||
public:
|
||||
OSStatus Stop();
|
||||
void StopIfIdle();
|
||||
|
||||
private:
|
||||
|
||||
static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID,
|
||||
UInt32 inNumberAddresses,
|
||||
const AudioObjectPropertyAddress* inAddresses,
|
||||
void* __nullable inClientData);
|
||||
static bool RunningSomewhereOtherThanBGMApp(const CAHALAudioDevice inBGMDevice);
|
||||
static void HandleBGMDeviceIsRunning(BGMPlayThrough* refCon);
|
||||
static void HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlayThrough* refCon);
|
||||
|
||||
static bool IsRunningSomewhereOtherThanBGMApp(const CAHALAudioDevice& inBGMDevice);
|
||||
|
||||
static OSStatus InputDeviceIOProc(AudioObjectID inDevice,
|
||||
const AudioTimeStamp* inNow,
|
||||
@@ -109,6 +146,22 @@ private:
|
||||
AudioBufferList* outOutputData,
|
||||
const AudioTimeStamp* inOutputTime,
|
||||
void* __nullable inClientData);
|
||||
|
||||
// The state of an IOProc. Used by the IOProc to tell other threads when it's finished starting. Used by other
|
||||
// threads to tell the IOProc to stop itself. (Probably used for other things as well.)
|
||||
enum class IOState
|
||||
{
|
||||
Stopped, Starting, Running, Stopping
|
||||
};
|
||||
|
||||
// The IOProcs call this to update their IOState member. Also stops the IOProc if its state has been set to Stopping.
|
||||
// Returns true if it changes the state.
|
||||
static bool UpdateIOProcState(const char* __nullable callerName,
|
||||
std::atomic<IOState>& inState,
|
||||
AudioDeviceIOProcID __nullable inIOProcID,
|
||||
CAHALAudioDevice& inDevice,
|
||||
IOState& outNewState);
|
||||
|
||||
static void HandleRingBufferError(CARingBufferError err,
|
||||
const char* methodName,
|
||||
const char* callReturningErr);
|
||||
@@ -116,31 +169,31 @@ private:
|
||||
private:
|
||||
CARingBuffer mBuffer;
|
||||
|
||||
AudioDeviceIOProcID mInputDeviceIOProcID;
|
||||
AudioDeviceIOProcID mOutputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mInputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID;
|
||||
|
||||
CAHALAudioDevice mInputDevice { kAudioDeviceUnknown };
|
||||
CAHALAudioDevice mOutputDevice { kAudioDeviceUnknown };
|
||||
CAHALAudioDevice mInputDevice { kAudioObjectUnknown };
|
||||
CAHALAudioDevice mOutputDevice { kAudioObjectUnknown };
|
||||
|
||||
CAMutex mStateMutex { "Playthrough state" };
|
||||
|
||||
// Signalled when the output IO proc runs. We use it to tell BGMDriver when the output device is ready to receive audio data.
|
||||
// Signalled when the output IOProc runs. We use it to tell BGMDriver when the output device is ready to receive audio data.
|
||||
semaphore_t mOutputDeviceIOProcSemaphore { SEMAPHORE_NULL };
|
||||
|
||||
bool mActive = false;
|
||||
bool mPlayingThrough = false;
|
||||
|
||||
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice;
|
||||
|
||||
bool mInputDeviceIOProcShouldStop = false;
|
||||
bool mOutputDeviceIOProcShouldStop = false;
|
||||
|
||||
std::atomic<IOState> mInputDeviceIOProcState { IOState::Stopped };
|
||||
std::atomic<IOState> mOutputDeviceIOProcState { IOState::Stopped };
|
||||
|
||||
// For debug logging.
|
||||
UInt64 mToldOutputDeviceToStartAt;
|
||||
|
||||
// IO proc vars. (Should only be used inside IO procs.)
|
||||
// IOProc vars. (Should only be used inside IOProcs.)
|
||||
|
||||
// The earliest/latest sample times seen by the IO procs since starting playthrough. -1 for unset.
|
||||
// The earliest/latest sample times seen by the IOProcs since starting playthrough. -1 for unset.
|
||||
Float64 mFirstInputSampleTime = -1;
|
||||
Float64 mLastInputSampleTime = -1;
|
||||
Float64 mLastOutputSampleTime = -1;
|
||||
@@ -152,5 +205,5 @@ private:
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* __BGMApp__BGMPlayThrough__ */
|
||||
#endif /* BGMApp__BGMPlayThrough */
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMUserDefaults.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// A simple wrapper around our use of NSUserDefaults. Used to store the preferences/state that only
|
||||
// apply to BGMApp. The others are stored on BGMDriver.
|
||||
//
|
||||
|
||||
// System includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMUserDefaults : NSObject
|
||||
|
||||
// If inDefaults is nil, settings are not loaded from or saved to disk, which is useful for testing.
|
||||
- (instancetype) initWithDefaults:(NSUserDefaults* __nullable)inDefaults;
|
||||
|
||||
// The musicPlayerID (see BGMMusicPlayer.h), as a string, of the music player selected by the user.
|
||||
// Must be either null or a string that can be parsed by NSUUID.
|
||||
@property NSString* __nullable selectedMusicPlayerID;
|
||||
|
||||
@property BOOL autoPauseMusicEnabled;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMUserDefaults.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// Keys
|
||||
static NSString* const BGMDefaults_AutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
|
||||
static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayerID";
|
||||
|
||||
@implementation BGMUserDefaults {
|
||||
// The defaults object wrapped by this object.
|
||||
NSUserDefaults* defaults;
|
||||
// When we're not persisting defaults, settings are stored in this dictionary instead. This
|
||||
// var should only be accessed if 'defaults' is nil.
|
||||
NSMutableDictionary<NSString*,id>* transientDefaults;
|
||||
}
|
||||
|
||||
- (instancetype) initWithDefaults:(NSUserDefaults* __nullable)inDefaults {
|
||||
if ((self = [super init])) {
|
||||
defaults = inDefaults;
|
||||
|
||||
// Register the settings defaults.
|
||||
//
|
||||
// iTunes is the default music player, but we don't set BGMDefaults_SelectedMusicPlayerID
|
||||
// here so we know when it's never been set. (If it hasn't, we try using BGMDevice's
|
||||
// kAudioDeviceCustomPropertyMusicPlayerBundleID property to tell which music player should
|
||||
// be selected. See BGMMusicPlayers.)
|
||||
NSDictionary* defaultsDict = @{ BGMDefaults_AutoPauseMusicEnabled: @YES };
|
||||
|
||||
if (defaults) {
|
||||
[defaults registerDefaults:defaultsDict];
|
||||
} else {
|
||||
transientDefaults = [defaultsDict mutableCopy];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString* __nullable) selectedMusicPlayerID {
|
||||
return [self get:BGMDefaults_SelectedMusicPlayerID];
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID {
|
||||
[self set:BGMDefaults_SelectedMusicPlayerID to:selectedMusicPlayerID];
|
||||
}
|
||||
|
||||
- (BOOL) autoPauseMusicEnabled {
|
||||
return [self getBool:BGMDefaults_AutoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
|
||||
[self setBool:BGMDefaults_AutoPauseMusicEnabled to:autoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (id __nullable) get:(NSString*)key {
|
||||
return defaults ? [defaults objectForKey:key] : transientDefaults[key];
|
||||
}
|
||||
|
||||
- (void) set:(NSString*)key to:(NSObject<NSCopying,NSSecureCoding>* __nullable)value {
|
||||
if (defaults) {
|
||||
[defaults setObject:value forKey:key];
|
||||
} else {
|
||||
transientDefaults[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL) getBool:(NSString*)key {
|
||||
return defaults ? [defaults boolForKey:key] : [transientDefaults[key] boolValue];
|
||||
}
|
||||
|
||||
- (void) setBool:(NSString*)key to:(BOOL)value {
|
||||
if (defaults) {
|
||||
[defaults setBool:value forKey:key];
|
||||
} else {
|
||||
transientDefaults[key] = [NSNumber numberWithBool:value];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
// Self Include
|
||||
#import "BGMXPCListener.h"
|
||||
#import "BGMPlayThrough.h" // For kDeviceNotStarting.
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -175,11 +176,26 @@
|
||||
}
|
||||
|
||||
- (void) waitForOutputDeviceToStartWithReply:(void (^)(NSError*))reply {
|
||||
OSStatus err = [audioDevices waitForOutputDeviceToStart];
|
||||
NSString* description;
|
||||
OSStatus err;
|
||||
|
||||
try {
|
||||
err = [audioDevices waitForOutputDeviceToStart];
|
||||
} catch (CAException e) {
|
||||
// waitForOutputDeviceToStart should never throw a CAException, but check anyway in case we change that at some point.
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
e.GetError());
|
||||
err = kBGMXPC_HardwareError;
|
||||
} catch (...) {
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
err = kBGMXPC_InternalError;
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (err) {
|
||||
case noErr:
|
||||
case kAudioHardwareNoError:
|
||||
description = @"BGMApp started the output device.";
|
||||
err = kBGMXPC_Success;
|
||||
break;
|
||||
@@ -194,6 +210,12 @@
|
||||
err = kBGMXPC_HardwareError;
|
||||
break;
|
||||
|
||||
case BGMPlayThrough::kDeviceNotStarting:
|
||||
// We have to send a more specific error in this case because BGMDevice handles this case differently.
|
||||
description = @"The output device is not starting.";
|
||||
err = kBGMXPC_HardwareNotStartingError;
|
||||
break;
|
||||
|
||||
default:
|
||||
description = @"Unknown error while waiting for the output device.";
|
||||
err = kBGMXPC_InternalError;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="14F1509" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/>
|
||||
<development version="7000" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12120"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -12,12 +13,12 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate">
|
||||
<customObject id="Voe-Tx-rLC" customClass="BGMAppDelegate">
|
||||
<connections>
|
||||
<outlet property="aboutPanel" destination="Cf4-3V-gl1" id="cgo-Hw-rE2"/>
|
||||
<outlet property="aboutPanelLicenseView" destination="LSG-PF-cl8" id="mbu-kv-Jfc"/>
|
||||
<outlet property="appVolumeView" destination="MWB-XH-kFI" id="eFA-RN-VMC"/>
|
||||
<outlet property="autoPauseMenuItem" destination="nHv-T8-1nb" id="skN-ap-dre"/>
|
||||
<outlet property="autoPauseMenuItemUnwrapped" destination="nHv-T8-1nb" id="Lie-Cx-jw6"/>
|
||||
<outlet property="bgmMenu" destination="8AN-nh-rEe" id="UWn-BX-eLy"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
@@ -26,9 +27,7 @@
|
||||
<items>
|
||||
<menuItem title="Auto-pause Music" tag="2" id="nHv-T8-1nb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutoPauseMusic:" target="Voe-Tx-rLC" id="fL3-wA-voV"/>
|
||||
</connections>
|
||||
<accessibility description="Enable to automatically pause your selected music player when a different app starts playing audio." identifier="Auto-pause enabled"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="ZGd-Pq-YeA"/>
|
||||
<menuItem title="App Volumes" tag="3" enabled="NO" id="8PP-wA-Pae">
|
||||
@@ -60,14 +59,16 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
<accessibility description="Background Music Main Menu" identifier="MainMenu"/>
|
||||
<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"/>
|
||||
<textField identifier="AppName" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xmd-bg-huG" customClass="BGMAVM_AppNameLabel">
|
||||
<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"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -75,92 +76,152 @@
|
||||
</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="17"/>
|
||||
<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="buttonCell:IXo-C7-3uE:image" 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" 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="748" height="293"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
|
||||
<rect key="contentRect" x="248" y="350" width="1002" height="335"/>
|
||||
<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="748" height="293"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="1002" height="335"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r51-dd-LGP">
|
||||
<rect key="frame" x="18" y="78" width="240" height="22"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r51-dd-LGP">
|
||||
<rect key="frame" x="71" 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"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="ekc-h0-I43">
|
||||
<rect key="frame" x="18" y="53" width="240" height="17"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ekc-h0-I43">
|
||||
<rect key="frame" x="71" 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"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L5P-Lw-aCd">
|
||||
<rect key="frame" x="299" y="256" width="270" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Licensed under GPLv2 or any later version." id="ETh-En-bzX">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L5P-Lw-aCd">
|
||||
<rect key="frame" x="413" 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 GPL v2 or any later version." id="ETh-En-bzX">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<box horizontalHuggingPriority="750" fixedFrame="YES" title="Box" boxType="separator" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="Zc9-gs-X8C">
|
||||
<rect key="frame" x="264" y="71" width="5" height="150"/>
|
||||
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<font key="titleFont" metaFont="system"/>
|
||||
<box horizontalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Zc9-gs-X8C">
|
||||
<rect key="frame" x="383" 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="28" width="240" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016 Kyle Neideck" placeholderString="" id="ctF-95-uVu">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
|
||||
<rect key="frame" x="18" y="75" width="346" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016, 2017 Background Music contributors" placeholderString="" id="ctF-95-uVu">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nx6-kQ-N8Z" customClass="BGMLinkField">
|
||||
<rect key="frame" x="36" 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"/>
|
||||
<color key="textColor" red="0.20000000000000001" green="0.40000000000000002" blue="0.59999999999999998" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Tui-Hf-FLv">
|
||||
<rect key="frame" x="63" y="108" width="150" height="150"/>
|
||||
<rect key="frame" x="116" 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"/>
|
||||
</shadow>
|
||||
<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="63" y="108" width="150" height="150"/>
|
||||
<rect key="frame" x="116" 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"/>
|
||||
</shadow>
|
||||
<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="301" y="28" width="427" height="220"/>
|
||||
<rect key="frame" x="415" 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="218"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<rect key="frame" x="1" y="1" width="565" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView ambiguous="YES" editable="NO" importsGraphics="NO" findStyle="panel" continuousSpellChecking="YES" allowsUndo="YES" usesRuler="YES" usesFontPanel="YES" verticallyResizable="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="218"/>
|
||||
<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="218"/>
|
||||
<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"/>
|
||||
<size key="minSize" width="425" height="218"/>
|
||||
<size key="maxSize" width="477" height="10000000"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
@@ -170,15 +231,24 @@
|
||||
<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="218"/>
|
||||
<rect key="frame" x="-15" y="1" width="16" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6qu-yI-r00">
|
||||
<rect key="frame" x="413" y="20" width="203" height="11"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<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"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
</view>
|
||||
<point key="canvasLocation" x="101" y="211.5"/>
|
||||
<point key="canvasLocation" x="-200" y="232.5"/>
|
||||
</window>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" id="IoN-sN-cCx">
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" allowsCharacterPickerTouchBarItem="YES" id="IoN-sN-cCx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="471" height="180"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" controlSize="mini" sendsActionOnEndEditing="YES" drawsBackground="YES" id="Ay8-8n-FHi">
|
||||
@@ -188,10 +258,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="FermataIcon" width="284" height="284"/>
|
||||
<image name="buttonCell:IXo-C7-3uE:image" 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>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,18 @@
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016 Kyle Neideck</string>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>OSAScriptingDefinition</key>
|
||||
<string>BGMApp.sdef</string>
|
||||
<key>NSServices</key>
|
||||
<array>
|
||||
<dict/>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMDecibel.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMDecibel : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMDecibel.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMDecibel.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Decibel.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMDecibel {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"A9790CD5-4886-47C7-9FFC-DD70743CF2BF"]
|
||||
name:@"Decibel"
|
||||
bundleID:@"org.sbooth.Decibel"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (DecibelApplication* __nullable) decibel {
|
||||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.decibel.running;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && self.decibel.playing;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
// We don't want to return true when Decibel is stopped, rather than paused. At least for me, Decibel
|
||||
// returns -1 for playbackTime and playbackPosition when it's neither playing nor paused.
|
||||
BOOL probablyNotStopped =
|
||||
self.decibel.playbackTime >= 0 || self.decibel.playbackPosition >= 0;
|
||||
|
||||
return self.running && !self.decibel.playing && probablyNotStopped;
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMDecibel::pause: Pausing Decibel");
|
||||
[self.decibel pause];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMDecibel::unpause: Unpausing Decibel");
|
||||
[self.decibel play];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMHermes.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMHermes : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMHermes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMHermes.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Hermes.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMHermes {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen. (Command line tool.)
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"0CDC67B0-56D3-4D94-BC06-6E380D8F5E34"]
|
||||
name:@"Hermes"
|
||||
bundleID:@"com.alexcrichton.Hermes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (HermesApplication* __nullable) hermes {
|
||||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.hermes is nil (i.e. Hermes isn't running).
|
||||
return self.hermes.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case Hermes is closed but self.hermes hasn't become
|
||||
// nil yet. In that case, reading self.hermes.playerState could make Scripting Bridge open Hermes.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.hermes.playbackState == HermesPlayerStatesPlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.hermes.playbackState == HermesPlayerStatesPaused);
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMHermes::pause: Pausing Hermes");
|
||||
[self.hermes pause];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMHermes::unpause: Unpausing Hermes");
|
||||
[self.hermes play];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -19,62 +19,83 @@
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
// The base class and protocol for music player apps. Also holds the state of the currently
|
||||
// selected music player.
|
||||
// The base classes and protocol for objects that represent a music player app.
|
||||
//
|
||||
// To add support for a music player, create a subclass of BGMMusicPlayerBase that implements
|
||||
// BGMMusicPlayerProtocol. BGMSpotify will probably be the most useful example.
|
||||
// To add support for a music player, create a class that implements the BGMMusicPlayer protocol
|
||||
// and add it to initWithAudioDevices in BGMMusicPlayers.mm.
|
||||
//
|
||||
// Include the BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD macro somewhere in the @implementation block.
|
||||
// You might also want to override the icon method if the default implementation from
|
||||
// BGMMusicPlayerBase doesn't work.
|
||||
// You'll probably want to subclass BGMMusicPlayerBase and, if the music player supports
|
||||
// AppleScript, use BGMScriptingBridge. Your class might need to override the icon method if the
|
||||
// default implementation from BGMMusicPlayerBase doesn't work.
|
||||
//
|
||||
// The music player classes written so far use Scripting Bridge to communicate with the music
|
||||
// player apps (see iTunes.h/Spotify.h) but any other way is fine too.
|
||||
// BGMSpotify will probably be the most useful example to follow, but they're all pretty
|
||||
// similar. The music player classes written so far all use Scripting Bridge to communicate with
|
||||
// the music player apps (see iTunes.h/Spotify.h) but any other way is fine too.
|
||||
//
|
||||
// BGMDriver will use either the music player's bundle ID or PID to match it to the audio it
|
||||
// plays. (Though using PIDs hasn't been tested yet.)
|
||||
//
|
||||
// If you're not sure what bundle ID the music player uses, install a debug build of BGMDriver
|
||||
// and play something in the music player. The easiest way is to do
|
||||
// build_and_install.sh -d
|
||||
// BGMDriver will log the bundle ID to system.log when it becomes aware of the music player.
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#define BGM_MUSIC_PLAYER_ADD_SELF_TO_CLASSES_LIST \
|
||||
[BGMMusicPlayerBase addToMusicPlayerClasses:[self class]];
|
||||
@protocol BGMMusicPlayer <NSObject>
|
||||
|
||||
#define BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD \
|
||||
+ (void) load { \
|
||||
BGM_MUSIC_PLAYER_ADD_SELF_TO_CLASSES_LIST \
|
||||
}
|
||||
// Classes return an instance of themselves for each music player app they make available in
|
||||
// BGMApp. So far that's always been a single instance, and classes haven't needed to override
|
||||
// the default implementation of createInstances from BGMMusicPlayerBase. But that will probably
|
||||
// change eventually.
|
||||
//
|
||||
// For example, a class for custom music players would probably return an instance for each
|
||||
// custom player the user has created. (Also note that it could return an empty array.) In that
|
||||
// case the class would probably restore some state from user defaults in its createInstances.
|
||||
//
|
||||
// TODO: I think the return type should actually be NSArray<instancetype>*, but that doesn't seem
|
||||
// to work. There's a Clang bug about this: https://llvm.org/bugs/show_bug.cgi?id=27323
|
||||
// (though it hasn't been confirmed yet).
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances;
|
||||
|
||||
// Forward declarations (just for the typedef)
|
||||
@class BGMMusicPlayerBase;
|
||||
@protocol BGMMusicPlayerProtocol;
|
||||
// We need a unique ID for each music player to store in user defaults. In the most common case,
|
||||
// classes that provide a static (or at least bounded) number of music players, you can generate
|
||||
// IDs with uuidgen (the command line tool) and include them in your class as constants. Otherwise,
|
||||
// you'll probably want to store them in user defaults and retrieve them in your createInstances.
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
|
||||
typedef BGMMusicPlayerBase<BGMMusicPlayerProtocol> BGMMusicPlayer;
|
||||
// The name and icon of the music player, to be used in the UI.
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
|
||||
@protocol BGMMusicPlayerProtocol
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
|
||||
@optional
|
||||
// Subclasses usually won't need to implement these unless the music player has no bundle ID.
|
||||
+ (id) initWithPID:(pid_t)pid;
|
||||
+ (id) initWithPIDFromNSNumber:(NSNumber*)pid;
|
||||
+ (id) initWithPIDFromCFNumber:(CFNumberRef)pid;
|
||||
// The pid of each instance of the music player app currently running
|
||||
+ (NSArray<NSNumber*>*) pidsOfRunningInstances;
|
||||
// Classes will usually ignore this property and leave it nil unless the music player has no
|
||||
// bundle ID.
|
||||
//
|
||||
// TODO: If we ever add a music player class that uses this property, it'll need a way to inform
|
||||
// BGMDevice of changes. It might be easiest to have BGMMusicPlayers to observe this property,
|
||||
// on the selected music player, with KVO and update BGMDevice when it changes. Or
|
||||
// BGMMusicPlayers could pass a pointer to itself to createInstances.
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
@required
|
||||
// The name of the music player, to be used in the UI
|
||||
+ (NSString*) name;
|
||||
|
||||
// The refs returned by the bundleID and pid methods don't need to be released by users, but may be
|
||||
// released by the class/instance at some point (get rule applies).
|
||||
+ (CFStringRef __nullable) bundleID;
|
||||
// Subclasses will usually always return NULL unless they implement the optional methods above.
|
||||
- (CFNumberRef __nullable) pid;
|
||||
|
||||
- (BOOL) isRunning;
|
||||
// The state of the music player.
|
||||
//
|
||||
// True if the music player app is open.
|
||||
@property (readonly, getter=isRunning) BOOL running;
|
||||
// True if the music player is playing a song or some other user-selected audio file. Note that
|
||||
// the music player playing audio for UI, notifications, etc. won't make this true (which is why we
|
||||
// need this property and can't just ask BGMDriver if the music player is playing audio).
|
||||
@property (readonly, getter=isPlaying) BOOL playing;
|
||||
// True if the music player has a current/open song (or whatever) and will continue playing it if
|
||||
// BGMMusicPlayer::unpause is called. Normally because the user was playing a song and they or
|
||||
// BGMApp paused it.
|
||||
@property (readonly, getter=isPaused) BOOL paused;
|
||||
|
||||
// Pause the music player. Does nothing if the music player is already paused or isn't running.
|
||||
// Returns YES if the music player is paused now but wasn't before, returns NO otherwise.
|
||||
@@ -84,27 +105,31 @@ typedef BGMMusicPlayerBase<BGMMusicPlayerProtocol> BGMMusicPlayer;
|
||||
// Returns YES if the music player is playing now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) unpause;
|
||||
|
||||
- (BOOL) isPlaying;
|
||||
|
||||
- (BOOL) isPaused;
|
||||
|
||||
@end
|
||||
|
||||
@interface BGMMusicPlayerBase : NSObject <SBApplicationDelegate>
|
||||
|
||||
+ (NSArray*) musicPlayerClasses;
|
||||
+ (void) addToMusicPlayerClasses:(Class)musicPlayerClass;
|
||||
@interface BGMMusicPlayerBase : NSObject
|
||||
|
||||
// The music player currently selected in the preferences menu. (There's no real reason for this to be
|
||||
// global or in this class. I was just trying it out of curiosity.)
|
||||
+ (BGMMusicPlayer*) selectedMusicPlayer;
|
||||
+ (void) setSelectedMusicPlayer:(BGMMusicPlayer*)musicPlayer;
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
+ (NSImage* __nullable) icon;
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid;
|
||||
|
||||
// If the music player application is running, the scripting bridge object representing it. Otherwise
|
||||
// nil.
|
||||
@property (readonly) __kindof SBApplication* __nullable sbApplication;
|
||||
// Convenience wrapper around NSUUID's initWithUUIDString. musicPlayerIDString must be a string
|
||||
// generated by uuidgen (command line tool), e.g. "60BA9739-B6DD-4E6A-8134-51410A45BB84".
|
||||
+ (NSUUID*) makeID:(NSString*)musicPlayerIDString;
|
||||
|
||||
// BGMMusicPlayer default implementations
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -28,135 +28,60 @@
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMMusicPlayerBase {
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id didLaunchToken;
|
||||
id didTerminateToken;
|
||||
@implementation BGMMusicPlayerBase
|
||||
|
||||
@synthesize musicPlayerID = _musicPlayerID;
|
||||
@synthesize name = _name;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
return [self initWithMusicPlayerID:musicPlayerID name:name bundleID:bundleID pid:nil];
|
||||
}
|
||||
|
||||
@synthesize sbApplication = sbApplication;
|
||||
|
||||
// A array of the subclasses of BGMMusicPlayer
|
||||
static NSArray* sMusicPlayerClasses;
|
||||
|
||||
// The user-selected music player. One of BGMMusicPlayer's subclasses declares itself the default music player by
|
||||
// setting this to an instance of itself in its load method.
|
||||
static BGMMusicPlayer* sSelectedMusicPlayer;
|
||||
|
||||
// Load-time static initializer
|
||||
+ (void) load {
|
||||
sMusicPlayerClasses = @[];
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid {
|
||||
if ((self = [super init])) {
|
||||
NSString* bundleID = (__bridge NSString*)[[self class] bundleID];
|
||||
NSAssert(musicPlayerID, @"BGMMusicPlayerBase::initWithMusicPlayerID: !musicPlayerID");
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
sbApplication = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
sbApplication.delegate = self;
|
||||
};
|
||||
NSAssert([self conformsToProtocol:@protocol(BGMMusicPlayer)],
|
||||
@"BGMMusicPlayerBase::initWithMusicPlayerID: !conformsToProtocol");
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note){
|
||||
return [[note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier] isEqualToString:bundleID];
|
||||
};
|
||||
|
||||
// Add observers that create/destroy the SBApplication when the music player is launched/terminated. We
|
||||
// only create the SBApplication when the music player is open because, if it isn't, creating the
|
||||
// SBApplication, or sending it events, could launch the music player. Whether it does or not depends on
|
||||
// the music player, and possibly the version of the music player, so to be safe we assume they all do.
|
||||
//
|
||||
// From the docs for SBApplication's applicationWithBundleIdentifier method:
|
||||
// "For applications that declare themselves to have a dynamic scripting interface, this method will
|
||||
// launch the application if it is not already running."
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
#if DEBUG
|
||||
const char* mpName = [[[self class] name] UTF8String];
|
||||
#endif
|
||||
didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMMusicPlayer::init: %s launched", mpName);
|
||||
createSBApplication();
|
||||
}
|
||||
}];
|
||||
didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMMusicPlayer::init: %s terminated", mpName);
|
||||
sbApplication = nil;
|
||||
}
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([[NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID] count] > 0) {
|
||||
createSBApplication();
|
||||
}
|
||||
_musicPlayerID = musicPlayerID;
|
||||
_name = name;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) eventDidFail:(const AppleEvent*)event withError:(NSError*)error {
|
||||
// SBApplicationDelegate method. So far, this just logs the error.
|
||||
+ (NSUUID*) makeID:(NSString*)musicPlayerIDString {
|
||||
NSUUID* __nullable musicPlayerID = [[NSUUID alloc] initWithUUIDString:musicPlayerIDString];
|
||||
NSAssert(musicPlayerID, @"BGMMusicPlayerBase::makeID: !musicPlayerID");
|
||||
|
||||
#if DEBUG
|
||||
NSString* vars = [NSString stringWithFormat:@"event=%@ error=%@ sbApplication=%@", event, error, sbApplication];
|
||||
DebugMsg("BGMMusicPlayer::eventDidFail: Apple event sent to %s failed. %s",
|
||||
[[[self class] name] UTF8String],
|
||||
[vars UTF8String]);
|
||||
#else
|
||||
#pragma unused (event, error)
|
||||
#endif
|
||||
return (NSUUID*)musicPlayerID;
|
||||
}
|
||||
|
||||
#pragma mark BGMMusicPlayer default implementations
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances {
|
||||
return @[ [self new] ];
|
||||
}
|
||||
|
||||
- (NSImage* __nullable) icon {
|
||||
NSString* __nullable bundleID = self.bundleID;
|
||||
NSString* __nullable bundlePath =
|
||||
(!bundleID ? nil : [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:(NSString*)bundleID]);
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void) addToMusicPlayerClasses:(Class)musicPlayerClass {
|
||||
sMusicPlayerClasses = [sMusicPlayerClasses arrayByAddingObject:musicPlayerClass];
|
||||
}
|
||||
|
||||
+ (NSArray*) musicPlayerClasses {
|
||||
return sMusicPlayerClasses;
|
||||
}
|
||||
|
||||
+ (BGMMusicPlayer*) selectedMusicPlayer {
|
||||
NSAssert(sSelectedMusicPlayer != nil, @"One of BGMMusicPlayer's subclasses should set itself as the default "
|
||||
"music player (i.e. set sSelectedMusicPlayer) in its initialize method");
|
||||
return sSelectedMusicPlayer;
|
||||
}
|
||||
|
||||
+ (void) setSelectedMusicPlayer:(BGMMusicPlayer*)musicPlayer {
|
||||
sSelectedMusicPlayer = musicPlayer;
|
||||
}
|
||||
|
||||
+ (NSImage* __nullable) icon {
|
||||
NSString* bundleID = (__bridge NSString*)[(id<BGMMusicPlayerProtocol>)self bundleID];
|
||||
NSString* bundlePath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:bundleID];
|
||||
return bundlePath == nil ? nil : [[NSWorkspace sharedWorkspace] iconForFile:bundlePath];
|
||||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMMusicPlayers.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
// Holds the music players (i.e. BGMMusicPlayer objects) available in BGMApp. Also keeps track of
|
||||
// which music player is currently selected by the user.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMMusicPlayer.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMMusicPlayers : NSObject
|
||||
|
||||
// Calls initWithAudioDevices:musicPlayers: with sensible defaults.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults;
|
||||
|
||||
// defaultMusicPlayerID is the musicPlayerID (see BGMMusicPlayer.h) of the music player that should be
|
||||
// selected by default.
|
||||
//
|
||||
// The createInstances method of each class in musicPlayerClasses will be called, and the results stored
|
||||
// in the musicPlayers property.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID
|
||||
musicPlayerClasses:(NSArray<Class<BGMMusicPlayer>>*)musicPlayerClasses
|
||||
userDefaults:(BGMUserDefaults*)defaults;
|
||||
|
||||
@property (readonly) NSArray<id<BGMMusicPlayer>>* musicPlayers;
|
||||
|
||||
// The music player currently selected in the preferences menu. BGMDevice is informed when this property
|
||||
// is changed.
|
||||
@property id<BGMMusicPlayer> selectedMusicPlayer;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMMusicPlayers.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// Local includes
|
||||
#import "BGM_Types.h"
|
||||
|
||||
// Music player includes
|
||||
#import "BGMiTunes.h"
|
||||
#import "BGMSpotify.h"
|
||||
#import "BGMVLC.h"
|
||||
#import "BGMVOX.h"
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMHermes.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMMusicPlayers {
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
BGMUserDefaults* userDefaults;
|
||||
}
|
||||
|
||||
@synthesize selectedMusicPlayer = _selectedMusicPlayer;
|
||||
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
return [self initWithAudioDevices:devices
|
||||
defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
// If you write a new music player class, add it to this array.
|
||||
musicPlayerClasses:@[ [BGMVOX class],
|
||||
[BGMVLC class],
|
||||
[BGMSpotify class],
|
||||
[BGMiTunes class],
|
||||
[BGMDecibel class],
|
||||
[BGMHermes class] ]
|
||||
userDefaults:defaults];
|
||||
}
|
||||
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID
|
||||
musicPlayerClasses:(NSArray<Class<BGMMusicPlayer>>*)musicPlayerClasses
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
if ((self = [super init])) {
|
||||
audioDevices = devices;
|
||||
userDefaults = defaults;
|
||||
|
||||
// Init _musicPlayers, an array containing one object for each music player in BGMApp.
|
||||
//
|
||||
// Each music player class has a factory method, createInstances, that returns all the instances of that
|
||||
// class BGMApp will use. (Though so far it's always just one instance.)
|
||||
NSMutableArray* musicPlayers = [NSMutableArray new];
|
||||
for (Class<BGMMusicPlayer> musicPlayerClass in musicPlayerClasses) {
|
||||
[musicPlayers addObjectsFromArray:[musicPlayerClass createInstances]];
|
||||
}
|
||||
|
||||
_musicPlayers = [NSArray arrayWithArray:musicPlayers];
|
||||
|
||||
// Set _selectedMusicPlayer to its setting from last time BGMApp ran. (Unless this is the first run or
|
||||
// that music player isn't available this time.)
|
||||
[self initSelectedMusicPlayerFromUserDefaults];
|
||||
|
||||
if (!_selectedMusicPlayer) {
|
||||
// Couldn't set _selectedMusicPlayer from user defaults, so try BGMDevice's music player property.
|
||||
[self initSelectedMusicPlayerFromBGMDevice];
|
||||
}
|
||||
|
||||
if (!_selectedMusicPlayer) {
|
||||
// The user hasn't changed the music player yet, so we set the default music player as selected.
|
||||
[self setSelectedMusicPlayerByID:defaultMusicPlayerID];
|
||||
}
|
||||
|
||||
NSAssert(_selectedMusicPlayer, @"BGMMusicPlayers::initWithAudioDevices: !_selectedMusicPlayer");
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initSelectedMusicPlayerFromUserDefaults {
|
||||
// Load the selected music player setting from user defaults.
|
||||
|
||||
NSString* __nullable selectedMusicPlayerIDStr = userDefaults.selectedMusicPlayerID;
|
||||
NSUUID* __nullable selectedMusicPlayerID = nil;
|
||||
|
||||
if (selectedMusicPlayerIDStr) {
|
||||
NSString* idStrNN = selectedMusicPlayerIDStr;
|
||||
selectedMusicPlayerID = [[NSUUID alloc] initWithUUIDString:idStrNN];
|
||||
|
||||
NSAssert(selectedMusicPlayerID,
|
||||
@"BGMMusicPlayers::initSelectedMusicPlayerFromUserDefaults: !selectedMusicPlayerID");
|
||||
}
|
||||
|
||||
if (selectedMusicPlayerID) {
|
||||
NSUUID* idNN = selectedMusicPlayerID;
|
||||
BOOL didChangeMusicPlayer = [self setSelectedMusicPlayerByID:idNN];
|
||||
|
||||
#if DEBUG
|
||||
DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromUserDefaults: %s selectedMusicPlayerIDStr=%s",
|
||||
(didChangeMusicPlayer ?
|
||||
"Selected music player restored from user defaults." :
|
||||
"The selected music player setting found in user defaults didn't match an available music player."),
|
||||
selectedMusicPlayerIDStr.UTF8String);
|
||||
#else
|
||||
#pragma unused (didChangeMusicPlayer)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void) initSelectedMusicPlayerFromBGMDevice {
|
||||
// When the selected music player setting hasn't been stored in user defaults yet, we get the music player
|
||||
// bundle ID from the driver and look for the music player with that bundle ID. This is mainly done for
|
||||
// backwards compatability.
|
||||
|
||||
NSString* __nullable bundleID =
|
||||
(__bridge_transfer NSString* __nullable)[audioDevices bgmDevice].GetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress);
|
||||
|
||||
DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromBGMDevice: "
|
||||
"Trying to set selected music player by bundle ID (from BGMDriver). bundleID=%s",
|
||||
(bundleID ? bundleID.UTF8String : "(null)"));
|
||||
|
||||
if (bundleID && ![bundleID isEqualToString:@""]) {
|
||||
// Find any music players with a bundle ID matching the one from BGMDriver.
|
||||
NSArray<id<BGMMusicPlayer>>* matchingMusicPlayers = @[ ];
|
||||
|
||||
for (id<BGMMusicPlayer> musicPlayer in _musicPlayers) {
|
||||
NSString* bundleIDNN = bundleID;
|
||||
if ([musicPlayer.bundleID isEqualToString:bundleIDNN]) {
|
||||
DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromBGMDevice: Bundle ID on BGMDevice matches %s",
|
||||
musicPlayer.name.UTF8String);
|
||||
|
||||
matchingMusicPlayers = [matchingMusicPlayers arrayByAddingObject:musicPlayer];
|
||||
}
|
||||
}
|
||||
|
||||
// Currently, the music players all have different bundle IDs, but that might change at some point. We
|
||||
// might want to consider some websites as music players, for example. So we don't change the setting
|
||||
// unless the bundle ID only matches one music player.
|
||||
if (matchingMusicPlayers.count == 1) {
|
||||
// (Use setSelectedMusicPlayerImpl to avoid setSelectedMusicPlayer being called in init.)
|
||||
[self setSelectedMusicPlayerImpl:matchingMusicPlayers[0]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id<BGMMusicPlayer>) selectedMusicPlayer {
|
||||
return _selectedMusicPlayer;
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayer:(id<BGMMusicPlayer>)newSelectedMusicPlayer {
|
||||
// Apparently you shouldn't call properties' setter methods in init (KVO notifications might trigger, etc.)
|
||||
// so the actual work is done in setSelectedMusicPlayerImpl.
|
||||
[self setSelectedMusicPlayerImpl:newSelectedMusicPlayer];
|
||||
|
||||
NSAssert(self.selectedMusicPlayer == newSelectedMusicPlayer,
|
||||
@"BGMMusicPlayers::setSelectedMusicPlayer: selectedMusicPlayer wasn't set to the object expected");
|
||||
}
|
||||
|
||||
- (BOOL) setSelectedMusicPlayerByID:(NSUUID*)newSelectedMusicPlayerID {
|
||||
id<BGMMusicPlayer> __nullable newSelectedMusicPlayer = nil;
|
||||
|
||||
// Find the music player with the given ID, if there is one.
|
||||
for (id<BGMMusicPlayer> musicPlayer in _musicPlayers) {
|
||||
if ([musicPlayer.musicPlayerID isEqual:newSelectedMusicPlayerID]) {
|
||||
NSAssert(!newSelectedMusicPlayer, @"BGMMusicPlayers::setSelectedMusicPlayerByID: Non-unique musicPlayerID");
|
||||
|
||||
newSelectedMusicPlayer = musicPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
if (newSelectedMusicPlayer) {
|
||||
// (Use setSelectedMusicPlayerImpl to avoid setSelectedMusicPlayer being called in init.)
|
||||
id<BGMMusicPlayer> newPlayerNN = newSelectedMusicPlayer;
|
||||
[self setSelectedMusicPlayerImpl:newPlayerNN];
|
||||
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayerImpl:(id<BGMMusicPlayer>)newSelectedMusicPlayer {
|
||||
NSAssert([_musicPlayers containsObject:newSelectedMusicPlayer],
|
||||
@"BGMMusicPlayers::setSelectedMusicPlayerImpl: Only the music players in the musicPlayers array can be selected. "
|
||||
"newSelectedMusicPlayer=%@",
|
||||
newSelectedMusicPlayer.name);
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: Set selected music player to %s",
|
||||
_selectedMusicPlayer.name.UTF8String);
|
||||
|
||||
// Update the selected music player on the driver.
|
||||
[self updateBGMDeviceMusicPlayerProperties];
|
||||
|
||||
// Save the new setting in user defaults.
|
||||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
// Send the music player's PID and/or bundle ID to the driver.
|
||||
|
||||
NSAssert(self.selectedMusicPlayer.pid || self.selectedMusicPlayer.bundleID,
|
||||
@"BGMMusicPlayers::updateBGMDeviceMusicPlayerProperties: Music player has neither bundle ID nor PID");
|
||||
|
||||
if (self.selectedMusicPlayer.pid) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress,
|
||||
(__bridge CFNumberRef)self.selectedMusicPlayer.pid);
|
||||
}
|
||||
|
||||
if (self.selectedMusicPlayer.bundleID) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress,
|
||||
(__bridge CFStringRef)self.selectedMusicPlayer.bundleID);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMScriptingBridge.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
// A wrapper around Scripting Bridge's SBApplication that tries to avoid ever launching the application.
|
||||
//
|
||||
// We use Scripting Bridge to communicate with music player apps, which we never want to launch
|
||||
// ourselves. But creating an SBApplication for an app, or sending messages/events to an existing one,
|
||||
// can launch the app.
|
||||
//
|
||||
// As a workaround, this class has an SBApplication property, application (see below), which is nil
|
||||
// unless the music player app is running. That way messages sent while the app is closed are ignored.
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMScriptingBridge : NSObject <SBApplicationDelegate>
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID;
|
||||
|
||||
// If the music player application is running, this property is the Scripting Bridge object representing
|
||||
// it. If not, it's set to nil. Used to send Apple events to the music player app.
|
||||
@property (readonly) __kindof SBApplication* __nullable application;
|
||||
|
||||
// SBApplicationDelegate
|
||||
|
||||
// On 10.11, SBApplicationDelegate.h declares eventDidFail with a non-null return type, but the docs
|
||||
// specifically say that returning nil is allowed.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnullability"
|
||||
- (id __nullable) eventDidFail:(const AppleEvent*)event withError:(NSError*)error;
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMScriptingBridge {
|
||||
NSString* bundleID;
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id didLaunchToken, didTerminateToken;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)inBundleID {
|
||||
if ((self = [super init])) {
|
||||
bundleID = inBundleID;
|
||||
|
||||
[self initApplication];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initApplication {
|
||||
void (^createSBApplication)(void) = ^{
|
||||
_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
_application.delegate = self;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
return [[note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier] isEqualToString:bundleID];
|
||||
};
|
||||
|
||||
// Add observers that create/destroy the SBApplication when the music player is launched/terminated. We
|
||||
// only create the SBApplication when the music player is open. If it isn't open, creating the
|
||||
// SBApplication or sending it events could launch the music player. Whether or not it does depends on
|
||||
// the music player, and possibly the version of the music player, so to be safe we assume they all do.
|
||||
//
|
||||
// From the docs for SBApplication's applicationWithBundleIdentifier method:
|
||||
// "For applications that declare themselves to have a dynamic scripting interface, this method will
|
||||
// launch the application if it is not already running."
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
}
|
||||
}];
|
||||
didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
_application = nil;
|
||||
}
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) {
|
||||
createSBApplication();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark SBApplicationDelegate
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnullability" // See explanation in the header file.
|
||||
- (id __nullable) eventDidFail:(const AppleEvent*)event withError:(NSError*)error {
|
||||
#pragma clang diagnostic pop
|
||||
// So far, this just logs the error.
|
||||
|
||||
#if DEBUG
|
||||
NSString* vars = [NSString stringWithFormat:@"event='%4.4s' error=%@ application=%@",
|
||||
(char*)&(event->descriptorType), error, self.application];
|
||||
DebugMsg("BGMScriptingBridge::eventDidFail: Apple event sent to %s failed. %s",
|
||||
bundleID.UTF8String,
|
||||
vars.UTF8String);
|
||||
#else
|
||||
#pragma unused (event, error)
|
||||
#endif
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMSpotify : BGMMusicPlayer
|
||||
@interface BGMSpotify : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -29,39 +29,55 @@
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Spotify.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
@implementation BGMSpotify
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
+ (NSString*) name {
|
||||
return @"Spotify";
|
||||
@implementation BGMSpotify {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (CFNumberRef) pid {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+ (CFStringRef) bundleID {
|
||||
return CFSTR("com.spotify.client");
|
||||
- (id) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen. (Command line tool.)
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"EC2A907F-8515-4687-9570-1BF63176E6D8"]
|
||||
name:@"Spotify"
|
||||
bundleID:@"com.spotify.client"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (SpotifyApplication* __nullable) spotify {
|
||||
return (SpotifyApplication*) self.sbApplication;
|
||||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.spotify && [self.spotify isRunning];
|
||||
// Note that this will return NO if is self.spotify is nil (i.e. Spotify isn't running).
|
||||
return self.spotify.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case Spotify is closed but self.spotify hasn't become
|
||||
// nil yet. In that case, reading self.spotify.playerState could make Scripting Bridge open Spotify.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.spotify.playerState == SpotifyEPlSPlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.spotify.playerState == SpotifyEPlSPaused);
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = [self isPlaying];
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMSpotify::pause: Pausing Spotify");
|
||||
@@ -73,7 +89,7 @@ BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = [self isPaused];
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMSpotify::unpause: Unpausing Spotify");
|
||||
@@ -83,13 +99,7 @@ BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return [self isRunning] && [self.spotify playerState] == SpotifyEPlSPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return [self isRunning] && [self.spotify playerState] == SpotifyEPlSPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMVLC : BGMMusicPlayer
|
||||
@interface BGMVLC : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -27,43 +27,58 @@
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "VLC.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
@implementation BGMVLC
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
+ (NSString*) name {
|
||||
return @"VLC";
|
||||
@implementation BGMVLC {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (CFNumberRef) pid {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+ (CFStringRef) bundleID {
|
||||
return CFSTR("org.videolan.vlc");
|
||||
- (id) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"]
|
||||
name:@"VLC"
|
||||
bundleID:@"org.videolan.vlc"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (VLCApplication* __nullable) vlc {
|
||||
return (VLCApplication*) self.sbApplication;
|
||||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vlc && [self.vlc isRunning];
|
||||
return self.vlc.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case VLC is closed but self.vlc hasn't become
|
||||
// nil yet. In that case, reading other properties of self.vlc could make Scripting Bridge open VLC.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && self.vlc.playing;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
// VLC is paused if it has a file open but isn't playing it
|
||||
return self.running && (self.vlc.nameOfCurrentItem != nil) && !self.vlc.playing;
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = [self isPlaying];
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMVLC::pause: Pausing VLC");
|
||||
[self togglePlay];
|
||||
[BGMVLC togglePlay];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
@@ -71,30 +86,21 @@ BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = [self isPaused];
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMVLC::unpause: Unpausing VLC");
|
||||
[self togglePlay];
|
||||
[BGMVLC togglePlay];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return [self isRunning] && [self.vlc playing];
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
// VLC is paused if it has a file open but isn't playing it
|
||||
return [self isRunning] && [self.vlc nameOfCurrentItem] != nil && ![self.vlc playing];
|
||||
}
|
||||
|
||||
// This is from SubTTS's STVLCPlayer class:
|
||||
// https://github.com/heatherleaf/subtts-mac/blob/master/SubTTS/STVLCPlayer.m
|
||||
//
|
||||
// VLC's Scripting Bridge interface doesn't seem to have a cleaner way to do this.
|
||||
- (void) togglePlay {
|
||||
+ (void) togglePlay {
|
||||
NSString* src = @"tell application \"VLC\" to play";
|
||||
NSAppleScript* script = [[NSAppleScript alloc] initWithSource:src];
|
||||
[script executeAndReturnError:nil];
|
||||
@@ -102,3 +108,5 @@ BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMVox.h
|
||||
// BGMVOX.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
@@ -24,7 +24,7 @@
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMVox : BGMMusicPlayer
|
||||
@interface BGMVOX : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,17 +14,20 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMVox.m
|
||||
// BGMVOX.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMVox.h"
|
||||
#import "BGMVOX.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Vox.h"
|
||||
#import "VOX.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
@@ -32,36 +35,49 @@
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
@implementation BGMVox
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
+ (NSString*) name {
|
||||
return @"VOX";
|
||||
@implementation BGMVOX {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (CFNumberRef) pid {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+ (CFStringRef) bundleID {
|
||||
return CFSTR("com.coppertino.Vox");
|
||||
- (id) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"26498C5D-C18B-4689-8B41-9DA91A78FFAD"]
|
||||
name:@"VOX"
|
||||
bundleID:@"com.coppertino.Vox"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (VoxApplication* __nullable) vox {
|
||||
return (VoxApplication*) self.sbApplication;
|
||||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vox && [self.vox isRunning];
|
||||
return self.vox.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case VOX is closed but self.vox hasn't become
|
||||
// nil yet. In that case, reading self.vox.playerState could make Scripting Bridge open VOX.
|
||||
//
|
||||
// VOX's comment for its playerState property says "playing = 1, paused = 0".
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.vox.playerState == 1);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.vox.playerState == 0);
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = [self isPlaying];
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMVox::pause: Pausing VOX");
|
||||
DebugMsg("BGMVOX::pause: Pausing VOX");
|
||||
[self.vox pause];
|
||||
}
|
||||
|
||||
@@ -70,25 +86,17 @@ BGM_MUSIC_PLAYER_DEFAULT_LOAD_METHOD
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = [self isPaused];
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMVox::unpause: Unpausing VOX");
|
||||
DebugMsg("BGMVOX::unpause: Unpausing VOX");
|
||||
[self.vox playpause];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
// Vox's comment for playerState says "playing = 1, paused = 0"
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return [self isRunning] && [self.vox playerState] == 1;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return [self isRunning] && [self.vox playerState] == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
@interface BGMiTunes : BGMMusicPlayer
|
||||
@interface BGMiTunes : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
// The music player ID (see BGMMusicPlayer.h) used by BGMiTunes instances. (Though BGMApp only ever creates one instance of
|
||||
// BGMiTunes, sharedMusicPlayerID is exposed so iTunes can be set as the default music player.)
|
||||
+ (NSUUID*) sharedMusicPlayerID;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -26,44 +26,59 @@
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "iTunes.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
@implementation BGMiTunes
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
+ (void) load {
|
||||
BGM_MUSIC_PLAYER_ADD_SELF_TO_CLASSES_LIST
|
||||
@implementation BGMiTunes {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
+ (NSUUID*) sharedMusicPlayerID {
|
||||
NSUUID* __nullable musicPlayerID = [[NSUUID alloc] initWithUUIDString:@"7B62B5BF-CF90-4938-84E3-F16DEDC3F608"];
|
||||
NSAssert(musicPlayerID, @"BGMiTunes::sharedMusicPlayerID: !musicPlayerID");
|
||||
return (NSUUID*)musicPlayerID;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
name:@"iTunes"
|
||||
bundleID:@"com.apple.iTunes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString* __nonnull)self.bundleID];
|
||||
}
|
||||
|
||||
// iTunes is selected as the music player when the user hasn't changed the setting yet
|
||||
[self setSelectedMusicPlayer:[BGMiTunes new]];
|
||||
}
|
||||
|
||||
+ (NSString*) name {
|
||||
return @"iTunes";
|
||||
}
|
||||
|
||||
- (CFNumberRef) pid {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+ (CFStringRef) bundleID {
|
||||
return CFSTR("com.apple.iTunes");
|
||||
return self;
|
||||
}
|
||||
|
||||
- (iTunesApplication* __nullable) iTunes {
|
||||
return (iTunesApplication*) self.sbApplication;
|
||||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.iTunes && [self.iTunes isRunning];
|
||||
return self.iTunes.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case iTunes is closed but self.iTunes hasn't become
|
||||
// nil yet. In that case, reading self.iTunes.playerState could make Scripting Bridge open iTunes.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.iTunes.playerState == iTunesEPlSPlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.iTunes.playerState == iTunesEPlSPaused);
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPlaying = [self isPlaying];
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMiTunes::pause: Pausing iTunes");
|
||||
@@ -75,7 +90,7 @@
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event
|
||||
BOOL wasPaused = [self isPaused];
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMiTunes::unpause: Unpausing iTunes");
|
||||
@@ -85,13 +100,7 @@
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return [self isRunning] && [self.iTunes playerState] == iTunesEPlSPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return [self isRunning] && [self.iTunes playerState] == iTunesEPlSPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Decibel.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /Applications/Decibel.app | sdp -fh --basename Decibel
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class DecibelApplication, DecibelDocument, DecibelWindow, DecibelApplication, DecibelTrack;
|
||||
|
||||
enum DecibelSaveOptions {
|
||||
DecibelSaveOptionsYes = 'yes ' /* Save the file. */,
|
||||
DecibelSaveOptionsNo = 'no ' /* Do not save the file. */,
|
||||
DecibelSaveOptionsAsk = 'ask ' /* Ask the user whether or not to save the file. */
|
||||
};
|
||||
typedef enum DecibelSaveOptions DecibelSaveOptions;
|
||||
|
||||
enum DecibelPrintingErrorHandling {
|
||||
DecibelPrintingErrorHandlingStandard = 'lwst' /* Standard PostScript error handling */,
|
||||
DecibelPrintingErrorHandlingDetailed = 'lwdt' /* print a detailed report of PostScript errors */
|
||||
};
|
||||
typedef enum DecibelPrintingErrorHandling DecibelPrintingErrorHandling;
|
||||
|
||||
enum DecibelShuffleMode {
|
||||
DecibelShuffleModeOff = 'off ' /* Off */,
|
||||
DecibelShuffleModeTrack = 'trck' /* Track */,
|
||||
DecibelShuffleModeAlbum = 'albm' /* Album */,
|
||||
DecibelShuffleModeArtist = 'arts' /* Artist */
|
||||
};
|
||||
typedef enum DecibelShuffleMode DecibelShuffleMode;
|
||||
|
||||
enum DecibelRepeatMode {
|
||||
DecibelRepeatModeOff = 'off ' /* Off */,
|
||||
DecibelRepeatModeTrack = 'trck' /* Track */,
|
||||
DecibelRepeatModeAlbum = 'albm' /* Album */,
|
||||
DecibelRepeatModeArtist = 'arts' /* Artist */,
|
||||
DecibelRepeatModeAll = 'all ' /* All */
|
||||
};
|
||||
typedef enum DecibelRepeatMode DecibelRepeatMode;
|
||||
|
||||
@protocol DecibelGenericMethods
|
||||
|
||||
- (void) closeSaving:(DecibelSaveOptions)saving savingIn:(NSURL *)savingIn; // Close a document.
|
||||
- (void) saveIn:(NSURL *)in_ as:(id)as; // Save a document.
|
||||
- (void) printWithProperties:(NSDictionary *)withProperties printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void) duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy an object.
|
||||
- (void) moveTo:(SBObject *)to; // Move an object to a new location.
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Standard Suite
|
||||
*/
|
||||
|
||||
// The application's top-level scripting object.
|
||||
@interface DecibelApplication : SBApplication
|
||||
|
||||
- (SBElementArray<DecibelDocument *> *) documents;
|
||||
- (SBElementArray<DecibelWindow *> *) windows;
|
||||
|
||||
@property (copy, readonly) NSString *name; // The name of the application.
|
||||
@property (readonly) BOOL frontmost; // Is this the active application?
|
||||
@property (copy, readonly) NSString *version; // The version number of the application.
|
||||
|
||||
- (id) open:(id)x; // Open a document.
|
||||
- (void) print:(id)x withProperties:(NSDictionary *)withProperties printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) quitSaving:(DecibelSaveOptions)saving; // Quit the application.
|
||||
- (BOOL) exists:(id)x; // Verify that an object exists.
|
||||
- (void) play; // Begin audio playback
|
||||
- (void) pause; // Suspend audio playback
|
||||
- (void) stop; // Stop audio playback
|
||||
- (void) playPause; // Begin or suspend audio playback
|
||||
- (void) seekForward; // Seek forward three seconds
|
||||
- (void) seekBackward; // Seek backward three seconds
|
||||
- (void) playSelection; // Play the selected track, or the first track if more than one are selected
|
||||
- (void) playPreviousTrack; // Play the previous logical track in the playlist
|
||||
- (void) playNextTrack; // Play the next logical track in the playlist
|
||||
- (void) addFile:(NSURL *)x; // Add a file to the playlist
|
||||
- (void) playFile:(NSURL *)x; // Add a file to the playlist and play it
|
||||
- (void) playTrackAtIndex:(NSInteger)x; // Play a track in the playlist
|
||||
- (void) increaseDeviceVolume; // Increase the device volume
|
||||
- (void) decreaseDeviceVolume; // Decrease the device volume
|
||||
- (void) increaseDigitalVolume; // Increase the digital volume
|
||||
- (void) decreaseDigitalVolume; // Decrease the digital volume
|
||||
- (void) clearPlaylist; // Clear the playlist
|
||||
- (void) scramblePlaylist; // Scramble the playlist
|
||||
|
||||
@end
|
||||
|
||||
// A document.
|
||||
@interface DecibelDocument : SBObject <DecibelGenericMethods>
|
||||
|
||||
@property (copy, readonly) NSString *name; // Its name.
|
||||
@property (readonly) BOOL modified; // Has it been modified since the last save?
|
||||
@property (copy, readonly) NSURL *file; // Its location on disk, if it has one.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A window.
|
||||
@interface DecibelWindow : SBObject <DecibelGenericMethods>
|
||||
|
||||
@property (copy, readonly) NSString *name; // The title of the window.
|
||||
- (NSInteger) id; // The unique identifier of the window.
|
||||
@property NSInteger index; // The index of the window, ordered front to back.
|
||||
@property NSRect bounds; // The bounding rectangle of the window.
|
||||
@property (readonly) BOOL closeable; // Does the window have a close button?
|
||||
@property (readonly) BOOL miniaturizable; // Does the window have a minimize button?
|
||||
@property BOOL miniaturized; // Is the window minimized right now?
|
||||
@property (readonly) BOOL resizable; // Can the window be resized?
|
||||
@property BOOL visible; // Is the window visible right now?
|
||||
@property (readonly) BOOL zoomable; // Does the window have a zoom button?
|
||||
@property BOOL zoomed; // Is the window zoomed right now?
|
||||
@property (copy, readonly) DecibelDocument *document; // The document whose contents are displayed in the window.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Decibel Scripting Suite
|
||||
*/
|
||||
|
||||
// The Decibel application class.
|
||||
@interface DecibelApplication (DecibelScriptingSuite)
|
||||
|
||||
- (SBElementArray<DecibelTrack *> *) tracks;
|
||||
|
||||
@property (readonly) BOOL playing; // Is the player currently playing?
|
||||
@property (readonly) BOOL shuffling; // Is the player currently shuffling?
|
||||
@property (readonly) BOOL repeating; // Is the player currently repeating?
|
||||
@property (copy, readonly) DecibelTrack *nowPlaying; // The track that is currently playing?
|
||||
@property double deviceVolume; // The current device volume
|
||||
@property double digitalVolume; // The current digital volume
|
||||
@property double playbackPosition; // The current playback position [0, 1]
|
||||
@property double playbackTime; // The current playback time in seconds
|
||||
@property (readonly) BOOL canPlay; // Is the player currently playing?
|
||||
@property (readonly) BOOL canPlayPreviousTrack; // Is the player currently playing?
|
||||
@property (readonly) BOOL canPlayNextTrack; // Is the player currently playing?
|
||||
@property (readonly) BOOL canAdjustDeviceVolume; // Can the device volume be adjusted?
|
||||
@property DecibelShuffleMode shuffleMode; // Player shuffle mode
|
||||
@property DecibelRepeatMode repeatMode; // Player repeat mode
|
||||
@property (copy, readonly) SBObject *currentPlaylist; // The current playlist
|
||||
|
||||
@end
|
||||
|
||||
// A track in the playlist
|
||||
@interface DecibelTrack : SBObject <DecibelGenericMethods>
|
||||
|
||||
- (NSString *) id; // The track's ID
|
||||
@property (copy, readonly) NSURL *file; // The track's location
|
||||
@property (readonly) double duration; // The track's duration in seconds
|
||||
@property (readonly) double sampleRate; // The track's sample rate in Hz
|
||||
@property (readonly) NSInteger bitDepth; // The bit depth
|
||||
@property (readonly) NSInteger channels; // The track's channels
|
||||
@property (copy) NSString *title; // The track's title
|
||||
@property (copy) NSString *artist; // The track's artist
|
||||
@property (copy) NSString *albumTitle; // The track's album title
|
||||
@property (copy) NSString *albumArtist; // The track's album artist
|
||||
@property NSInteger trackNumber; // The track's track number
|
||||
@property NSInteger trackTotal; // The total number of tracks on the album
|
||||
@property NSInteger discNumber; // The disc number containing the track
|
||||
@property NSInteger discTotal; // The total number of discs (for multidisc albums)
|
||||
@property BOOL partOfACompilation; // Is the track part of a compilation?
|
||||
@property (copy) NSString *genre; // The track's genre
|
||||
@property (copy) NSString *composer; // The track's composer
|
||||
@property (copy) NSString *releaseDate; // The track's release date
|
||||
@property (copy) NSString *ISRC; // The track's ISRC
|
||||
@property (copy) id MCN; // The track's MCN
|
||||
|
||||
- (void) playTrack; // Play a track in the playlist
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Hermes.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /Applications/Hermes.app | sdp -fh --basename Hermes
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class HermesApplication, HermesSong, HermesStation;
|
||||
|
||||
// Legal player states
|
||||
enum HermesPlayerStates {
|
||||
HermesPlayerStatesStopped = 'stop' /* Player is stopped */,
|
||||
HermesPlayerStatesPlaying = 'play' /* Player is playing */,
|
||||
HermesPlayerStatesPaused = 'paus' /* Player is paused */
|
||||
};
|
||||
typedef enum HermesPlayerStates HermesPlayerStates;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Hermes Suite
|
||||
*/
|
||||
|
||||
// The Pandora player.
|
||||
@interface HermesApplication : SBApplication
|
||||
|
||||
- (SBElementArray<HermesStation *> *) stations;
|
||||
|
||||
@property NSInteger playbackVolume; // The current playback volume (0–100).
|
||||
@property HermesPlayerStates playbackState; // The current playback state.
|
||||
@property (readonly) double playbackPosition; // The current song’s playback position, in seconds.
|
||||
@property (readonly) double currentSongDuration; // The duration (length) of the current song, in seconds.
|
||||
@property (copy) HermesStation *currentStation; // The currently selected Pandora station.
|
||||
@property (copy, readonly) HermesSong *currentSong; // The currently playing (or paused) Pandora song (WARNING: This is an invalid reference in current versions of Hermes; you must access the current song’s properties individually or as a group directly instead.)
|
||||
|
||||
- (void) playpause; // Play the current song if it is paused; pause the current song if it is playing.
|
||||
- (void) pause; // Pause the currently playing song.
|
||||
- (void) play; // Resume playing the current song.
|
||||
- (void) nextSong; // Skip to the next song on the current station.
|
||||
- (void) thumbsUp; // Tell Pandora you like the current song.
|
||||
- (void) thumbsDown; // Tell Pandora you don’t like the current song.
|
||||
- (void) tiredOfSong; // Tell Pandora you’re tired of the current song.
|
||||
- (void) increaseVolume; // Increase the playback volume.
|
||||
- (void) decreaseVolume; // Decrease the playback volume.
|
||||
- (void) maximizeVolume; // Set the playback volume to its maximum level.
|
||||
- (void) mute; // Mutes playback, saving the current volume level.
|
||||
- (void) unmute; // Restores the volume to the level prior to muting.
|
||||
|
||||
@end
|
||||
|
||||
// A Pandora song (track).
|
||||
@interface HermesSong : SBObject
|
||||
|
||||
@property (copy, readonly) NSString *title; // The song’s title.
|
||||
@property (copy, readonly) NSString *artist; // The song’s artist.
|
||||
@property (copy, readonly) NSString *album; // The song’s album.
|
||||
@property (copy, readonly) NSString *artworkURL; // An image URL for the album’s cover artwork.
|
||||
@property (readonly) NSInteger rating; // The song’s numeric rating.
|
||||
@property (copy, readonly) NSString *albumURL; // A Pandora URL for more information on the album.
|
||||
@property (copy, readonly) NSString *artistURL; // A Pandora URL for more information on the artist.
|
||||
@property (copy, readonly) NSString *trackURL; // A Pandora URL for more information on the track.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A Pandora station.
|
||||
@interface HermesStation : SBObject
|
||||
|
||||
@property (copy, readonly) NSString *name; // The station’s name.
|
||||
@property (copy, readonly) NSString *stationID; // The station’s ID.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Vox.h
|
||||
* VOX.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /Applications/Vox.app | sdp -fh --basename Vox
|
||||
* sdef /Applications/VOX.app | sdp -fh --basename VOX
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
@@ -14,45 +14,30 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppTests.m
|
||||
// BGMAppTests
|
||||
// BGMAboutPanel.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
// This class manages the "About Background Music" window.
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
|
||||
@interface BGMAppTests : XCTestCase
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BGMAboutPanel : NSObject
|
||||
|
||||
- (instancetype)initWithPanel:(NSPanel*)inAboutPanel licenseView:(NSTextView*)inLicenseView;
|
||||
- (void) show;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAppTests
|
||||
|
||||
// TODO: More than no tests
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testExample {
|
||||
// This is an example of a functional test case.
|
||||
XCTAssert(YES, @"Pass");
|
||||
}
|
||||
|
||||
- (void)testPerformanceExample {
|
||||
// This is an example of a performance test case.
|
||||
[self measureBlock:^{
|
||||
// Put the code you want to measure the time of here.
|
||||
}];
|
||||
}
|
||||
|
||||
@interface BGMLinkField : NSTextField
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAboutPanel.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAboutPanel.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static NSInteger const kVersionLabelTag = 1;
|
||||
static NSInteger const kCopyrightLabelTag = 2;
|
||||
static NSInteger const kProjectWebsiteLabelTag = 3;
|
||||
|
||||
@implementation BGMAboutPanel {
|
||||
NSPanel* aboutPanel;
|
||||
|
||||
NSTextField* versionLabel;
|
||||
NSTextField* copyrightLabel;
|
||||
NSTextField* websiteLabel;
|
||||
|
||||
NSTextView* licenseView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPanel:(NSPanel*)inAboutPanel licenseView:(NSTextView*)inLicenseView {
|
||||
if ((self = [super init])) {
|
||||
aboutPanel = inAboutPanel;
|
||||
|
||||
versionLabel = [[aboutPanel contentView] viewWithTag:kVersionLabelTag];
|
||||
copyrightLabel = [[aboutPanel contentView] viewWithTag:kCopyrightLabelTag];
|
||||
websiteLabel = [[aboutPanel contentView] viewWithTag:kProjectWebsiteLabelTag];
|
||||
|
||||
licenseView = inLicenseView;
|
||||
|
||||
[self initAboutPanel];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initAboutPanel {
|
||||
// Set up the About Background Music window
|
||||
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
|
||||
if (bundle == nil) {
|
||||
NSLog(@"Background Music: BGMAboutPanel::initAboutPanel: Could not find main bundle");
|
||||
} else {
|
||||
// Version number label
|
||||
NSString* __nullable version =
|
||||
[[bundle infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
|
||||
if (version) {
|
||||
versionLabel.stringValue = [NSString stringWithFormat:@"Version %@", version];
|
||||
}
|
||||
|
||||
// Copyright notice label
|
||||
NSString* __nullable copyrightNotice =
|
||||
[[bundle infoDictionary] objectForKey:@"NSHumanReadableCopyright"];
|
||||
|
||||
if (copyrightNotice) {
|
||||
copyrightLabel.stringValue = (NSString*)copyrightNotice;
|
||||
}
|
||||
|
||||
// Project website link label
|
||||
websiteLabel.selectable = YES;
|
||||
websiteLabel.allowsEditingTextAttributes = YES;
|
||||
|
||||
NSString* projectURL = [NSString stringWithUTF8String:kBGMProjectURL];
|
||||
websiteLabel.attributedStringValue =
|
||||
[[NSAttributedString alloc] initWithString:projectURL
|
||||
attributes:@{ NSLinkAttributeName: projectURL,
|
||||
NSFontAttributeName: websiteLabel.font }];
|
||||
|
||||
// Load the text of the license into the text view
|
||||
NSString* __nullable licensePath = [bundle pathForResource:@"LICENSE" ofType:nil];
|
||||
|
||||
NSError* err;
|
||||
NSString* __nullable licenseStr = (!licensePath ? nil :
|
||||
[NSString stringWithContentsOfFile:(NSString*)licensePath
|
||||
encoding:NSASCIIStringEncoding
|
||||
error:&err]);
|
||||
|
||||
if (err || !licenseStr || [licenseStr isEqualToString:@""]) {
|
||||
NSLog(@"Error loading license file: %@", err);
|
||||
licenseStr = @"Error: could not open license file.";
|
||||
}
|
||||
|
||||
licenseView.string = licenseStr;
|
||||
|
||||
NSFont* __nullable font = [NSFont fontWithName:@"Andale Mono" size:0.0];
|
||||
if (font) {
|
||||
licenseView.textStorage.font = font;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) show {
|
||||
DebugMsg("BGMAboutPanel::showAboutPanel: Opening \"About Background Music\" panel");
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
[aboutPanel setIsVisible:YES];
|
||||
[aboutPanel makeKeyAndOrderFront:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMLinkField
|
||||
|
||||
- (void) resetCursorRects {
|
||||
// Change the mouse cursor when hovering over the link. (It does change by default, but only after
|
||||
// you've clicked it once.)
|
||||
[self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -20,21 +20,23 @@
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "BGMAudioDeviceManager.h"
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// System Includes
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAutoPauseMusicPrefs : NSObject
|
||||
|
||||
// Note that toggleAutoPauseMusicMenuItem is the item in the main menu that enables/disables auto-pausing, rather than the
|
||||
// disabled "Auto-pause" menu item in the preferences menu that acts as a section heading. This class updates the text of
|
||||
// toggleAutoPauseMusicMenuItem when the user changes the music player.
|
||||
- (id) initWithPreferencesMenu:(NSMenu*)inPrefsMenu
|
||||
toggleAutoPauseMusicMenuItem:(NSMenuItem*)inToggleAutoPauseMusicMenuItem
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices;
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -24,182 +24,94 @@
|
||||
#import "BGMAutoPauseMusicPrefs.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#import "BGM_Types.h"
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
static NSString* const kToggleAutoPauseMusicMenuItemTitleFormat = @"Auto-pause %@";
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static float const kMenuItemIconScalingFactor = 1.15f;
|
||||
static NSInteger const kPrefsMenuAutoPauseHeaderTag = 1;
|
||||
|
||||
@implementation BGMAutoPauseMusicPrefs {
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
NSMenuItem* toggleAutoPauseMusicMenuItem;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
NSMenu* prefsMenu;
|
||||
NSArray<NSMenuItem*>* musicPlayerMenuItems;
|
||||
}
|
||||
|
||||
- (id) initWithPreferencesMenu:(NSMenu*)inPrefsMenu
|
||||
toggleAutoPauseMusicMenuItem:(NSMenuItem*)inToggleAutoPauseMusicMenuItem
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices {
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers {
|
||||
if ((self = [super init])) {
|
||||
prefsMenu = inPrefsMenu;
|
||||
toggleAutoPauseMusicMenuItem = inToggleAutoPauseMusicMenuItem;
|
||||
audioDevices = inAudioDevices;
|
||||
musicPlayers = inMusicPlayers;
|
||||
|
||||
musicPlayerMenuItems = @[];
|
||||
|
||||
[self initSelectedMusicPlayer];
|
||||
[self initMenuSection];
|
||||
[self updateMenuItemTitle];
|
||||
[self initPreferencesMenuSection];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initSelectedMusicPlayer {
|
||||
// TODO: It would make more sense to either just save the music player setting in the User Defaults (the same way AppDelegate saves
|
||||
// whether auto-pause is enabled) or to send a "musicPlayerID" to the driver, which would only be used by BGMApp. If the latter,
|
||||
// we might as well save the auto-pause setting on the driver as well just so all the settings are saved in the same place.
|
||||
|
||||
// Get the currently selected music player from the driver and update the global in BGMMusicPlayerBase
|
||||
|
||||
// The bundle ID and PID set on the driver
|
||||
CFNumberRef selectedPID = static_cast<CFNumberRef>([audioDevices bgmDevice].GetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress));
|
||||
CFStringRef selectedBundleID = [audioDevices bgmDevice].GetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress);
|
||||
|
||||
DebugMsg("BGMAutoPauseMusicPrefs::initSelectedMusicPlayer: Music player on BGMDriver: bundleID=%s PID=%s",
|
||||
selectedBundleID == NULL ? "null" : CFStringGetCStringPtr(selectedBundleID, kCFStringEncodingUTF8),
|
||||
selectedPID == NULL ? "null" : [[(__bridge NSNumber*)selectedPID stringValue] UTF8String]);
|
||||
|
||||
// If no music player is set on the driver, set it to the one set in the app and return
|
||||
if ((selectedBundleID == NULL || CFEqual(selectedBundleID, CFSTR(""))) &&
|
||||
(selectedPID == NULL || [(__bridge NSNumber*)selectedPID intValue] < 1)) {
|
||||
[self updateBGMDevice];
|
||||
return;
|
||||
}
|
||||
|
||||
// The IDs set in the app, which will be updated if they don't match the values from the driver
|
||||
CFNumberRef selectedPIDInBGMApp = [[BGMMusicPlayerBase selectedMusicPlayer] pid];
|
||||
CFStringRef selectedBundleIDInBGMApp = [[[BGMMusicPlayerBase selectedMusicPlayer] class] bundleID];
|
||||
|
||||
// Return early if the music player selected in the app already matches the driver
|
||||
if ((selectedPID != NULL && selectedPIDInBGMApp != NULL && CFEqual(selectedPID, selectedPIDInBGMApp)) ||
|
||||
(selectedBundleID != NULL && selectedBundleIDInBGMApp != NULL && CFEqual(selectedBundleID, selectedBundleIDInBGMApp))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each selectable music player
|
||||
for (Class mpClass in [BGMMusicPlayerBase musicPlayerClasses]) {
|
||||
// Look for a running instance of the music player by PID
|
||||
if (selectedPID != NULL &&
|
||||
[mpClass respondsToSelector:@selector(pidsOfRunningInstances)] &&
|
||||
[mpClass respondsToSelector:@selector(initWithPIDFromNSNumber:)]) {
|
||||
NSArray<NSNumber*>* mpPIDs = [mpClass pidsOfRunningInstances];
|
||||
for (NSNumber* mpPID in mpPIDs) {
|
||||
if (CFEqual((__bridge CFNumberRef)mpPID, selectedPID)) {
|
||||
DebugMsg("BGMAutoPauseMusicPrefs::initSelectedMusicPlayer: Selected music player on driver was %s (found by pid)",
|
||||
[[mpClass name] UTF8String]);
|
||||
[BGMMusicPlayerBase setSelectedMusicPlayer:[[mpClass alloc] initWithPIDFromNSNumber:mpPID]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check by bundle ID
|
||||
CFStringRef mpBundleID = [mpClass bundleID];
|
||||
if (selectedBundleID != NULL &&
|
||||
mpBundleID != NULL &&
|
||||
CFEqual(mpBundleID, selectedBundleID)) {
|
||||
// Found the selected music player. Update the app to match the driver and return.
|
||||
DebugMsg("BGMAutoPauseMusicPrefs::initSelectedMusicPlayer: Selected music player on driver was %s",
|
||||
[[mpClass name] UTF8String]);
|
||||
|
||||
[BGMMusicPlayerBase setSelectedMusicPlayer:[mpClass new]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) initMenuSection {
|
||||
// Add the menu items related to auto-pausing music to the settings submenu
|
||||
- (void) initPreferencesMenuSection {
|
||||
// Add the menu items related to auto-pausing music to the Preferences submenu
|
||||
|
||||
// The index to start inserting music player menu items at
|
||||
NSInteger musicPlayerItemsIndex = [prefsMenu indexOfItemWithTag:kPrefsMenuAutoPauseHeaderTag] + 1;
|
||||
|
||||
// Insert the options to change the music player app
|
||||
for (Class musicPlayerClass in [BGMMusicPlayerBase musicPlayerClasses]) {
|
||||
NSMenuItem* menuItem = [prefsMenu insertItemWithTitle:[musicPlayerClass name]
|
||||
// Insert the menu items used to change the music player app.
|
||||
for (id<BGMMusicPlayer> musicPlayer in musicPlayers.musicPlayers) {
|
||||
// Create an menu item for this music player.
|
||||
NSMenuItem* menuItem = [prefsMenu insertItemWithTitle:musicPlayer.name
|
||||
action:@selector(handleMusicPlayerChange:)
|
||||
keyEquivalent:@""
|
||||
atIndex:musicPlayerItemsIndex];
|
||||
atIndex:musicPlayerItemsIndex];
|
||||
|
||||
musicPlayerMenuItems = [musicPlayerMenuItems arrayByAddingObject:menuItem];
|
||||
|
||||
// Create an instance for this music player and associate it with the menu item
|
||||
[menuItem setRepresentedObject:[musicPlayerClass new]];
|
||||
// Associate the music player with the menu item
|
||||
menuItem.representedObject = musicPlayer;
|
||||
|
||||
// Show the default music player as selected
|
||||
if (musicPlayerClass == [[BGMMusicPlayerBase selectedMusicPlayer] class]) {
|
||||
[menuItem setState:NSOnState];
|
||||
// Show the menu item for the selected music player as selected
|
||||
if (musicPlayers.selectedMusicPlayer == musicPlayer) {
|
||||
menuItem.state = NSOnState;
|
||||
}
|
||||
|
||||
// Set the item's icon
|
||||
NSImage* icon = [musicPlayerClass icon];
|
||||
// Set the menu item's icon
|
||||
NSImage* __nullable icon = musicPlayer.icon;
|
||||
if (icon == nil) {
|
||||
// Set a blank icon so the text lines up
|
||||
icon = [NSImage new];
|
||||
}
|
||||
// Size the icon relative to the size of the item's text
|
||||
CGFloat length = [[NSFont menuBarFontOfSize:0] pointSize] * kMenuItemIconScalingFactor;
|
||||
[icon setSize:NSMakeSize(length, length)];
|
||||
[menuItem setImage:icon];
|
||||
|
||||
[menuItem setTarget:self];
|
||||
[menuItem setIndentationLevel:1];
|
||||
// Size the icon relative to the size of the item's text
|
||||
CGFloat length = [NSFont menuBarFontOfSize:0].pointSize * kMenuItemIconScalingFactor;
|
||||
icon.size = NSMakeSize(length, length);
|
||||
menuItem.image = icon;
|
||||
|
||||
menuItem.target = self;
|
||||
menuItem.indentationLevel = 1;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleMusicPlayerChange:(NSMenuItem*)sender {
|
||||
// Set the new music player as the selected music player
|
||||
BGMMusicPlayer* musicPlayer = [sender representedObject];
|
||||
assert(musicPlayer != nil);
|
||||
[BGMMusicPlayerBase setSelectedMusicPlayer:musicPlayer];
|
||||
id<BGMMusicPlayer> musicPlayer = sender.representedObject;
|
||||
NSAssert(musicPlayer, @"BGMAutoPauseMusicPrefs::handleMusicPlayerChange: !musicPlayer");
|
||||
|
||||
// Select/Deselect the menu items
|
||||
musicPlayers.selectedMusicPlayer = musicPlayer;
|
||||
|
||||
// Select/deselect the menu items
|
||||
for (NSMenuItem* item in musicPlayerMenuItems) {
|
||||
BOOL isNewlySelectedMusicPlayer = item == sender;
|
||||
[item setState:(isNewlySelectedMusicPlayer ? NSOnState : NSOffState)];
|
||||
BOOL isNewlySelectedMusicPlayer = (item == sender);
|
||||
item.state = (isNewlySelectedMusicPlayer ? NSOnState : NSOffState);
|
||||
}
|
||||
|
||||
[self updateMenuItemTitle];
|
||||
[self updateBGMDevice];
|
||||
}
|
||||
|
||||
- (void) updateBGMDevice {
|
||||
// Send the music player's PID or bundle ID to the driver
|
||||
|
||||
DebugMsg("BGMAutoPauseMusicPrefs::updateBGMDevice: Setting the music player to %s on the driver",
|
||||
[[[[BGMMusicPlayer selectedMusicPlayer] class] name] UTF8String]);
|
||||
|
||||
CFNumberRef __nullable pid = [[BGMMusicPlayer selectedMusicPlayer] pid];
|
||||
|
||||
if (pid != NULL) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress, pid);
|
||||
} else {
|
||||
CFStringRef __nullable bundleID = [[[BGMMusicPlayer selectedMusicPlayer] class] bundleID];
|
||||
|
||||
if (bundleID != NULL) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress, bundleID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) updateMenuItemTitle {
|
||||
// Set the title of the Auto-pause Music menu item, including the name of the selected music player
|
||||
NSString* musicPlayerName = [[[BGMMusicPlayer selectedMusicPlayer] class] name];
|
||||
NSString* title = [NSString stringWithFormat:kToggleAutoPauseMusicMenuItemTitleFormat, musicPlayerName];
|
||||
[toggleAutoPauseMusicMenuItem setTitle:title];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMOutputDevicePrefs : NSObject
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices;
|
||||
@@ -34,3 +36,5 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -23,12 +23,19 @@
|
||||
// Self Include
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGM_Types.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAAutoDisposer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
|
||||
@implementation BGMOutputDevicePrefs {
|
||||
@@ -50,58 +57,209 @@ 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]);
|
||||
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 isSelected = [audioDevices isOutputDevice:device.GetObjectID()];
|
||||
[item setState:(isSelected ? NSOnState : NSOffState)];
|
||||
[item setTarget:self];
|
||||
[item setIndentationLevel:1];
|
||||
[item setRepresentedObject:[NSNumber numberWithUnsignedInt:device.GetObjectID()]];
|
||||
|
||||
[prefsMenu insertItem:item atIndex:menuItemsIdx];
|
||||
|
||||
[outputDeviceMenuItems addObject:item];
|
||||
}
|
||||
[self insertMenuItemsForDevice:devices[i] preferencesMenu:prefsMenu];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) outputDeviceWasChanged:(NSMenuItem*)menuItem {
|
||||
BOOL success = [audioDevices setOutputDeviceWithID:[[menuItem representedObject] unsignedIntValue] revertOnFailure:YES];
|
||||
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
|
||||
// Insert menu items after the item for the "Output Device" heading.
|
||||
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||
|
||||
BOOL canBeOutputDevice = YES;
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", ([&] {
|
||||
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
|
||||
}));
|
||||
|
||||
if (canBeOutputDevice) {
|
||||
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 (!success) {
|
||||
// Couldn't change the output device, so show a warning and change the menu selection back
|
||||
NSAlert* alert = [NSAlert new];
|
||||
NSString* deviceName = [menuItem title];
|
||||
[alert setMessageText:[NSString stringWithFormat:@"Failed to set %@ as the output device", deviceName]];
|
||||
[alert setInformativeText:@"This is probably a bug. Feel free to report it."];
|
||||
[alert runModal];
|
||||
// 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);
|
||||
|
||||
[menuItem setState:NSOffState];
|
||||
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());
|
||||
|
||||
// TODO: Reselect previous menu item
|
||||
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]);
|
||||
|
||||
// Make sure the menu item is actually for an output device.
|
||||
if (![outputDeviceMenuItems containsObject:menuItem]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Change to the new output device.
|
||||
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 gets repopulated every time it's opened.)
|
||||
|
||||
// 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];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@@ -34,8 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BGMPreferencesMenu : NSObject <NSMenuDelegate>
|
||||
|
||||
- (id) initWithbgmMenu:(NSMenu*)inBGMMenu
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
|
||||
|
||||
|
||||
@@ -26,105 +26,56 @@
|
||||
// Local Includes
|
||||
#import "BGMAutoPauseMusicPrefs.h"
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMAboutPanel.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Interface Builder tags
|
||||
static NSInteger const kToggleAutoPauseMusicMenuItemTag = 2;
|
||||
|
||||
static NSInteger const kPreferencesMenuItemTag = 1;
|
||||
static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||
|
||||
static NSInteger const kAboutPanelVersionLabelTag = 1;
|
||||
static NSInteger const kAboutPanelCopyrightLabelTag = 2;
|
||||
|
||||
@implementation BGMPreferencesMenu {
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
|
||||
// Menu sections
|
||||
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
|
||||
BGMOutputDevicePrefs* outputDevicePrefs;
|
||||
|
||||
// About Background Music window
|
||||
NSPanel* aboutPanel;
|
||||
NSTextField* aboutPanelVersionLabel;
|
||||
NSTextField* aboutPanelCopyrightLabel;
|
||||
NSTextView* aboutPanelLicenceView;
|
||||
// The About Background Music window
|
||||
BGMAboutPanel* aboutPanel;
|
||||
}
|
||||
|
||||
- (id) initWithbgmMenu:(NSMenu*)inBGMMenu
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
|
||||
if ((self = [super init])) {
|
||||
audioDevices = inAudioDevices;
|
||||
aboutPanel = inAboutPanel;
|
||||
|
||||
aboutPanelVersionLabel = [[aboutPanel contentView] viewWithTag:kAboutPanelVersionLabelTag];
|
||||
aboutPanelCopyrightLabel = [[aboutPanel contentView] viewWithTag:kAboutPanelCopyrightLabelTag];
|
||||
aboutPanelLicenceView = inAboutPanelLicenseView;
|
||||
|
||||
NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu];
|
||||
[prefsMenu setDelegate:self];
|
||||
|
||||
NSMenuItem* toggleAutoPauseMusicMenuItem = [inBGMMenu itemWithTag:kToggleAutoPauseMusicMenuItemTag];
|
||||
autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu
|
||||
toggleAutoPauseMusicMenuItem:toggleAutoPauseMusicMenuItem
|
||||
audioDevices:audioDevices];
|
||||
audioDevices:inAudioDevices
|
||||
musicPlayers:inMusicPlayers];
|
||||
|
||||
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:audioDevices];
|
||||
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices];
|
||||
|
||||
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
|
||||
|
||||
// Set up the "About Background Music" menu item
|
||||
NSMenuItem* aboutMenuItem = [prefsMenu itemWithTag:kAboutPanelMenuItemTag];
|
||||
[aboutMenuItem setTarget:self];
|
||||
[aboutMenuItem setAction:@selector(showAboutPanel)];
|
||||
|
||||
[self initAboutPanel];
|
||||
[aboutMenuItem setTarget:aboutPanel];
|
||||
[aboutMenuItem setAction:@selector(show)];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) initAboutPanel {
|
||||
// Set up the About Background Music window
|
||||
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
|
||||
if (bundle == nil) {
|
||||
LogWarning("Background Music: BGMPreferencesMenu::initAboutPanel: Could not find main bundle");
|
||||
} else {
|
||||
// Version number label
|
||||
NSString* version = [[bundle infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
[aboutPanelVersionLabel setStringValue:[NSString stringWithFormat:@"Version %@", version]];
|
||||
|
||||
// Copyright notice label
|
||||
NSString* copyrightNotice = [[bundle infoDictionary] objectForKey:@"NSHumanReadableCopyright"];
|
||||
[aboutPanelCopyrightLabel setStringValue:copyrightNotice];
|
||||
|
||||
// Load the text of the license into the text view
|
||||
NSString* licensePath = [bundle pathForResource:@"LICENSE" ofType:nil];
|
||||
NSError* err;
|
||||
NSString* licenseStr = [NSString stringWithContentsOfFile:licensePath encoding:NSASCIIStringEncoding error:&err];
|
||||
if (err != nil || [licenseStr isEqualToString:@""]) {
|
||||
NSLog(@"Error loading license file: %@", err);
|
||||
licenseStr = @"Error: could not open license file.";
|
||||
}
|
||||
[aboutPanelLicenceView setString:licenseStr];
|
||||
}
|
||||
}
|
||||
#pragma mark NSMenuDelegate
|
||||
|
||||
- (void) menuNeedsUpdate:(NSMenu*)menu {
|
||||
[outputDevicePrefs populatePreferencesMenu:menu];
|
||||
}
|
||||
|
||||
- (void) showAboutPanel {
|
||||
DebugMsg("BGMPreferencesMenu::showAboutPanel: Opening \"About Background Music\" panel");
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
[aboutPanel setIsVisible:YES];
|
||||
[aboutPanel makeKeyAndOrderFront:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMASOutputDevice.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// An AppleScript class for the output devices that can be selected in the preferences menu.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMASOutputDevice : NSObject
|
||||
|
||||
- (instancetype) initWithAudioObjectID:(AudioObjectID)objID
|
||||
audioDevices:(BGMAudioDeviceManager*)devices
|
||||
parentSpecifier:(NSScriptObjectSpecifier* __nullable)parentSpecifier;
|
||||
|
||||
@property (readonly) NSString* name;
|
||||
@property BOOL selected; // is this the device to be used for audio output?
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMASOutputDevice.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMASOutputDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMASOutputDevice {
|
||||
NSScriptObjectSpecifier* parentSpecifier;
|
||||
BGMAudioDevice device;
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
}
|
||||
|
||||
- (instancetype) initWithAudioObjectID:(AudioObjectID)objID
|
||||
audioDevices:(BGMAudioDeviceManager*)devices
|
||||
parentSpecifier:(NSScriptObjectSpecifier* __nullable)parent {
|
||||
if ((self = [super init])) {
|
||||
parentSpecifier = parent;
|
||||
device = objID;
|
||||
audioDevices = devices;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString*) name {
|
||||
return (NSString*)CFBridgingRelease(device.CopyName());
|
||||
}
|
||||
|
||||
- (BOOL) selected {
|
||||
return [audioDevices isOutputDevice:device];
|
||||
}
|
||||
|
||||
- (void) setSelected:(BOOL)selected {
|
||||
if (selected && ![self selected]) {
|
||||
DebugMsg("BGMASOutputDevice::setSelected: A script is setting output device to %s",
|
||||
[[self name] UTF8String]);
|
||||
|
||||
NSError* err = [audioDevices setOutputDeviceWithID:device revertOnFailure:YES];
|
||||
(void)err; // TODO: Return an error to the script somehow if this isn't nil. Also, should
|
||||
// we return an error if the script tries to set this property to false?
|
||||
}
|
||||
}
|
||||
|
||||
- (NSScriptObjectSpecifier* __nullable) objectSpecifier {
|
||||
NSScriptClassDescription* parentClassDescription = [parentSpecifier keyClassDescription];
|
||||
return [[NSNameSpecifier alloc] initWithContainerClassDescription:parentClassDescription
|
||||
containerSpecifier:parentSpecifier
|
||||
key:@"output devices"
|
||||
name:self.name];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
|
||||
<dictionary>
|
||||
<suite name="Background Music" code="BGMs" description="Background Music specific classes">
|
||||
<!-- TODO: Figure out why Script Editor returns output device objects as
|
||||
«class» "Built-in Output" of application "Background Music"
|
||||
instead of
|
||||
device "Built-in Output" of application "Background Music" -->
|
||||
<class name="output device"
|
||||
code="aDev"
|
||||
description="A hardware device that can play audio"
|
||||
plural="output devices"
|
||||
inherits="item">
|
||||
<synonym name="audio device"/>
|
||||
|
||||
<cocoa class="BGMASOutputDevice"/>
|
||||
|
||||
<property name="name"
|
||||
code="pnam"
|
||||
description="The name of the output device."
|
||||
type="text"
|
||||
access="r"/>
|
||||
|
||||
<property name="selected"
|
||||
code="Slcd"
|
||||
type="boolean"
|
||||
access="rw"
|
||||
description="Is this the device to be used for audio output?">
|
||||
<synonym name="default"/>
|
||||
</property>
|
||||
</class>
|
||||
|
||||
<class name="application"
|
||||
code="capp"
|
||||
description="The application program">
|
||||
<cocoa class="NSApplication"/>
|
||||
|
||||
<!-- We have a class called "output device", so we have to call this class something
|
||||
else. -->
|
||||
<property name="selected output device"
|
||||
type="output device"
|
||||
code="selO"
|
||||
access="rw"
|
||||
description="The device to be used for audio output">
|
||||
<synonym name="selected device"/>
|
||||
<synonym name="default device"/>
|
||||
<synonym name="default output device"/>
|
||||
|
||||
<cocoa key="selectedOutputDevice"/>
|
||||
</property>
|
||||
|
||||
<!-- Unintuitively, this is for the array of output devices. -->
|
||||
<element type="output device" access="r">
|
||||
<cocoa key="outputDevices"/>
|
||||
</element>
|
||||
</class>
|
||||
</suite>
|
||||
</dictionary>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppDelegate+AppleScript.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#import "BGMAppDelegate.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMASOutputDevice.h"
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppDelegate (AppleScript)
|
||||
|
||||
- (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key;
|
||||
|
||||
@property BGMASOutputDevice* selectedOutputDevice;
|
||||
@property (readonly) NSArray<BGMASOutputDevice*>* outputDevices;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppDelegate+AppleScript.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppDelegate+AppleScript.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMAppDelegate (AppleScript)
|
||||
|
||||
- (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key {
|
||||
#pragma unused (sender)
|
||||
DebugMsg("BGMAppDelegate:application:delegateHandlesKey: Key queried: '%s'", [key UTF8String]);
|
||||
|
||||
return [@[@"selectedOutputDevice", @"outputDevices"] containsObject:key];
|
||||
}
|
||||
|
||||
- (BGMASOutputDevice*) selectedOutputDevice {
|
||||
AudioObjectID outputDeviceID = [self.audioDevices outputDevice].GetObjectID();
|
||||
|
||||
return [[BGMASOutputDevice alloc] initWithAudioObjectID:outputDeviceID
|
||||
audioDevices:self.audioDevices
|
||||
parentSpecifier:[self objectSpecifier]];
|
||||
}
|
||||
|
||||
- (void) setSelectedOutputDevice:(BGMASOutputDevice*)device {
|
||||
[device setSelected:YES];
|
||||
}
|
||||
|
||||
- (NSArray<BGMASOutputDevice*>*) outputDevices {
|
||||
UInt32 numDevices = CAHALAudioSystemObject().GetNumberAudioDevices();
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
CAHALAudioSystemObject().GetAudioDevices(numDevices, devices);
|
||||
|
||||
NSMutableArray<BGMASOutputDevice*>* outputDevices =
|
||||
[NSMutableArray arrayWithCapacity:numDevices];
|
||||
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
BGMAudioDevice device(devices[i]);
|
||||
|
||||
if (device.CanBeOutputDeviceInBGMApp()) {
|
||||
BGMASOutputDevice* outputDevice =
|
||||
[[BGMASOutputDevice alloc] initWithAudioObjectID:device.GetObjectID()
|
||||
audioDevices:self.audioDevices
|
||||
parentSpecifier:[self objectSpecifier]];
|
||||
|
||||
[outputDevices addObject:outputDevice];
|
||||
}
|
||||
}
|
||||
|
||||
return outputDevices;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* SystemPreferences.h
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class SystemPreferencesItem, SystemPreferencesApplication, SystemPreferencesColor, SystemPreferencesDocument, SystemPreferencesWindow, SystemPreferencesAttributeRun, SystemPreferencesCharacter, SystemPreferencesParagraph, SystemPreferencesText, SystemPreferencesAttachment, SystemPreferencesWord, SystemPreferencesAnchor, SystemPreferencesPane, SystemPreferencesPrintSettings;
|
||||
|
||||
enum SystemPreferencesSavo {
|
||||
SystemPreferencesSavoAsk = 'ask ' /* Ask the user whether or not to save the file. */,
|
||||
SystemPreferencesSavoNo = 'no ' /* Do not save the file. */,
|
||||
SystemPreferencesSavoYes = 'yes ' /* Save the file. */
|
||||
};
|
||||
typedef enum SystemPreferencesSavo SystemPreferencesSavo;
|
||||
|
||||
enum SystemPreferencesEnum {
|
||||
SystemPreferencesEnumStandard = 'lwst' /* Standard PostScript error handling */,
|
||||
SystemPreferencesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */
|
||||
};
|
||||
typedef enum SystemPreferencesEnum SystemPreferencesEnum;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Standard Suite
|
||||
*/
|
||||
|
||||
// A scriptable object.
|
||||
@interface SystemPreferencesItem : SBObject
|
||||
|
||||
@property (copy) NSDictionary *properties; // All of the object's properties.
|
||||
|
||||
- (void)closeSaving:(SystemPreferencesSavo) saving savingIn:(NSURL *)savingIn; // Close an object.
|
||||
- (void)delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location.
|
||||
- (BOOL)exists; // Verify if an object exists.
|
||||
- (void)moveTo:(SBObject *)to; // Move object(s) to a new location.
|
||||
- (void)saveAs:(NSString *)as in:(NSURL *)in_; // Save an object.
|
||||
|
||||
@end
|
||||
|
||||
// An application's top level scripting object.
|
||||
@interface SystemPreferencesApplication : SBApplication
|
||||
|
||||
- (SBElementArray *)documents;
|
||||
- (SBElementArray *)windows;
|
||||
|
||||
@property (readonly) BOOL frontmost; // Is this the frontmost (active) application?
|
||||
@property (copy, readonly) NSString *name; // The name of the application.
|
||||
@property (copy, readonly) NSString *version; // The version of the application.
|
||||
|
||||
- (SystemPreferencesDocument *)open:(NSURL *)x; // Open an object.
|
||||
- (void)print:(NSURL *)x printDialog:(BOOL) printDialog withProperties:(SystemPreferencesPrintSettings *)withProperties; // Print an object.
|
||||
- (void)quitSaving:(SystemPreferencesSavo)saving; // Quit an application.
|
||||
|
||||
@end
|
||||
|
||||
// A color.
|
||||
@interface SystemPreferencesColor : SystemPreferencesItem
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A document.
|
||||
@interface SystemPreferencesDocument : SystemPreferencesItem
|
||||
|
||||
@property (readonly) BOOL modified; // Has the document been modified since the last save?
|
||||
@property (copy) NSString *name; // The document's name.
|
||||
@property (copy) NSString *path; // The document's path.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A window.
|
||||
@interface SystemPreferencesWindow : SystemPreferencesItem
|
||||
|
||||
@property NSRect bounds; // The bounding rectangle of the window.
|
||||
@property (readonly) BOOL closeable; // Whether the window has a close box.
|
||||
@property (copy, readonly) SystemPreferencesDocument *document; // The document whose contents are being displayed in the window.
|
||||
@property (readonly) BOOL floating; // Whether the window floats.
|
||||
- (NSInteger)id; // The unique identifier of the window.
|
||||
@property NSInteger index; // The index of the window, ordered front to back.
|
||||
@property (readonly) BOOL miniaturizable; // Whether the window can be miniaturized.
|
||||
@property BOOL miniaturized; // Whether the window is currently miniaturized.
|
||||
@property (readonly) BOOL modal; // Whether the window is the application's current modal window.
|
||||
@property (copy) NSString *name; // The full title of the window.
|
||||
@property (readonly) BOOL resizable; // Whether the window can be resized.
|
||||
@property (readonly) BOOL titled; // Whether the window has a title bar.
|
||||
@property BOOL visible; // Whether the window is currently visible.
|
||||
@property (readonly) BOOL zoomable; // Whether the window can be zoomed.
|
||||
@property BOOL zoomed; // Whether the window is currently zoomed.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Text Suite
|
||||
*/
|
||||
|
||||
// This subdivides the text into chunks that all have the same attributes.
|
||||
@interface SystemPreferencesAttributeRun : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)attachments;
|
||||
- (SBElementArray *)attributeRuns;
|
||||
- (SBElementArray *)characters;
|
||||
- (SBElementArray *)paragraphs;
|
||||
- (SBElementArray *)words;
|
||||
|
||||
@property (copy) NSColor *color; // The color of the first character.
|
||||
@property (copy) NSString *font; // The name of the font of the first character.
|
||||
@property NSInteger size; // The size in points of the first character.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// This subdivides the text into characters.
|
||||
@interface SystemPreferencesCharacter : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)attachments;
|
||||
- (SBElementArray *)attributeRuns;
|
||||
- (SBElementArray *)characters;
|
||||
- (SBElementArray *)paragraphs;
|
||||
- (SBElementArray *)words;
|
||||
|
||||
@property (copy) NSColor *color; // The color of the first character.
|
||||
@property (copy) NSString *font; // The name of the font of the first character.
|
||||
@property NSInteger size; // The size in points of the first character.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// This subdivides the text into paragraphs.
|
||||
@interface SystemPreferencesParagraph : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)attachments;
|
||||
- (SBElementArray *)attributeRuns;
|
||||
- (SBElementArray *)characters;
|
||||
- (SBElementArray *)paragraphs;
|
||||
- (SBElementArray *)words;
|
||||
|
||||
@property (copy) NSColor *color; // The color of the first character.
|
||||
@property (copy) NSString *font; // The name of the font of the first character.
|
||||
@property NSInteger size; // The size in points of the first character.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// Rich (styled) text
|
||||
@interface SystemPreferencesText : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)attachments;
|
||||
- (SBElementArray *)attributeRuns;
|
||||
- (SBElementArray *)characters;
|
||||
- (SBElementArray *)paragraphs;
|
||||
- (SBElementArray *)words;
|
||||
|
||||
@property (copy) NSColor *color; // The color of the first character.
|
||||
@property (copy) NSString *font; // The name of the font of the first character.
|
||||
@property NSInteger size; // The size in points of the first character.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// Represents an inline text attachment. This class is used mainly for make commands.
|
||||
@interface SystemPreferencesAttachment : SystemPreferencesText
|
||||
|
||||
@property (copy) NSString *fileName; // The path to the file for the attachment
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// This subdivides the text into words.
|
||||
@interface SystemPreferencesWord : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)attachments;
|
||||
- (SBElementArray *)attributeRuns;
|
||||
- (SBElementArray *)characters;
|
||||
- (SBElementArray *)paragraphs;
|
||||
- (SBElementArray *)words;
|
||||
|
||||
@property (copy) NSColor *color; // The color of the first character.
|
||||
@property (copy) NSString *font; // The name of the font of the first character.
|
||||
@property NSInteger size; // The size in points of the first character.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* System Preferences
|
||||
*/
|
||||
|
||||
// an anchor within a preference pane
|
||||
@interface SystemPreferencesAnchor : SystemPreferencesItem
|
||||
|
||||
@property (copy, readonly) NSString *name; // name of the anchor within a preference pane
|
||||
|
||||
- (SystemPreferencesAnchor *)reveal; // Reveals an anchor within a preference pane or preference pane itself
|
||||
|
||||
@end
|
||||
|
||||
// System Preferences top level scripting object
|
||||
@interface SystemPreferencesApplication (SystemPreferences)
|
||||
|
||||
- (SBElementArray *)panes;
|
||||
|
||||
@property (copy) SystemPreferencesPane *currentPane; // the currently selected pane
|
||||
@property (copy, readonly) SystemPreferencesWindow *preferencesWindow; // the main preferences window
|
||||
@property BOOL showAll; // Is SystemPrefs in show all view. (Setting to false will do nothing)
|
||||
|
||||
@end
|
||||
|
||||
// a preference pane
|
||||
@interface SystemPreferencesPane : SystemPreferencesItem
|
||||
|
||||
- (SBElementArray *)anchors;
|
||||
|
||||
- (NSString *)id; // locale independent name of the preference pane; can refer to a pane using the expression: pane id "<name>"
|
||||
@property (copy, readonly) NSString *localizedName; // localized name of the preference pane
|
||||
@property (copy, readonly) NSString *name; // name of the preference pane as it appears in the title bar; can refer to a pane using the expression: pane "<name>"
|
||||
|
||||
- (NSInteger)timedLoad; // This command does xxxx.
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Type Definitions
|
||||
*/
|
||||
|
||||
@interface SystemPreferencesPrintSettings : SBObject
|
||||
|
||||
@property NSInteger copies; // the number of copies of a document to be printed
|
||||
@property BOOL collating; // Should printed copies be collated?
|
||||
@property NSInteger startingPage; // the first page of the document to be printed
|
||||
@property NSInteger endingPage; // the last page of the document to be printed
|
||||
@property NSInteger pagesAcross; // number of logical pages laid across a physical page
|
||||
@property NSInteger pagesDown; // number of logical pages laid out down a physical page
|
||||
@property (copy) NSDate *requestedPrintTime; // the time at which the desktop printer should print the document
|
||||
@property SystemPreferencesEnum errorHandling; // how errors are handled
|
||||
@property (copy) NSString *faxNumber; // for fax number
|
||||
@property (copy) NSString *targetPrinter; // for target printer
|
||||
|
||||
- (void)closeSaving:(SystemPreferencesSavo) saving savingIn:(NSURL *)savingIn; // Close an object.
|
||||
- (void)delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location.
|
||||
- (BOOL)exists; // Verify if an object exists.
|
||||
- (void)moveTo:(SBObject *)to; // Move object(s) to a new location.
|
||||
- (void)saveAs:(NSString *)as in:(NSURL *)in_; // Save an object.
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,754 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2D46CA7B17D6ADCA00049D4A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2D46CA7817D6AD8A00049D4A /* Localizable.strings */; };
|
||||
2D46CA7D17D6AF4200049D4A /* DeviceIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 2D46CA7C17D6AF4200049D4A /* DeviceIcon.icns */; };
|
||||
2D47CAA415FEC82B002AAFB5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2D47CAA215FEC82B002AAFB5 /* Localizable.strings */; };
|
||||
2D4DE41415EDF8D500E96F0D /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2D4DE41215EDF8D500E96F0D /* CAVolumeCurve.cpp */; };
|
||||
2D4DE41515EDF8D500E96F0D /* CAVolumeCurve.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D4DE41315EDF8D500E96F0D /* CAVolumeCurve.h */; };
|
||||
2D616EF415B8C82500D598BD /* NullAudio.c in Sources */ = {isa = PBXBuildFile; fileRef = 2D616EF215B8C82500D598BD /* NullAudio.c */; };
|
||||
2D7477AD1578168D00412279 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D7477AC1578168D00412279 /* CoreFoundation.framework */; };
|
||||
2D76D96115E48B2000FF0F33 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D7477AC1578168D00412279 /* CoreFoundation.framework */; };
|
||||
2D76D97615E48B6400FF0F33 /* SA_PlugIn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2D76D97415E48B6400FF0F33 /* SA_PlugIn.cpp */; };
|
||||
2D76D97715E48B6400FF0F33 /* SA_PlugIn.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D76D97515E48B6400FF0F33 /* SA_PlugIn.h */; };
|
||||
2D76D97A15E498EB00FF0F33 /* SA_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2D76D97815E498EB00FF0F33 /* SA_Object.cpp */; };
|
||||
2D76D97B15E498EB00FF0F33 /* SA_Object.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D76D97915E498EB00FF0F33 /* SA_Object.h */; };
|
||||
2DD7AA0A15EACDE000C67AE1 /* SA_IOKit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA0815EACDDF00C67AE1 /* SA_IOKit.cpp */; };
|
||||
2DD7AA0B15EACDE000C67AE1 /* SA_IOKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA0915EACDDF00C67AE1 /* SA_IOKit.h */; };
|
||||
2DD7AA2315EAFD5100C67AE1 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA0E15EAFD3300C67AE1 /* CACFArray.cpp */; };
|
||||
2DD7AA2415EAFD5100C67AE1 /* CACFArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA0F15EAFD3300C67AE1 /* CACFArray.h */; };
|
||||
2DD7AA2515EAFD5100C67AE1 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1015EAFD3300C67AE1 /* CACFDictionary.cpp */; };
|
||||
2DD7AA2615EAFD5100C67AE1 /* CACFDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1115EAFD3300C67AE1 /* CACFDictionary.h */; };
|
||||
2DD7AA2715EAFD5100C67AE1 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1215EAFD3300C67AE1 /* CACFNumber.cpp */; };
|
||||
2DD7AA2815EAFD5100C67AE1 /* CACFNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1315EAFD3300C67AE1 /* CACFNumber.h */; };
|
||||
2DD7AA2915EAFD5100C67AE1 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1415EAFD3300C67AE1 /* CACFString.cpp */; };
|
||||
2DD7AA2A15EAFD5100C67AE1 /* CACFString.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1515EAFD3300C67AE1 /* CACFString.h */; };
|
||||
2DD7AA2B15EAFD5100C67AE1 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1615EAFD3300C67AE1 /* CADebugger.cpp */; };
|
||||
2DD7AA2C15EAFD5100C67AE1 /* CADebugger.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1715EAFD3300C67AE1 /* CADebugger.h */; };
|
||||
2DD7AA2D15EAFD5100C67AE1 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1815EAFD3300C67AE1 /* CADebugMacros.cpp */; };
|
||||
2DD7AA2E15EAFD5100C67AE1 /* CADebugMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1915EAFD3300C67AE1 /* CADebugMacros.h */; };
|
||||
2DD7AA2F15EAFD5100C67AE1 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1A15EAFD3300C67AE1 /* CADebugPrintf.cpp */; };
|
||||
2DD7AA3015EAFD5100C67AE1 /* CADebugPrintf.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1B15EAFD3300C67AE1 /* CADebugPrintf.h */; };
|
||||
2DD7AA3115EAFD5100C67AE1 /* CAException.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1C15EAFD3300C67AE1 /* CAException.h */; };
|
||||
2DD7AA3215EAFD5100C67AE1 /* CAGuard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1D15EAFD3300C67AE1 /* CAGuard.cpp */; };
|
||||
2DD7AA3315EAFD5100C67AE1 /* CAGuard.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA1E15EAFD3300C67AE1 /* CAGuard.h */; };
|
||||
2DD7AA3415EAFD5100C67AE1 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA1F15EAFD3300C67AE1 /* CAHostTimeBase.cpp */; };
|
||||
2DD7AA3515EAFD5100C67AE1 /* CAHostTimeBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA2015EAFD3300C67AE1 /* CAHostTimeBase.h */; };
|
||||
2DD7AA3615EAFD5100C67AE1 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA2115EAFD3300C67AE1 /* CAMutex.cpp */; };
|
||||
2DD7AA3715EAFD5100C67AE1 /* CAMutex.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA2215EAFD3300C67AE1 /* CAMutex.h */; };
|
||||
2DD7AA7D15EC20FD00C67AE1 /* CADispatchQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA7B15EC20FD00C67AE1 /* CADispatchQueue.cpp */; };
|
||||
2DD7AA7E15EC20FD00C67AE1 /* CADispatchQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA7C15EC20FD00C67AE1 /* CADispatchQueue.h */; };
|
||||
2DD7AA8015EC3DB800C67AE1 /* CACFObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA7F15EC3DB800C67AE1 /* CACFObject.h */; };
|
||||
2DD7AA9715EC551600C67AE1 /* SA_Device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7AA9515EC551500C67AE1 /* SA_Device.cpp */; };
|
||||
2DD7AA9815EC551600C67AE1 /* SA_Device.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD7AA9615EC551600C67AE1 /* SA_Device.h */; };
|
||||
2DD7AA9A15EC572000C67AE1 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD7AA9915EC572000C67AE1 /* IOKit.framework */; };
|
||||
2DED184915C359AC0091BE97 /* SimpleAudioDriver.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DED182515C356BA0091BE97 /* SimpleAudioDriver.cpp */; };
|
||||
2DED184A15C359AC0091BE97 /* SimpleAudioDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DED182615C356BA0091BE97 /* SimpleAudioDriver.h */; };
|
||||
2DED184B15C359AC0091BE97 /* SimpleAudioDriverUserClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2DED182915C356BA0091BE97 /* SimpleAudioDriverUserClient.cpp */; };
|
||||
2DED184C15C359AC0091BE97 /* SimpleAudioDriverUserClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DED182A15C356BA0091BE97 /* SimpleAudioDriverUserClient.h */; };
|
||||
2DED184D15C359AC0091BE97 /* SimpleAudioDriverTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DED182B15C356BA0091BE97 /* SimpleAudioDriverTypes.h */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2D46CA7917D6AD8A00049D4A /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
2D46CA7C17D6AF4200049D4A /* DeviceIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = DeviceIcon.icns; sourceTree = "<group>"; };
|
||||
2D47CAA315FEC82B002AAFB5 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
2D4DE41215EDF8D500E96F0D /* CAVolumeCurve.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CAVolumeCurve.cpp; sourceTree = "<group>"; };
|
||||
2D4DE41315EDF8D500E96F0D /* CAVolumeCurve.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAVolumeCurve.h; sourceTree = "<group>"; };
|
||||
2D616EF115B8C82500D598BD /* NullAudio-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "NullAudio-Info.plist"; sourceTree = "<group>"; };
|
||||
2D616EF215B8C82500D598BD /* NullAudio.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = NullAudio.c; sourceTree = "<group>"; };
|
||||
2D7477A91578168D00412279 /* NullAudio.driver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NullAudio.driver; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2D7477AC1578168D00412279 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||
2D7477EC157823CF00412279 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; };
|
||||
2D76D96015E48B2000FF0F33 /* SimpleAudioPlugIn.driver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleAudioPlugIn.driver; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2D76D97415E48B6400FF0F33 /* SA_PlugIn.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SA_PlugIn.cpp; sourceTree = "<group>"; };
|
||||
2D76D97515E48B6400FF0F33 /* SA_PlugIn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SA_PlugIn.h; sourceTree = "<group>"; };
|
||||
2D76D97815E498EB00FF0F33 /* SA_Object.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SA_Object.cpp; sourceTree = "<group>"; };
|
||||
2D76D97915E498EB00FF0F33 /* SA_Object.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SA_Object.h; sourceTree = "<group>"; };
|
||||
2D76D98B15E56E4E00FF0F33 /* SA_PlugIn-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SA_PlugIn-Info.plist"; sourceTree = "<group>"; };
|
||||
2DA8FA1515FEAAB000F04B50 /* ReadMe.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReadMe.txt; sourceTree = "<group>"; };
|
||||
2DD7AA0815EACDDF00C67AE1 /* SA_IOKit.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SA_IOKit.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA0915EACDDF00C67AE1 /* SA_IOKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SA_IOKit.h; sourceTree = "<group>"; };
|
||||
2DD7AA0E15EAFD3300C67AE1 /* CACFArray.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CACFArray.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA0F15EAFD3300C67AE1 /* CACFArray.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CACFArray.h; sourceTree = "<group>"; };
|
||||
2DD7AA1015EAFD3300C67AE1 /* CACFDictionary.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CACFDictionary.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1115EAFD3300C67AE1 /* CACFDictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CACFDictionary.h; sourceTree = "<group>"; };
|
||||
2DD7AA1215EAFD3300C67AE1 /* CACFNumber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CACFNumber.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1315EAFD3300C67AE1 /* CACFNumber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CACFNumber.h; sourceTree = "<group>"; };
|
||||
2DD7AA1415EAFD3300C67AE1 /* CACFString.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CACFString.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1515EAFD3300C67AE1 /* CACFString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CACFString.h; sourceTree = "<group>"; };
|
||||
2DD7AA1615EAFD3300C67AE1 /* CADebugger.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CADebugger.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1715EAFD3300C67AE1 /* CADebugger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CADebugger.h; sourceTree = "<group>"; };
|
||||
2DD7AA1815EAFD3300C67AE1 /* CADebugMacros.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CADebugMacros.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1915EAFD3300C67AE1 /* CADebugMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CADebugMacros.h; sourceTree = "<group>"; };
|
||||
2DD7AA1A15EAFD3300C67AE1 /* CADebugPrintf.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CADebugPrintf.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1B15EAFD3300C67AE1 /* CADebugPrintf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CADebugPrintf.h; sourceTree = "<group>"; };
|
||||
2DD7AA1C15EAFD3300C67AE1 /* CAException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CAException.h; sourceTree = "<group>"; };
|
||||
2DD7AA1D15EAFD3300C67AE1 /* CAGuard.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CAGuard.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA1E15EAFD3300C67AE1 /* CAGuard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CAGuard.h; sourceTree = "<group>"; };
|
||||
2DD7AA1F15EAFD3300C67AE1 /* CAHostTimeBase.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CAHostTimeBase.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA2015EAFD3300C67AE1 /* CAHostTimeBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CAHostTimeBase.h; sourceTree = "<group>"; };
|
||||
2DD7AA2115EAFD3300C67AE1 /* CAMutex.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CAMutex.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA2215EAFD3300C67AE1 /* CAMutex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CAMutex.h; sourceTree = "<group>"; };
|
||||
2DD7AA7B15EC20FD00C67AE1 /* CADispatchQueue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CADispatchQueue.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA7C15EC20FD00C67AE1 /* CADispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CADispatchQueue.h; sourceTree = "<group>"; };
|
||||
2DD7AA7F15EC3DB800C67AE1 /* CACFObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CACFObject.h; sourceTree = "<group>"; };
|
||||
2DD7AA9515EC551500C67AE1 /* SA_Device.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SA_Device.cpp; sourceTree = "<group>"; };
|
||||
2DD7AA9615EC551600C67AE1 /* SA_Device.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SA_Device.h; sourceTree = "<group>"; };
|
||||
2DD7AA9915EC572000C67AE1 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
|
||||
2DED182415C356BA0091BE97 /* SimpleAudioDriver-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SimpleAudioDriver-Info.plist"; sourceTree = "<group>"; };
|
||||
2DED182515C356BA0091BE97 /* SimpleAudioDriver.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SimpleAudioDriver.cpp; sourceTree = "<group>"; };
|
||||
2DED182615C356BA0091BE97 /* SimpleAudioDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleAudioDriver.h; sourceTree = "<group>"; };
|
||||
2DED182915C356BA0091BE97 /* SimpleAudioDriverUserClient.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SimpleAudioDriverUserClient.cpp; sourceTree = "<group>"; };
|
||||
2DED182A15C356BA0091BE97 /* SimpleAudioDriverUserClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleAudioDriverUserClient.h; sourceTree = "<group>"; };
|
||||
2DED182B15C356BA0091BE97 /* SimpleAudioDriverTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleAudioDriverTypes.h; sourceTree = "<group>"; };
|
||||
2DED183815C357180091BE97 /* SimpleAudioDriver.kext */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleAudioDriver.kext; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2DED183A15C357180091BE97 /* Kernel.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kernel.framework; path = System/Library/Frameworks/Kernel.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
2D7477A61578168D00412279 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D7477AD1578168D00412279 /* CoreFoundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2D76D95D15E48B2000FF0F33 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D76D96115E48B2000FF0F33 /* CoreFoundation.framework in Frameworks */,
|
||||
2DD7AA9A15EC572000C67AE1 /* IOKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2DED183315C357180091BE97 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
2D616EF015B8C82500D598BD /* NullAudio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D46CA7C17D6AF4200049D4A /* DeviceIcon.icns */,
|
||||
2D46CA7817D6AD8A00049D4A /* Localizable.strings */,
|
||||
2D616EF115B8C82500D598BD /* NullAudio-Info.plist */,
|
||||
2D616EF215B8C82500D598BD /* NullAudio.c */,
|
||||
);
|
||||
path = NullAudio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D74779B1578162B00412279 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DA8FA1515FEAAB000F04B50 /* ReadMe.txt */,
|
||||
2D616EF015B8C82500D598BD /* NullAudio */,
|
||||
2DED182215C356BA0091BE97 /* SimpleAudio */,
|
||||
2DD7AA0D15EAFD3300C67AE1 /* PublicUtility */,
|
||||
2D7477AB1578168D00412279 /* Frameworks */,
|
||||
2D7477AA1578168D00412279 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D7477AA1578168D00412279 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7477A91578168D00412279 /* NullAudio.driver */,
|
||||
2DED183815C357180091BE97 /* SimpleAudioDriver.kext */,
|
||||
2D76D96015E48B2000FF0F33 /* SimpleAudioPlugIn.driver */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D7477AB1578168D00412279 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7477EC157823CF00412279 /* CoreAudio.framework */,
|
||||
2D7477AC1578168D00412279 /* CoreFoundation.framework */,
|
||||
2DD7AA9915EC572000C67AE1 /* IOKit.framework */,
|
||||
2DED183A15C357180091BE97 /* Kernel.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DA8FA7E15FEC6B500F04B50 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D47CAA215FEC82B002AAFB5 /* Localizable.strings */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DD7AA0D15EAFD3300C67AE1 /* PublicUtility */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DD7AA0E15EAFD3300C67AE1 /* CACFArray.cpp */,
|
||||
2DD7AA0F15EAFD3300C67AE1 /* CACFArray.h */,
|
||||
2DD7AA1015EAFD3300C67AE1 /* CACFDictionary.cpp */,
|
||||
2DD7AA1115EAFD3300C67AE1 /* CACFDictionary.h */,
|
||||
2DD7AA1215EAFD3300C67AE1 /* CACFNumber.cpp */,
|
||||
2DD7AA1315EAFD3300C67AE1 /* CACFNumber.h */,
|
||||
2DD7AA7F15EC3DB800C67AE1 /* CACFObject.h */,
|
||||
2DD7AA1415EAFD3300C67AE1 /* CACFString.cpp */,
|
||||
2DD7AA1515EAFD3300C67AE1 /* CACFString.h */,
|
||||
2DD7AA1615EAFD3300C67AE1 /* CADebugger.cpp */,
|
||||
2DD7AA1715EAFD3300C67AE1 /* CADebugger.h */,
|
||||
2DD7AA1815EAFD3300C67AE1 /* CADebugMacros.cpp */,
|
||||
2DD7AA1915EAFD3300C67AE1 /* CADebugMacros.h */,
|
||||
2DD7AA1A15EAFD3300C67AE1 /* CADebugPrintf.cpp */,
|
||||
2DD7AA1B15EAFD3300C67AE1 /* CADebugPrintf.h */,
|
||||
2DD7AA7B15EC20FD00C67AE1 /* CADispatchQueue.cpp */,
|
||||
2DD7AA7C15EC20FD00C67AE1 /* CADispatchQueue.h */,
|
||||
2DD7AA1C15EAFD3300C67AE1 /* CAException.h */,
|
||||
2DD7AA1D15EAFD3300C67AE1 /* CAGuard.cpp */,
|
||||
2DD7AA1E15EAFD3300C67AE1 /* CAGuard.h */,
|
||||
2DD7AA1F15EAFD3300C67AE1 /* CAHostTimeBase.cpp */,
|
||||
2DD7AA2015EAFD3300C67AE1 /* CAHostTimeBase.h */,
|
||||
2DD7AA2115EAFD3300C67AE1 /* CAMutex.cpp */,
|
||||
2DD7AA2215EAFD3300C67AE1 /* CAMutex.h */,
|
||||
2D4DE41215EDF8D500E96F0D /* CAVolumeCurve.cpp */,
|
||||
2D4DE41315EDF8D500E96F0D /* CAVolumeCurve.h */,
|
||||
);
|
||||
path = PublicUtility;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DED182215C356BA0091BE97 /* SimpleAudio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DED182315C356BA0091BE97 /* Driver */,
|
||||
2DED182C15C356BA0091BE97 /* Plug-In */,
|
||||
);
|
||||
path = SimpleAudio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DED182315C356BA0091BE97 /* Driver */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DED182415C356BA0091BE97 /* SimpleAudioDriver-Info.plist */,
|
||||
2DED182515C356BA0091BE97 /* SimpleAudioDriver.cpp */,
|
||||
2DED182615C356BA0091BE97 /* SimpleAudioDriver.h */,
|
||||
2DED182915C356BA0091BE97 /* SimpleAudioDriverUserClient.cpp */,
|
||||
2DED182A15C356BA0091BE97 /* SimpleAudioDriverUserClient.h */,
|
||||
2DED182B15C356BA0091BE97 /* SimpleAudioDriverTypes.h */,
|
||||
);
|
||||
path = Driver;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DED182C15C356BA0091BE97 /* Plug-In */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DD7AA9515EC551500C67AE1 /* SA_Device.cpp */,
|
||||
2DD7AA9615EC551600C67AE1 /* SA_Device.h */,
|
||||
2DD7AA0815EACDDF00C67AE1 /* SA_IOKit.cpp */,
|
||||
2DD7AA0915EACDDF00C67AE1 /* SA_IOKit.h */,
|
||||
2D76D97815E498EB00FF0F33 /* SA_Object.cpp */,
|
||||
2D76D97915E498EB00FF0F33 /* SA_Object.h */,
|
||||
2D76D98B15E56E4E00FF0F33 /* SA_PlugIn-Info.plist */,
|
||||
2D76D97415E48B6400FF0F33 /* SA_PlugIn.cpp */,
|
||||
2D76D97515E48B6400FF0F33 /* SA_PlugIn.h */,
|
||||
2DA8FA7E15FEC6B500F04B50 /* Resources */,
|
||||
);
|
||||
path = "Plug-In";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
2D76D95E15E48B2000FF0F33 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D76D97715E48B6400FF0F33 /* SA_PlugIn.h in Headers */,
|
||||
2D76D97B15E498EB00FF0F33 /* SA_Object.h in Headers */,
|
||||
2DD7AA0B15EACDE000C67AE1 /* SA_IOKit.h in Headers */,
|
||||
2DD7AA2415EAFD5100C67AE1 /* CACFArray.h in Headers */,
|
||||
2DD7AA2615EAFD5100C67AE1 /* CACFDictionary.h in Headers */,
|
||||
2DD7AA2815EAFD5100C67AE1 /* CACFNumber.h in Headers */,
|
||||
2DD7AA2A15EAFD5100C67AE1 /* CACFString.h in Headers */,
|
||||
2DD7AA2C15EAFD5100C67AE1 /* CADebugger.h in Headers */,
|
||||
2DD7AA2E15EAFD5100C67AE1 /* CADebugMacros.h in Headers */,
|
||||
2DD7AA3015EAFD5100C67AE1 /* CADebugPrintf.h in Headers */,
|
||||
2DD7AA3115EAFD5100C67AE1 /* CAException.h in Headers */,
|
||||
2DD7AA3315EAFD5100C67AE1 /* CAGuard.h in Headers */,
|
||||
2DD7AA3515EAFD5100C67AE1 /* CAHostTimeBase.h in Headers */,
|
||||
2DD7AA3715EAFD5100C67AE1 /* CAMutex.h in Headers */,
|
||||
2DD7AA7E15EC20FD00C67AE1 /* CADispatchQueue.h in Headers */,
|
||||
2DD7AA8015EC3DB800C67AE1 /* CACFObject.h in Headers */,
|
||||
2DD7AA9815EC551600C67AE1 /* SA_Device.h in Headers */,
|
||||
2D4DE41515EDF8D500E96F0D /* CAVolumeCurve.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2DED183415C357180091BE97 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2DED184A15C359AC0091BE97 /* SimpleAudioDriver.h in Headers */,
|
||||
2DED184C15C359AC0091BE97 /* SimpleAudioDriverUserClient.h in Headers */,
|
||||
2DED184D15C359AC0091BE97 /* SimpleAudioDriverTypes.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
2D7477A81578168D00412279 /* NullAudio */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 2D7477B51578168D00412279 /* Build configuration list for PBXNativeTarget "NullAudio" */;
|
||||
buildPhases = (
|
||||
2D7477A51578168D00412279 /* Sources */,
|
||||
2D7477A61578168D00412279 /* Frameworks */,
|
||||
2D46CA7A17D6ADC500049D4A /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = NullAudio;
|
||||
productName = AudioNULLDriver;
|
||||
productReference = 2D7477A91578168D00412279 /* NullAudio.driver */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
2D76D95F15E48B2000FF0F33 /* SimpleAudioPlugIn */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 2D76D97215E48B2000FF0F33 /* Build configuration list for PBXNativeTarget "SimpleAudioPlugIn" */;
|
||||
buildPhases = (
|
||||
2D76D95E15E48B2000FF0F33 /* Headers */,
|
||||
2D76D95C15E48B2000FF0F33 /* Sources */,
|
||||
2D76D95D15E48B2000FF0F33 /* Frameworks */,
|
||||
2DA8FA4B15FEABE000F04B50 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SimpleAudioPlugIn;
|
||||
productName = SimpleAudioPlugIn;
|
||||
productReference = 2D76D96015E48B2000FF0F33 /* SimpleAudioPlugIn.driver */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
2DED183715C357180091BE97 /* SimpleAudioDriver */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 2DED184515C357180091BE97 /* Build configuration list for PBXNativeTarget "SimpleAudioDriver" */;
|
||||
buildPhases = (
|
||||
2DED183415C357180091BE97 /* Headers */,
|
||||
2DED183215C357180091BE97 /* Sources */,
|
||||
2DED183315C357180091BE97 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SimpleAudioDriver;
|
||||
productName = SimpleAudioDriver;
|
||||
productReference = 2DED183815C357180091BE97 /* SimpleAudioDriver.kext */;
|
||||
productType = "com.apple.product-type.kernel-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
2D74779D1578162B00412279 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0500;
|
||||
};
|
||||
buildConfigurationList = 2D7477A01578162B00412279 /* Build configuration list for PBXProject "AudioDriverExamples" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
English,
|
||||
);
|
||||
mainGroup = 2D74779B1578162B00412279;
|
||||
productRefGroup = 2D7477AA1578168D00412279 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
2D7477A81578168D00412279 /* NullAudio */,
|
||||
2DED183715C357180091BE97 /* SimpleAudioDriver */,
|
||||
2D76D95F15E48B2000FF0F33 /* SimpleAudioPlugIn */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
2D46CA7A17D6ADC500049D4A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D46CA7B17D6ADCA00049D4A /* Localizable.strings in Resources */,
|
||||
2D46CA7D17D6AF4200049D4A /* DeviceIcon.icns in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2DA8FA4B15FEABE000F04B50 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D47CAA415FEC82B002AAFB5 /* Localizable.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
2D7477A51578168D00412279 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D616EF415B8C82500D598BD /* NullAudio.c in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2D76D95C15E48B2000FF0F33 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2D76D97615E48B6400FF0F33 /* SA_PlugIn.cpp in Sources */,
|
||||
2D76D97A15E498EB00FF0F33 /* SA_Object.cpp in Sources */,
|
||||
2DD7AA0A15EACDE000C67AE1 /* SA_IOKit.cpp in Sources */,
|
||||
2DD7AA2315EAFD5100C67AE1 /* CACFArray.cpp in Sources */,
|
||||
2DD7AA2515EAFD5100C67AE1 /* CACFDictionary.cpp in Sources */,
|
||||
2DD7AA2715EAFD5100C67AE1 /* CACFNumber.cpp in Sources */,
|
||||
2DD7AA2915EAFD5100C67AE1 /* CACFString.cpp in Sources */,
|
||||
2DD7AA2B15EAFD5100C67AE1 /* CADebugger.cpp in Sources */,
|
||||
2DD7AA2D15EAFD5100C67AE1 /* CADebugMacros.cpp in Sources */,
|
||||
2DD7AA2F15EAFD5100C67AE1 /* CADebugPrintf.cpp in Sources */,
|
||||
2DD7AA3215EAFD5100C67AE1 /* CAGuard.cpp in Sources */,
|
||||
2DD7AA3415EAFD5100C67AE1 /* CAHostTimeBase.cpp in Sources */,
|
||||
2DD7AA3615EAFD5100C67AE1 /* CAMutex.cpp in Sources */,
|
||||
2DD7AA7D15EC20FD00C67AE1 /* CADispatchQueue.cpp in Sources */,
|
||||
2DD7AA9715EC551600C67AE1 /* SA_Device.cpp in Sources */,
|
||||
2D4DE41415EDF8D500E96F0D /* CAVolumeCurve.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2DED183215C357180091BE97 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2DED184915C359AC0091BE97 /* SimpleAudioDriver.cpp in Sources */,
|
||||
2DED184B15C359AC0091BE97 /* SimpleAudioDriverUserClient.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
2D46CA7817D6AD8A00049D4A /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
2D46CA7917D6AD8A00049D4A /* English */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D47CAA215FEC82B002AAFB5 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
2D47CAA315FEC82B002AAFB5 /* English */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
2D7477A21578162B00412279 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_64_BIT)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
|
||||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||
GCC_WARN_UNKNOWN_PRAGMAS = YES;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2D7477A31578162B00412279 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_64_BIT)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
|
||||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||
GCC_WARN_UNKNOWN_PRAGMAS = YES;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2D7477A41578164E00412279 /* Debug-Opt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_64_BIT)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
|
||||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||
GCC_WARN_UNKNOWN_PRAGMAS = YES;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = "Debug-Opt";
|
||||
};
|
||||
2D7477B61578168D00412279 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=0",
|
||||
);
|
||||
INFOPLIST_FILE = "NullAudio/NullAudio-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2D7477B71578168D00412279 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
INFOPLIST_FILE = "NullAudio/NullAudio-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2D7477B81578168D00412279 /* Debug-Opt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
INFOPLIST_FILE = "NullAudio/NullAudio-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = "Debug-Opt";
|
||||
};
|
||||
2D76D96F15E48B2000FF0F33 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"CoreAudio_Debug=0",
|
||||
"CoreAudio_UseSysLog=1",
|
||||
);
|
||||
INFOPLIST_FILE = "SimpleAudio/Plug-In/SA_PlugIn-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2D76D97015E48B2000FF0F33 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"CoreAudio_Debug=1",
|
||||
"CoreAudio_UseSysLog=1",
|
||||
);
|
||||
INFOPLIST_FILE = "SimpleAudio/Plug-In/SA_PlugIn-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2D76D97115E48B2000FF0F33 /* Debug-Opt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"CoreAudio_Debug=1",
|
||||
"CoreAudio_UseSysLog=1",
|
||||
);
|
||||
INFOPLIST_FILE = "SimpleAudio/Plug-In/SA_PlugIn-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = driver;
|
||||
};
|
||||
name = "Debug-Opt";
|
||||
};
|
||||
2DED184615C357180091BE97 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "";
|
||||
INFOPLIST_FILE = "SimpleAudio/Driver/SimpleAudioDriver-Info.plist";
|
||||
INSTALL_PATH = "$(SYSTEM_LIBRARY_DIR)/Extensions";
|
||||
MODULE_NAME = com.apple.audio.SimpleAudioDriver;
|
||||
MODULE_VERSION = 1.0;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = kext;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2DED184715C357180091BE97 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
|
||||
INFOPLIST_FILE = "SimpleAudio/Driver/SimpleAudioDriver-Info.plist";
|
||||
INSTALL_PATH = "$(SYSTEM_LIBRARY_DIR)/Extensions";
|
||||
MODULE_NAME = com.apple.audio.SimpleAudioDriver;
|
||||
MODULE_VERSION = 1.0;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = kext;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2DED184815C357180091BE97 /* Debug-Opt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
|
||||
INFOPLIST_FILE = "SimpleAudio/Driver/SimpleAudioDriver-Info.plist";
|
||||
INSTALL_PATH = "$(SYSTEM_LIBRARY_DIR)/Extensions";
|
||||
MODULE_NAME = com.apple.audio.SimpleAudioDriver;
|
||||
MODULE_VERSION = 1.0;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
WRAPPER_EXTENSION = kext;
|
||||
};
|
||||
name = "Debug-Opt";
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
2D7477A01578162B00412279 /* Build configuration list for PBXProject "AudioDriverExamples" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2D7477A21578162B00412279 /* Release */,
|
||||
2D7477A31578162B00412279 /* Debug */,
|
||||
2D7477A41578164E00412279 /* Debug-Opt */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
2D7477B51578168D00412279 /* Build configuration list for PBXNativeTarget "NullAudio" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2D7477B61578168D00412279 /* Release */,
|
||||
2D7477B71578168D00412279 /* Debug */,
|
||||
2D7477B81578168D00412279 /* Debug-Opt */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
2D76D97215E48B2000FF0F33 /* Build configuration list for PBXNativeTarget "SimpleAudioPlugIn" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2D76D96F15E48B2000FF0F33 /* Release */,
|
||||
2D76D97015E48B2000FF0F33 /* Debug */,
|
||||
2D76D97115E48B2000FF0F33 /* Debug-Opt */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
2DED184515C357180091BE97 /* Build configuration list for PBXNativeTarget "SimpleAudioDriver" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2DED184615C357180091BE97 /* Release */,
|
||||
2DED184715C357180091BE97 /* Debug */,
|
||||
2DED184815C357180091BE97 /* Debug-Opt */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 2D74779D1578162B00412279 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.apple.audio.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFPlugInFactories</key>
|
||||
<dict>
|
||||
<key>99A15A8B-DA3C-42C3-BD5D-D035A0C2377A</key>
|
||||
<string>NullAudio_Create</string>
|
||||
</dict>
|
||||
<key>CFPlugInTypes</key>
|
||||
<dict>
|
||||
<key>443ABAB8-E7B3-491A-B985-BEB9187030DB</key>
|
||||
<array>
|
||||
<string>99A15A8B-DA3C-42C3-BD5D-D035A0C2377A</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
TravisCI's OS X VMs don't have any audio devices installed by default, so we
|
||||
install Apple's NullAudio sample driver before the tests run. Otherwise the
|
||||
tests would fail because BGMApp currently crashes on launch if you don't have
|
||||
any audio devices.
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* BGMApp.h
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class BGMAppOutputDevice, BGMAppApplication;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Background Music
|
||||
*/
|
||||
|
||||
// an output audio device
|
||||
@interface BGMAppOutputDevice : SBObject
|
||||
|
||||
@property (copy, readonly) NSString *name;
|
||||
@property BOOL selected; // is this the device to be used for audio output?
|
||||
|
||||
@end
|
||||
|
||||
// The application program
|
||||
@interface BGMAppApplication : SBApplication
|
||||
|
||||
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
|
||||
|
||||
@end
|
||||
|
||||
-2
@@ -16,8 +16,6 @@
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
@@ -0,0 +1,173 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppUITests.m
|
||||
// BGMAppUITests
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// You might want to use Xcode's UI test recording feature if you add new tests.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_TestUtils.h"
|
||||
#import "BGM_Types.h"
|
||||
|
||||
// Scripting Bridge Includes
|
||||
#import "BGMApp.h"
|
||||
|
||||
|
||||
// TODO: Skip these tests if macOS SDK 10.11 or higher isn't available.
|
||||
// TODO: Mock BGMDevice and music players.
|
||||
|
||||
@interface BGMAppUITests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation BGMAppUITests {
|
||||
// The BGMApp instance.
|
||||
XCUIApplication* app;
|
||||
|
||||
// Convenience vars.
|
||||
//
|
||||
// The menu bar icon. (Called the status bar icon in some places.)
|
||||
XCUIElement* icon;
|
||||
// The menu items in the main menu.
|
||||
XCUIElementQuery* menuItems;
|
||||
// The Preferences menu item.
|
||||
XCUIElement* prefs;
|
||||
}
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
self.continueAfterFailure = NO;
|
||||
|
||||
// Set up the app object and some convenience vars.
|
||||
app = [[XCUIApplication alloc] init];
|
||||
menuItems = app.menuBars.menuItems;
|
||||
icon = [app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
|
||||
prefs = menuItems[@"Preferences"];
|
||||
|
||||
// TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp.
|
||||
|
||||
// Tell BGMApp not to load/store user defaults (settings) and to use
|
||||
// NSApplicationActivationPolicyRegular. If it used the "accessory" policy as usual, the tests
|
||||
// would fail to start because of a bug in Xcode.
|
||||
app.launchArguments = @[ @"--no-persistent-data", @"--show-dock-icon" ];
|
||||
|
||||
// Launch BGMApp.
|
||||
[app launch];
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
// Click the quit menu item.
|
||||
if (!menuItems.count) {
|
||||
[icon click];
|
||||
}
|
||||
|
||||
[menuItems[@"Quit Background Music"] click];
|
||||
|
||||
// BGMApp should quit.
|
||||
XCTAssert(!app.exists);
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void) testCycleOutputDevices {
|
||||
const int NUM_CYCLES = 2;
|
||||
|
||||
// Get the list of output devices from the preferences menu.
|
||||
[icon click];
|
||||
[prefs hover];
|
||||
NSArray<XCUIElement*>* outputDeviceMenuItems = [self outputDeviceMenuItems];
|
||||
|
||||
// For debugging certain issues, it can be useful to repeatedly switch between two
|
||||
// devices:
|
||||
// outputDeviceMenuItems = [outputDeviceMenuItems subarrayWithRange:NSMakeRange(0,2)];
|
||||
|
||||
XCTAssertGreaterThan(outputDeviceMenuItems.count, 0);
|
||||
|
||||
// Click the last device to close the menu again.
|
||||
[outputDeviceMenuItems.lastObject click];
|
||||
|
||||
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
|
||||
|
||||
for (int i = 0; i < NUM_CYCLES; i++) {
|
||||
// Select each output device.
|
||||
for (XCUIElement* item in outputDeviceMenuItems) {
|
||||
[icon click];
|
||||
[prefs hover];
|
||||
|
||||
[item click];
|
||||
|
||||
// Assert that the device we clicked is the selected device now.
|
||||
for (BGMAppOutputDevice* device in [sbApp outputDevices]) {
|
||||
// TODO: This seems a bit fragile. Would it still work with long device names?
|
||||
if ([device.name isEqualToString:[item title]]) {
|
||||
XCTAssert(device.selected);
|
||||
} else {
|
||||
XCTAssertFalse(device.selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the menu items for the output devices in the preferences menu.
|
||||
- (NSArray<XCUIElement*>*) outputDeviceMenuItems {
|
||||
NSArray<XCUIElement*>* items = @[];
|
||||
BOOL inOutputDeviceSection = NO;
|
||||
|
||||
for (int i = 0; i < prefs.menuItems.count; i++) {
|
||||
XCUIElement* menuItem = [prefs.menuItems elementBoundByIndex:i];
|
||||
|
||||
if ([menuItem.title isEqual:@"Output Device"]) {
|
||||
inOutputDeviceSection = YES;
|
||||
} else if (inOutputDeviceSection) {
|
||||
// Assume that finding a separator menu item means we've reached the end of the section.
|
||||
if (((NSMenuItem*)menuItem.value).separatorItem || [menuItem.title isEqual:@""]) {
|
||||
break;
|
||||
}
|
||||
|
||||
items = [items arrayByAddingObject:menuItem];
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
- (void) testSelectMusicPlayer {
|
||||
// Select VLC as the music player.
|
||||
[icon click];
|
||||
[prefs hover];
|
||||
[prefs.menuItems[@"VLC"] click];
|
||||
|
||||
// The name of the Auto-pause menu item should change. Also check the accessibility identifier.
|
||||
[icon click];
|
||||
XCTAssertEqualObjects(menuItems[@"Auto-pause VLC"].identifier, @"Auto-pause enabled");
|
||||
|
||||
// Select iTunes as the music player.
|
||||
[prefs hover];
|
||||
[prefs.menuItems[@"iTunes"] click];
|
||||
|
||||
// The name of the Auto-pause menu item should change back.
|
||||
[icon click];
|
||||
XCTAssert(menuItems[@"Auto-pause iTunes"].exists);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python2.7
|
||||
|
||||
# This file is part of Background Music.
|
||||
#
|
||||
# Background Music is free software: you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License as
|
||||
# published by the Free Software Foundation, either version 2 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Background Music is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# travis-skip.py
|
||||
# BGMAppUITests
|
||||
#
|
||||
# Copyright (c) 2017 Kyle Neideck
|
||||
#
|
||||
# Skip the UI tests in Travis builds because they aren't supported.
|
||||
#
|
||||
# We can't run the tests on Travis because Xcode needs permission to use the Accessibility API
|
||||
# to control BGMApp. There's no way to set that up programmatically without disabling SIP and
|
||||
# Travis doesn't support that.
|
||||
#
|
||||
# See https://github.com/travis-ci/travis-ci/issues/5819
|
||||
#
|
||||
# TODO: Figure out a better way to do this.
|
||||
#
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
SCHEME_FILE = "BGMApp/BGMApp.xcodeproj/xcshareddata/xcschemes/Background Music.xcscheme"
|
||||
UI_REF_XPATH = ".//BuildableReference[@BlueprintName='BGMAppUITests']/.."
|
||||
|
||||
# Parse the Xcode scheme.
|
||||
tree = ET.parse(SCHEME_FILE)
|
||||
|
||||
# Set the TestableReference for the UI tests to skipped.
|
||||
tree.getroot().findall(UI_REF_XPATH)[0].set("skipped", "YES")
|
||||
|
||||
# Save the scheme.
|
||||
tree.write(SCHEME_FILE)
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,210 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMMusicPlayersUnitTests.mm
|
||||
// BGMAppUnitTests
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Unit include
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// Local includes
|
||||
#import "BGM_TestUtils.h"
|
||||
|
||||
#import "BGM_Types.h"
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMiTunes.h"
|
||||
#import "BGMVLC.h"
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMSpotify.h"
|
||||
|
||||
// PublicUtility includes
|
||||
#import "CAHALAudioDevice.h" // Mocked
|
||||
|
||||
// System includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
@interface BGMMockAudioDeviceManager : BGMAudioDeviceManager
|
||||
@end
|
||||
|
||||
@implementation BGMMockAudioDeviceManager
|
||||
|
||||
+ (CAHALAudioDevice) bgmDevice {
|
||||
static CAHALAudioDevice device(CFSTR("MockBGMDevice"));
|
||||
return device;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ----
|
||||
|
||||
@interface BGMMockUserDefaults : BGMUserDefaults
|
||||
|
||||
@property NSUUID* selectedPlayerID;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMMockUserDefaults
|
||||
|
||||
- (void) registerDefaults {
|
||||
}
|
||||
|
||||
- (NSString* __nullable) selectedMusicPlayerID {
|
||||
return [self.selectedPlayerID UUIDString];
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID {
|
||||
#pragma unused (selectedMusicPlayerID)
|
||||
}
|
||||
|
||||
- (BOOL) autoPauseMusicEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
|
||||
#pragma unused (autoPauseMusicEnabled)
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ----
|
||||
|
||||
@interface BGMMusicPlayersUnitTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation BGMMusicPlayersUnitTests {
|
||||
BGMMockAudioDeviceManager* devices;
|
||||
BGMMockUserDefaults* defaults;
|
||||
|
||||
NSUUID* spotifyID;
|
||||
NSUUID* vlcID;
|
||||
}
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
|
||||
devices = [BGMMockAudioDeviceManager new];
|
||||
defaults = [BGMMockUserDefaults new];
|
||||
|
||||
// These are the IDs hardcoded in BGMSpotify and BGMVLC.
|
||||
spotifyID = [[NSUUID alloc] initWithUUIDString:@"EC2A907F-8515-4687-9570-1BF63176E6D8"];
|
||||
vlcID = [[NSUUID alloc] initWithUUIDString:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"];
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
[self resetDevice];
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void) testNoSelectedMusicPlayerStored {
|
||||
// Test the case where the user has never changed the music player preference.
|
||||
|
||||
// Test with iTunes as the default.
|
||||
BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
musicPlayerClasses:@[ BGMiTunes.class, BGMVLC.class ]
|
||||
userDefaults:defaults];
|
||||
|
||||
XCTAssertEqual(players.musicPlayers.count, 2);
|
||||
|
||||
for (id<BGMMusicPlayer> player in players.musicPlayers) {
|
||||
XCTAssertTrue([player isKindOfClass:BGMiTunes.class] || [player isKindOfClass:BGMVLC.class]);
|
||||
}
|
||||
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, [BGMiTunes sharedMusicPlayerID]);
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"iTunes");
|
||||
|
||||
[self resetDevice];
|
||||
|
||||
// Test with VLC as the default.
|
||||
players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
defaultMusicPlayerID:vlcID
|
||||
musicPlayerClasses:@[ BGMiTunes.class,
|
||||
BGMVLC.class,
|
||||
BGMDecibel.class ]
|
||||
userDefaults:defaults];
|
||||
|
||||
XCTAssertEqual(players.musicPlayers.count, 3);
|
||||
|
||||
for (id<BGMMusicPlayer> player in players.musicPlayers) {
|
||||
XCTAssertTrue([player isKindOfClass:BGMiTunes.class] ||
|
||||
[player isKindOfClass:BGMVLC.class] ||
|
||||
[player isKindOfClass:BGMDecibel.class]);
|
||||
}
|
||||
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, vlcID);
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"VLC");
|
||||
}
|
||||
|
||||
- (void) testSelectedMusicPlayerInUserDefaults {
|
||||
defaults.selectedPlayerID = spotifyID;
|
||||
|
||||
BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
musicPlayerClasses:@[ BGMiTunes.class,
|
||||
BGMVLC.class,
|
||||
BGMSpotify.class ]
|
||||
userDefaults:defaults];
|
||||
|
||||
XCTAssertEqual(players.musicPlayers.count, 3);
|
||||
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, spotifyID);
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"Spotify");
|
||||
|
||||
[self resetDevice];
|
||||
|
||||
// If there's an unrecognized ID in user defaults, the default music player should be selected.
|
||||
defaults.selectedPlayerID = [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-0000-000000000000"];
|
||||
|
||||
// This initializer sets iTunes as the default music player and adds all the other music players.
|
||||
players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
userDefaults:defaults];
|
||||
|
||||
XCTAssert(players.musicPlayers.count >= 6);
|
||||
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, [BGMiTunes sharedMusicPlayerID]);
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"iTunes");
|
||||
}
|
||||
|
||||
- (void) testSelectedMusicPlayerInBGMDeviceProperties {
|
||||
// When it doesn't find a selected music player in user defaults, it should check BGMDevice's music
|
||||
// player properties.
|
||||
|
||||
[devices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress,
|
||||
CFSTR("org.videolan.vlc"));
|
||||
|
||||
BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
userDefaults:defaults];
|
||||
|
||||
XCTAssert(players.musicPlayers.count >= 6);
|
||||
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, vlcID);
|
||||
XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"VLC");
|
||||
}
|
||||
|
||||
// TODO: Test setting the selectedMusicPlayer property
|
||||
|
||||
- (void) resetDevice {
|
||||
// Reset the mock BGMDevice.
|
||||
[devices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress, NULL);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// Mock_CAHALAudioObject.cpp
|
||||
// BGMAppUnitTests
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
#include "CAHALAudioObject.h"
|
||||
|
||||
// BGM Includes
|
||||
#include "BGM_Types.h"
|
||||
|
||||
// System includes
|
||||
#include <CoreAudio/AudioHardware.h>
|
||||
|
||||
|
||||
#pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
|
||||
// The value of the music player bundle ID property. Tests should set this back to NULL when they finish. (Has
|
||||
// to be static because we can't add to the real class's interface.)
|
||||
static CFStringRef __nullable playerBundleID = NULL;
|
||||
|
||||
CAHALAudioObject::CAHALAudioObject(AudioObjectID inObjectID)
|
||||
:
|
||||
mObjectID(inObjectID)
|
||||
{
|
||||
}
|
||||
|
||||
CAHALAudioObject::~CAHALAudioObject()
|
||||
{
|
||||
}
|
||||
|
||||
void CAHALAudioObject::GetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32& ioDataSize, void* outData) const
|
||||
{
|
||||
if(inAddress.mSelector == kAudioDeviceCustomPropertyMusicPlayerBundleID)
|
||||
{
|
||||
*reinterpret_cast<CFStringRef*>(outData) = playerBundleID;
|
||||
}
|
||||
}
|
||||
|
||||
void CAHALAudioObject::SetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData)
|
||||
{
|
||||
if(inAddress.mSelector == kAudioDeviceCustomPropertyMusicPlayerBundleID)
|
||||
{
|
||||
playerBundleID = *reinterpret_cast<const CFStringRef*>(inData);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Unimplemented methods
|
||||
|
||||
AudioObjectID CAHALAudioObject::GetObjectID() const
|
||||
{
|
||||
return kAudioObjectUnknown;
|
||||
}
|
||||
|
||||
void CAHALAudioObject::SetObjectID(AudioObjectID inObjectID)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioClassID CAHALAudioObject::GetClassID() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioObject::GetOwnerObjectID() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyOwningPlugInBundleID() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyName() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyManufacturer() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyCategoryNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
CFStringRef CAHALAudioObject::CopyNumberNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
bool CAHALAudioObject::ObjectExists(AudioObjectID inObjectID)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
UInt32 CAHALAudioObject::GetNumberOwnedObjects(AudioClassID inClass) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioObject::GetAllOwnedObjects(AudioClassID inClass, UInt32& ioNumberObjects, AudioObjectID* ioObjectIDs) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioObject::GetOwnedObjectByIndex(AudioClassID inClass, UInt32 inIndex)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
bool CAHALAudioObject::HasProperty(const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
bool CAHALAudioObject::IsPropertySettable(const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
UInt32 CAHALAudioObject::GetPropertyDataSize(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioObject::AddPropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioObject::RemovePropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static NSXPCListenerEndpoint* sBGMAppEndpoint = nil;
|
||||
static NSXPCConnection* sBGMAppConnection = nil;
|
||||
static NSXPCListenerEndpoint* __nullable sBGMAppEndpoint = nil;
|
||||
static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
|
||||
@implementation BGMXPCHelperService {
|
||||
NSXPCConnection* connection;
|
||||
@@ -73,7 +73,7 @@ static NSXPCConnection* sBGMAppConnection = nil;
|
||||
if (!sBGMAppConnection && sBGMAppEndpoint) {
|
||||
// Create a new connection to BGMApp from the endpoint
|
||||
@synchronized(self) {
|
||||
sBGMAppConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:sBGMAppEndpoint];
|
||||
sBGMAppConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:(NSXPCListenerEndpoint* __nonnull)sBGMAppEndpoint];
|
||||
NSAssert(sBGMAppConnection, @"NSXPCConnection::initWithListenerEndpoint returned nil");
|
||||
|
||||
[sBGMAppConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]];
|
||||
@@ -182,7 +182,7 @@ static NSXPCConnection* sBGMAppConnection = nil;
|
||||
}];
|
||||
|
||||
// Wait for BGMApp's reply
|
||||
long err = dispatch_semaphore_wait(bgmAppReplySemaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC));
|
||||
long err = dispatch_semaphore_wait(bgmAppReplySemaphore, dispatch_time(DISPATCH_TIME_NOW, kStartIOTimeoutNsec));
|
||||
|
||||
if (err != 0) {
|
||||
replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_Timeout
|
||||
|
||||
@@ -58,14 +58,14 @@
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
int main(int argc, const char* __nullable argv[]) {
|
||||
#pragma unused (argc, argv)
|
||||
|
||||
DebugMsg("BGMXPCHelper::main: Service starting up");
|
||||
|
||||
// Set up the one NSXPCListener for this service. It will handle all incoming connections. This checks our service in with
|
||||
// the bootstrap service.
|
||||
NSXPCListener *listener = [[NSXPCListener alloc] initWithMachServiceName:kBGMXPCHelperMachServiceName];
|
||||
NSXPCListener* listener = [[NSXPCListener alloc] initWithMachServiceName:kBGMXPCHelperMachServiceName];
|
||||
BGMXPCListenerDelegate* delegate = [BGMXPCListenerDelegate new];
|
||||
listener.delegate = delegate;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# post_install.sh
|
||||
# BGMXPCHelper
|
||||
#
|
||||
# Copyright © 2016 Kyle Neideck
|
||||
# Copyright © 2016, 2017 Kyle Neideck
|
||||
#
|
||||
# Installs BGMXPCHelper's launchd plist file and "bootstraps" (registers/enables) it with launchd.
|
||||
#
|
||||
@@ -28,30 +28,47 @@
|
||||
# runs as the final build phase.
|
||||
#
|
||||
|
||||
# Check the environment variables we need from Xcode.
|
||||
if [[ -z ${EXECUTABLE_PATH} ]]; then
|
||||
echo "Environment variable EXECUTABLE_PATH was not set." >&2
|
||||
exit 1
|
||||
PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH
|
||||
|
||||
# Check we have the paths we need, either in environment variables from Xcode or from the args.
|
||||
if [[ -z $1 ]]; then
|
||||
if [[ -z ${INSTALL_DIR} ]]; then
|
||||
echo "Environment variable INSTALL_DIR was not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
INSTALL_DIR="$1"
|
||||
fi
|
||||
if [[ -z ${INSTALL_DIR} ]]; then
|
||||
echo "Environment variable INSTALL_DIR was not set." >&2
|
||||
exit 1
|
||||
|
||||
if [[ -z $2 ]]; then
|
||||
if [[ -z ${EXECUTABLE_PATH} ]]; then
|
||||
echo "Environment variable EXECUTABLE_PATH was not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
EXECUTABLE_PATH="$2"
|
||||
fi
|
||||
if [[ -z ${TARGET_BUILD_DIR} ]]; then
|
||||
echo "Environment variable TARGET_BUILD_DIR was not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH} ]]; then
|
||||
echo "Environment variable UNLOCALIZED_RESOURCES_FOLDER_PATH was not set." >&2
|
||||
exit 1
|
||||
|
||||
if [[ -z $3 ]]; then
|
||||
if [[ -z ${TARGET_BUILD_DIR} ]]; then
|
||||
echo "Environment variable TARGET_BUILD_DIR was not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH} ]]; then
|
||||
echo "Environment variable UNLOCALIZED_RESOURCES_FOLDER_PATH was not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESOURCES_PATH="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
else
|
||||
RESOURCES_PATH="$3"
|
||||
fi
|
||||
|
||||
# Safe mode.
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
RESOURCES_PATH="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
|
||||
# Show a warning if INSTALL_DIR isn't set to a safe installation directory.
|
||||
if [[ $(bash "${RESOURCES_PATH}/safe_install_dir.sh" "${INSTALL_DIR}") != 1 ]]; then
|
||||
echo "$(tput setaf 11)WARNING$(tput sgr0): Installing to \"${INSTALL_DIR}\" may be" \
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# safe_install_dir.sh
|
||||
# BGMXPCHelper
|
||||
#
|
||||
# Copyright © 2016 Kyle Neideck
|
||||
# Copyright © 2016, 2017 Kyle Neideck
|
||||
#
|
||||
# Prints the path to a directory the BGMXPCHelper bundle can safely be installed to. Intended to be
|
||||
# used as the INSTALL_DIR environment variable for xcodebuild commands. For example,
|
||||
@@ -56,6 +56,8 @@
|
||||
# recommendation above, or "0" otherwise.
|
||||
#
|
||||
|
||||
PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH
|
||||
|
||||
# Safe mode.
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
@@ -96,13 +98,17 @@ check_dir() {
|
||||
# Used when we can't find a suitable installation directory. Prints an error message and exits.
|
||||
# (Reaching this point should be very uncommon.)
|
||||
fail() {
|
||||
echo "$(tput setaf 11)WARNING$(tput sgr0): Installing BGMXPCHelper to its default location" \
|
||||
"(${INSTALL_DIR} or, as a backup, ${BACKUP_INSTALL_DIR}) might not be secure on this" \
|
||||
"system. It's recommended that each directory from the installation directory up to the" \
|
||||
"root directory should be owned by root and not writable by any other user. See" \
|
||||
"safe_install_dir.sh for more details." >&2
|
||||
if [[ $ALLOW_UNSAFE_FALLBACK -eq 1 ]]; then
|
||||
CONTINUE_ANYWAY="y"
|
||||
else
|
||||
echo "$(tput setaf 11)WARNING$(tput sgr0): Installing BGMXPCHelper to its default" \
|
||||
"location (${INSTALL_DIR} or, as a backup, ${BACKUP_INSTALL_DIR}) might not be" \
|
||||
"secure on this system. It's recommended that each directory from the installation" \
|
||||
"directory up to the root directory should be owned by root and not writable by any" \
|
||||
"other user. See safe_install_dir.sh for more details." >&2
|
||||
|
||||
read -e -p "Continue anyway? [y/N]" CONTINUE_ANYWAY
|
||||
read -e -p "Continue anyway? [y/N]" CONTINUE_ANYWAY
|
||||
fi
|
||||
|
||||
if [[ "${CONTINUE_ANYWAY}" == "y" ]] || [[ "${CONTINUE_ANYWAY}" == "Y" ]]; then
|
||||
echo "${INSTALL_DIR}"
|
||||
@@ -114,16 +120,26 @@ fail() {
|
||||
fi
|
||||
}
|
||||
|
||||
# This script can be given a directory to check as an argument.
|
||||
# (Uses "${1+x}" instead of "$1" because having our "safe mode" enabled makes the script fail if you
|
||||
# reference an unset variable, even to check whether it's set or not.)
|
||||
if [[ ! -z "${1+x}" ]]; then
|
||||
# Check the given path exists and is a directory.
|
||||
if [[ ! -d "$1" ]]; then echo "$1 is not a directory." >&2; exit 1; fi
|
||||
ALLOW_UNSAFE_FALLBACK=0
|
||||
|
||||
check_dir "$1"
|
||||
echo ${DIR_IS_SAFE}
|
||||
exit 0
|
||||
# This script can be given a directory to check as an argument, or the -y option, which tells this
|
||||
# script to print the default dir if neither of the dirs are safe. The pkg installer uses -y so it
|
||||
# can install anyway and show the user instructions to fix the permissions, rather than just
|
||||
# failing.
|
||||
#
|
||||
# (This line uses "${1+x}" instead of "$1" because having our "safe mode" enabled makes the script
|
||||
# fail if you reference an unset variable, even to check whether it's set or not.)
|
||||
if [[ ! -z "${1+x}" ]]; then
|
||||
if [[ "$1" == "-y" ]]; then
|
||||
ALLOW_UNSAFE_FALLBACK=1
|
||||
else
|
||||
# Check the given path exists and is a directory.
|
||||
if [[ ! -d "$1" ]]; then echo "$1 is not a directory." >&2; exit 1; fi
|
||||
|
||||
check_dir "$1"
|
||||
echo ${DIR_IS_SAFE}
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# These are just for readability and to save keystrokes. If you change them, you'll have to change
|
||||
@@ -143,7 +159,7 @@ if [[ ! -e "${INSTALL_DIR}" ]]; then
|
||||
sudo chmod go-w "${INSTALL_DIR}"
|
||||
fi
|
||||
|
||||
# Check the directory Xcode installed the build to.
|
||||
# Check the directory the build was installed to.
|
||||
check_dir "${INSTALL_DIR}"
|
||||
|
||||
if [[ ${DIR_IS_SAFE} -eq 1 ]]; then
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
|
||||
//
|
||||
// BGMXPCHelperTests.m
|
||||
// BGMApp
|
||||
// BGMXPCHelperTests
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
@@ -69,7 +69,7 @@
|
||||
dispatch_semaphore_signal(replySemaphore);
|
||||
}];
|
||||
|
||||
if (0 != dispatch_semaphore_wait(replySemaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC))) {
|
||||
if (0 != dispatch_semaphore_wait(replySemaphore, dispatch_time(DISPATCH_TIME_NOW, kStartIOTimeoutNsec))) {
|
||||
XCTFail(@"Timed out waiting for BGMXPCHelper");
|
||||
}
|
||||
}
|
||||
@@ -67,24 +67,50 @@ void LogError(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
//#if TARGET_API_MAC_OSX
|
||||
// vsyslog(LOG_ERR, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[ERROR] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
vprintf(fmt, args);
|
||||
#endif
|
||||
#if TARGET_API_MAC_OSX
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
CADebuggerStop();
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void LogWarning(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
//#if TARGET_API_MAC_OSX
|
||||
// vsyslog(LOG_WARNING, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[WARNING] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
vprintf(fmt, args);
|
||||
#endif
|
||||
#if TARGET_API_MAC_OSX
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
//CADebuggerStop(); // TODO: Add a toggle for this to the project file (under "Preprocessor Macros"). Default to off.
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
@@ -194,8 +194,9 @@
|
||||
#define DebugMessageN8(msg, N1, N2, N3, N4, N5, N6, N7, N8) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8)
|
||||
#define DebugMessageN9(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9)
|
||||
|
||||
void LogError(const char *fmt, ...); // writes to syslog (and stderr if debugging)
|
||||
void LogWarning(const char *fmt, ...); // writes to syslog (and stderr if debugging)
|
||||
// BGM edit: Added __printflike.
|
||||
void LogError(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging)
|
||||
void LogWarning(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging)
|
||||
|
||||
#define NO_ACTION (void)0
|
||||
|
||||
|
||||
@@ -3,45 +3,53 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objectVersion = 47;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B01C642C600084C15A /* BGM_Client.cpp */; };
|
||||
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B21C642C600084C15A /* BGM_ClientMap.cpp */; };
|
||||
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B41C642C600084C15A /* BGM_Clients.cpp */; };
|
||||
1C305D9D1BE294B5004EBB91 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */; };
|
||||
1C30A69F1C1E98F000C05AA5 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; };
|
||||
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */; };
|
||||
1C3821111C4A18DE00A0C8C6 /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */; };
|
||||
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */; };
|
||||
1C8034DD1BDD073B00668E00 /* BGM_ClientsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */; };
|
||||
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; };
|
||||
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */; };
|
||||
1CB8B3731BBBD8A4000E2DD1 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3701BBBD8A4000E2DD1 /* CADebugMacros.cpp */; };
|
||||
1CB8B3761BBBD924000E2DD1 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */; };
|
||||
1CB8B3771BBBD924000E2DD1 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */; };
|
||||
1CB8B37A1BBBDFA2000E2DD1 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3781BBBDFA2000E2DD1 /* CADebugPrintf.cpp */; };
|
||||
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B37B1BBCCF62000E2DD1 /* BGM_PlugIn.cpp */; };
|
||||
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */; };
|
||||
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3811BBCE7B5000E2DD1 /* BGM_Object.cpp */; };
|
||||
1CB8B3861BBCEFE8000E2DD1 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; };
|
||||
1CB8B3891BBCF08A000E2DD1 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3871BBCF08A000E2DD1 /* CAHostTimeBase.cpp */; };
|
||||
1CB8B38E1BBCF4A9000E2DD1 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38A1BBCF4A9000E2DD1 /* CACFString.cpp */; };
|
||||
1CB8B38F1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38C1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp */; };
|
||||
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3901BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp */; };
|
||||
1CB8B3951BBD2418000E2DD1 /* CADispatchQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3931BBD2418000E2DD1 /* CADispatchQueue.cpp */; };
|
||||
1CBB322C1BDD3A3000C9BD55 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */; };
|
||||
1CC1DF891BE558B000FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF871BE558B000FB8FE4 /* CADebugger.cpp */; };
|
||||
1CC1DF8A1BE5703B00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68F1BE2683900167F5D /* CACFArray.cpp */; };
|
||||
1CC1DF8B1BE5703B00FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */; };
|
||||
1CC1DF8D1BE5705700FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */; };
|
||||
1CC1DF8E1BE5706C00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68F1BE2683900167F5D /* CACFArray.cpp */; };
|
||||
1CC1DF931BE7B79500FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF871BE558B000FB8FE4 /* CADebugger.cpp */; };
|
||||
1CC1DF941BE7B79500FB8FE4 /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38C1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp */; };
|
||||
1CC1DF9E1BE94AA200FB8FE4 /* DeviceIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 1CC1DF9D1BE94AA200FB8FE4 /* DeviceIcon.icns */; };
|
||||
1CD95B121E93AA5200EB8EF0 /* BGM_AbstractDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */; };
|
||||
1CD95B131E93AA5200EB8EF0 /* BGM_NullDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */; };
|
||||
1CD95B141E93AA5200EB8EF0 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; };
|
||||
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */; };
|
||||
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */; };
|
||||
27379B821C76D62D0084A24C /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3701BBBD8A4000E2DD1 /* CADebugMacros.cpp */; };
|
||||
27379B831C76D62D0084A24C /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3781BBBDFA2000E2DD1 /* CADebugPrintf.cpp */; };
|
||||
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 27381A141C8EF50F00DF167C /* BGM_XPCHelper.m */; };
|
||||
2743C9CD1D7EF8760089613B /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68F1BE2683900167F5D /* CACFArray.cpp */; };
|
||||
2743C9CF1D7EF8760089613B /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */; };
|
||||
2743C9D11D7EF8760089613B /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */; };
|
||||
2743C9D31D7EF8760089613B /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38A1BBCF4A9000E2DD1 /* CACFString.cpp */; };
|
||||
2743C9D51D7EF8760089613B /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF871BE558B000FB8FE4 /* CADebugger.cpp */; };
|
||||
2743C9D71D7EF8760089613B /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3701BBBD8A4000E2DD1 /* CADebugMacros.cpp */; };
|
||||
2743C9D91D7EF8760089613B /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3781BBBDFA2000E2DD1 /* CADebugPrintf.cpp */; };
|
||||
2743C9DB1D7EF8760089613B /* CADispatchQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3931BBD2418000E2DD1 /* CADispatchQueue.cpp */; };
|
||||
2743C9DE1D7EF8760089613B /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3871BBCF08A000E2DD1 /* CAHostTimeBase.cpp */; };
|
||||
2743C9E01D7EF8760089613B /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; };
|
||||
2743C9E21D7EF8760089613B /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */; };
|
||||
2743C9E41D7EF8760089613B /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38C1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp */; };
|
||||
2743C9E61D7EF8E00089613B /* libPublicUtility.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2743C9C61D7EF84B0089613B /* libPublicUtility.a */; };
|
||||
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 275343BC1DE9B44900DF3858 /* BGM_Utils.cpp */; };
|
||||
277170101CA0CFC300AB34B4 /* BGM_PlugInInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */; };
|
||||
277170111CA0CFC300AB34B4 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */; };
|
||||
277EE6591C7269910037F1EE /* BGM_ClientMapTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 277EE6581C7269910037F1EE /* BGM_ClientMapTests.mm */; };
|
||||
@@ -59,6 +67,7 @@
|
||||
277EE6651C728CDB0037F1EE /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3871BBCF08A000E2DD1 /* CAHostTimeBase.cpp */; };
|
||||
2795973E1C9847CF00A002FB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2795973D1C9847CF00A002FB /* Foundation.framework */; };
|
||||
27D643C31C9FBE1600737F6E /* BGM_XPCHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 27381A141C8EF50F00DF167C /* BGM_XPCHelper.m */; };
|
||||
27E6B5F01E01966A00EC0AAB /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 275343BC1DE9B44900DF3858 /* BGM_Utils.cpp */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -74,6 +83,7 @@
|
||||
1C0CB6B81C642C600084C15A /* BGM_ClientTasks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_ClientTasks.h; sourceTree = "<group>"; };
|
||||
1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFNumber.cpp; path = PublicUtility/CACFNumber.cpp; sourceTree = "<group>"; };
|
||||
1C305D9C1BE294B5004EBB91 /* CACFNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFNumber.h; path = PublicUtility/CACFNumber.h; sourceTree = "<group>"; };
|
||||
1C37B3681E9B8D3C000DF98F /* CAPropertyAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPropertyAddress.h; path = PublicUtility/CAPropertyAddress.h; sourceTree = "<group>"; };
|
||||
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_TaskQueue.cpp; sourceTree = "<group>"; };
|
||||
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_TaskQueue.h; sourceTree = "<group>"; };
|
||||
1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
|
||||
@@ -82,6 +92,8 @@
|
||||
1C8034DA1BDD073B00668E00 /* BGMDriverTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMDriverTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_ClientsTests.mm; sourceTree = "<group>"; };
|
||||
1C8034DE1BDD073B00668E00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_Stream.cpp; sourceTree = "<group>"; };
|
||||
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_Stream.h; sourceTree = "<group>"; };
|
||||
1CB8B3641BBBB78D000E2DD1 /* Background Music Device.driver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Background Music Device.driver"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1CB8B3681BBBB78D000E2DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_PlugInInterface.cpp; sourceTree = "<group>"; };
|
||||
@@ -114,12 +126,18 @@
|
||||
1CC1DF881BE558B000FB8FE4 /* CADebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugger.h; path = PublicUtility/CADebugger.h; sourceTree = "<group>"; };
|
||||
1CC1DF991BE865C000FB8FE4 /* quick_install.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = quick_install.sh; sourceTree = "<group>"; };
|
||||
1CC1DF9D1BE94AA200FB8FE4 /* DeviceIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = DeviceIcon.icns; sourceTree = "<group>"; };
|
||||
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_NullDevice.cpp; sourceTree = "<group>"; };
|
||||
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_NullDevice.h; sourceTree = "<group>"; };
|
||||
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_AbstractDevice.cpp; sourceTree = "<group>"; };
|
||||
1CDF3ABE1E8644C20001E9B7 /* BGM_AbstractDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_AbstractDevice.h; sourceTree = "<group>"; };
|
||||
1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFDictionary.cpp; path = PublicUtility/CACFDictionary.cpp; sourceTree = "<group>"; };
|
||||
1CE3E68D1BE263CA00167F5D /* CACFDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFDictionary.h; path = PublicUtility/CACFDictionary.h; sourceTree = "<group>"; };
|
||||
1CE3E68F1BE2683900167F5D /* CACFArray.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFArray.cpp; path = PublicUtility/CACFArray.cpp; sourceTree = "<group>"; };
|
||||
1CE3E6901BE2683900167F5D /* CACFArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFArray.h; path = PublicUtility/CACFArray.h; sourceTree = "<group>"; };
|
||||
27381A141C8EF50F00DF167C /* BGM_XPCHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BGM_XPCHelper.m; sourceTree = "<group>"; };
|
||||
27381A151C8EF50F00DF167C /* BGM_XPCHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_XPCHelper.h; sourceTree = "<group>"; };
|
||||
2743C9C61D7EF84B0089613B /* libPublicUtility.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPublicUtility.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
275343BC1DE9B44900DF3858 /* BGM_Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BGM_Utils.cpp; path = ../SharedSource/BGM_Utils.cpp; sourceTree = "<group>"; };
|
||||
2771700E1CA0C16200AB34B4 /* BGM_Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_Utils.h; path = ../SharedSource/BGM_Utils.h; sourceTree = "<group>"; };
|
||||
277EE6581C7269910037F1EE /* BGM_ClientMapTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_ClientMapTests.mm; sourceTree = "<group>"; };
|
||||
2795973D1C9847CF00A002FB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||
@@ -141,16 +159,24 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2743C9E61D7EF8E00089613B /* libPublicUtility.a in Frameworks */,
|
||||
2795973E1C9847CF00A002FB /* Foundation.framework in Frameworks */,
|
||||
1CB8B3761BBBD924000E2DD1 /* CoreAudio.framework in Frameworks */,
|
||||
1CB8B3771BBBD924000E2DD1 /* CoreFoundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2743C9C31D7EF84B0089613B /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1C0CB6AF1C642C600084C15A /* Device Clients */ = {
|
||||
1C0CB6AF1C642C600084C15A /* DeviceClients */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C0CB6B11C642C600084C15A /* BGM_Client.h */,
|
||||
@@ -161,7 +187,6 @@
|
||||
1C0CB6B41C642C600084C15A /* BGM_Clients.cpp */,
|
||||
1C0CB6B81C642C600084C15A /* BGM_ClientTasks.h */,
|
||||
);
|
||||
name = "Device Clients";
|
||||
path = DeviceClients;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -184,9 +209,7 @@
|
||||
27D643B61C9FABDE00737F6E /* SharedSource */,
|
||||
1CB8B36F1BBBD7AE000E2DD1 /* PublicUtility */,
|
||||
1CB8B3651BBBB78D000E2DD1 /* Products */,
|
||||
2795973D1C9847CF00A002FB /* Foundation.framework */,
|
||||
1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */,
|
||||
1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */,
|
||||
2743CA241D86E2E80089613B /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -195,6 +218,7 @@
|
||||
children = (
|
||||
1CB8B3641BBBB78D000E2DD1 /* Background Music Device.driver */,
|
||||
1C8034DA1BDD073B00668E00 /* BGMDriverTests.xctest */,
|
||||
2743C9C61D7EF84B0089613B /* libPublicUtility.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -207,9 +231,15 @@
|
||||
1CB8B37B1BBCCF62000E2DD1 /* BGM_PlugIn.cpp */,
|
||||
1CB8B3821BBCE7B5000E2DD1 /* BGM_Object.h */,
|
||||
1CB8B3811BBCE7B5000E2DD1 /* BGM_Object.cpp */,
|
||||
1CDF3ABE1E8644C20001E9B7 /* BGM_AbstractDevice.h */,
|
||||
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */,
|
||||
1CB8B37F1BBCCF87000E2DD1 /* BGM_Device.h */,
|
||||
1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */,
|
||||
1C0CB6AF1C642C600084C15A /* Device Clients */,
|
||||
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */,
|
||||
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */,
|
||||
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */,
|
||||
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */,
|
||||
1C0CB6AF1C642C600084C15A /* DeviceClients */,
|
||||
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */,
|
||||
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */,
|
||||
27381A151C8EF50F00DF167C /* BGM_XPCHelper.h */,
|
||||
@@ -235,6 +265,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C0CB6A71C4E06F70084C15A /* CAAtomic.h */,
|
||||
1C37B3681E9B8D3C000DF98F /* CAPropertyAddress.h */,
|
||||
1C0CB6A61C4E06C00084C15A /* CAAtomicStack.h */,
|
||||
1C0CB6A91C50A3AF0084C15A /* CAAutoDisposer.h */,
|
||||
1CE3E68F1BE2683900167F5D /* CACFArray.cpp */,
|
||||
@@ -266,11 +297,22 @@
|
||||
name = PublicUtility;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2743CA241D86E2E80089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */,
|
||||
1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */,
|
||||
2795973D1C9847CF00A002FB /* Foundation.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
27D643B61C9FABDE00737F6E /* SharedSource */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27D643B71C9FABF600737F6E /* BGM_Types.h */,
|
||||
2771700E1CA0C16200AB34B4 /* BGM_Utils.h */,
|
||||
275343BC1DE9B44900DF3858 /* BGM_Utils.cpp */,
|
||||
27D643C21C9FBC5800737F6E /* BGM_TestUtils.h */,
|
||||
27D643B81C9FABF600737F6E /* BGMXPCProtocols.h */,
|
||||
);
|
||||
@@ -279,6 +321,16 @@
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
2743C9C41D7EF84B0089613B /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
1C8034D91BDD073B00668E00 /* BGMDriverTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
@@ -314,6 +366,23 @@
|
||||
productReference = 1CB8B3641BBBB78D000E2DD1 /* Background Music Device.driver */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
2743C9C51D7EF84B0089613B /* PublicUtility */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 2743C9C71D7EF84B0089613B /* Build configuration list for PBXNativeTarget "PublicUtility" */;
|
||||
buildPhases = (
|
||||
2743C9C21D7EF84B0089613B /* Sources */,
|
||||
2743C9C31D7EF84B0089613B /* Frameworks */,
|
||||
2743C9C41D7EF84B0089613B /* Headers */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = PublicUtility;
|
||||
productName = PublicUtility;
|
||||
productReference = 2743C9C61D7EF84B0089613B /* libPublicUtility.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -322,7 +391,7 @@
|
||||
attributes = {
|
||||
CLASSPREFIX = BGM_;
|
||||
LastUpgradeCheck = 0700;
|
||||
ORGANIZATIONNAME = "Kyle Neideck";
|
||||
ORGANIZATIONNAME = "Background Music contributors";
|
||||
TargetAttributes = {
|
||||
1C8034D91BDD073B00668E00 = {
|
||||
CreatedOnToolsVersion = 7.0.1;
|
||||
@@ -330,10 +399,14 @@
|
||||
1CB8B3631BBBB78D000E2DD1 = {
|
||||
CreatedOnToolsVersion = 6.4;
|
||||
};
|
||||
2743C9C51D7EF84B0089613B = {
|
||||
CreatedOnToolsVersion = 8.0;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 1CB8B35D1BBBB69C000E2DD1 /* Build configuration list for PBXProject "BGMDriver" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
compatibilityVersion = "Xcode 6.3";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
@@ -346,6 +419,7 @@
|
||||
targets = (
|
||||
1CB8B3631BBBB78D000E2DD1 /* Background Music Device */,
|
||||
1C8034D91BDD073B00668E00 /* BGMDriverTests */,
|
||||
2743C9C51D7EF84B0089613B /* PublicUtility */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -373,6 +447,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1CD95B121E93AA5200EB8EF0 /* BGM_AbstractDevice.cpp in Sources */,
|
||||
1CD95B131E93AA5200EB8EF0 /* BGM_NullDevice.cpp in Sources */,
|
||||
1CD95B141E93AA5200EB8EF0 /* BGM_Stream.cpp in Sources */,
|
||||
27E6B5F01E01966A00EC0AAB /* BGM_Utils.cpp in Sources */,
|
||||
277170101CA0CFC300AB34B4 /* BGM_PlugInInterface.cpp in Sources */,
|
||||
277170111CA0CFC300AB34B4 /* CACFNumber.cpp in Sources */,
|
||||
27D643C31C9FBE1600737F6E /* BGM_XPCHelper.m in Sources */,
|
||||
@@ -405,28 +483,39 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1CC1DF8A1BE5703B00FB8FE4 /* CACFArray.cpp in Sources */,
|
||||
1CC1DF8B1BE5703B00FB8FE4 /* CACFDictionary.cpp in Sources */,
|
||||
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */,
|
||||
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */,
|
||||
1CB8B3951BBD2418000E2DD1 /* CADispatchQueue.cpp in Sources */,
|
||||
1CB8B37A1BBBDFA2000E2DD1 /* CADebugPrintf.cpp in Sources */,
|
||||
1C305D9D1BE294B5004EBB91 /* CACFNumber.cpp in Sources */,
|
||||
1CB8B3731BBBD8A4000E2DD1 /* CADebugMacros.cpp in Sources */,
|
||||
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */,
|
||||
1C3821111C4A18DE00A0C8C6 /* CAPThread.cpp in Sources */,
|
||||
1CB8B38E1BBCF4A9000E2DD1 /* CACFString.cpp in Sources */,
|
||||
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */,
|
||||
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */,
|
||||
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */,
|
||||
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */,
|
||||
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */,
|
||||
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */,
|
||||
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */,
|
||||
1CC1DF891BE558B000FB8FE4 /* CADebugger.cpp in Sources */,
|
||||
1CB8B3861BBCEFE8000E2DD1 /* CAMutex.cpp in Sources */,
|
||||
1CB8B3891BBCF08A000E2DD1 /* CAHostTimeBase.cpp in Sources */,
|
||||
1CB8B38F1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp in Sources */,
|
||||
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */,
|
||||
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */,
|
||||
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */,
|
||||
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2743C9C21D7EF84B0089613B /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2743C9CD1D7EF8760089613B /* CACFArray.cpp in Sources */,
|
||||
2743C9CF1D7EF8760089613B /* CACFDictionary.cpp in Sources */,
|
||||
2743C9D11D7EF8760089613B /* CACFNumber.cpp in Sources */,
|
||||
2743C9D31D7EF8760089613B /* CACFString.cpp in Sources */,
|
||||
2743C9D51D7EF8760089613B /* CADebugger.cpp in Sources */,
|
||||
2743C9D71D7EF8760089613B /* CADebugMacros.cpp in Sources */,
|
||||
2743C9D91D7EF8760089613B /* CADebugPrintf.cpp in Sources */,
|
||||
2743C9DB1D7EF8760089613B /* CADispatchQueue.cpp in Sources */,
|
||||
2743C9DE1D7EF8760089613B /* CAHostTimeBase.cpp in Sources */,
|
||||
2743C9E01D7EF8760089613B /* CAMutex.cpp in Sources */,
|
||||
2743C9E21D7EF8760089613B /* CAPThread.cpp in Sources */,
|
||||
2743C9E41D7EF8760089613B /* CAVolumeCurve.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -437,44 +526,21 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
INFOPLIST_FILE = BGMDriverTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGMDriverTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.DriverTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -482,52 +548,53 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
ENABLE_NS_ASSERTIONS = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
INFOPLIST_FILE = BGMDriverTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGMDriverTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.DriverTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
1CB8B35E1BBBB69C000E2DD1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -535,13 +602,22 @@
|
||||
"CoreAudio_Debug=1",
|
||||
"CoreAudio_UseSysLog=1",
|
||||
"CoreAudio_StopOnAssert=1",
|
||||
"CoreAudio_ThreadStampMessages=0",
|
||||
);
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
|
||||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
GCC_WARN_STRICT_SELECTOR_MATCH = YES;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
@@ -549,38 +625,68 @@
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1CB8B35F1BBBB69C000E2DD1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GCC_OPTIMIZATION_LEVEL = 3;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=0",
|
||||
"CoreAudio_Debug=0",
|
||||
"CoreAudio_UseSysLog=1",
|
||||
"CoreAudio_StopOnAssert=0",
|
||||
"CoreAudio_ThreadStampMessages=0",
|
||||
);
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
|
||||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
GCC_WARN_STRICT_SELECTOR_MATCH = YES;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -588,35 +694,26 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = "compiler-default";
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
INFOPLIST_FILE = BGMDriver/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.Driver;
|
||||
@@ -631,34 +728,26 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = "compiler-default";
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
INFOPLIST_FILE = BGMDriver/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.Driver;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -668,6 +757,57 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2743C9C81D7EF84B0089613B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_SUSPICIOUS_MOVES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
EXECUTABLE_PREFIX = lib;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2743C9C91D7EF84B0089613B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_SUSPICIOUS_MOVES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
EXECUTABLE_PREFIX = lib;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -698,6 +838,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
2743C9C71D7EF84B0089613B /* Build configuration list for PBXNativeTarget "PublicUtility" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2743C9C81D7EF84B0089613B /* Debug */,
|
||||
2743C9C91D7EF84B0089613B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 1CB8B35A1BBBB69C000E2DD1 /* Project object */;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0720"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2743C9C51D7EF84B0089613B"
|
||||
BuildableName = "libPublicUtility.a"
|
||||
BlueprintName = "PublicUtility"
|
||||
ReferencedContainer = "container:BGMDriver.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2743C9C51D7EF84B0089613B"
|
||||
BuildableName = "libPublicUtility.a"
|
||||
BlueprintName = "PublicUtility"
|
||||
ReferencedContainer = "container:BGMDriver.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2743C9C51D7EF84B0089613B"
|
||||
BuildableName = "libPublicUtility.a"
|
||||
BlueprintName = "PublicUtility"
|
||||
ReferencedContainer = "container:BGMDriver.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,409 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_AbstractDevice.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_AbstractDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAException.h"
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
BGM_AbstractDevice::BGM_AbstractDevice(AudioObjectID inObjectID, AudioObjectID inOwnerObjectID)
|
||||
:
|
||||
BGM_Object(inObjectID, kAudioDeviceClassID, kAudioObjectClassID, inOwnerObjectID)
|
||||
{
|
||||
}
|
||||
|
||||
BGM_AbstractDevice::~BGM_AbstractDevice()
|
||||
{
|
||||
}
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
bool BGM_AbstractDevice::HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioObjectPropertyName:
|
||||
case kAudioObjectPropertyManufacturer:
|
||||
case kAudioDevicePropertyDeviceUID:
|
||||
case kAudioDevicePropertyModelUID:
|
||||
case kAudioDevicePropertyTransportType:
|
||||
case kAudioDevicePropertyRelatedDevices:
|
||||
case kAudioDevicePropertyClockDomain:
|
||||
case kAudioDevicePropertyDeviceIsAlive:
|
||||
case kAudioDevicePropertyDeviceIsRunning:
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
|
||||
case kAudioDevicePropertyLatency:
|
||||
case kAudioDevicePropertyStreams:
|
||||
case kAudioObjectPropertyControlList:
|
||||
case kAudioDevicePropertySafetyOffset:
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
case kAudioDevicePropertyIsHidden:
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::HasProperty(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
bool BGM_AbstractDevice::IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioObjectPropertyName:
|
||||
case kAudioObjectPropertyManufacturer:
|
||||
case kAudioDevicePropertyDeviceUID:
|
||||
case kAudioDevicePropertyModelUID:
|
||||
case kAudioDevicePropertyTransportType:
|
||||
case kAudioDevicePropertyRelatedDevices:
|
||||
case kAudioDevicePropertyClockDomain:
|
||||
case kAudioDevicePropertyDeviceIsAlive:
|
||||
case kAudioDevicePropertyDeviceIsRunning:
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
|
||||
case kAudioDevicePropertyLatency:
|
||||
case kAudioDevicePropertyStreams:
|
||||
case kAudioObjectPropertyControlList:
|
||||
case kAudioDevicePropertySafetyOffset:
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
case kAudioDevicePropertyIsHidden:
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
theAnswer = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
UInt32 BGM_AbstractDevice::GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData) const
|
||||
{
|
||||
UInt32 theAnswer = 0;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioObjectPropertyName:
|
||||
theAnswer = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioObjectPropertyManufacturer:
|
||||
theAnswer = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceUID:
|
||||
theAnswer = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyModelUID:
|
||||
theAnswer = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyTransportType:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyRelatedDevices:
|
||||
theAnswer = sizeof(AudioObjectID);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyClockDomain:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceIsAlive:
|
||||
theAnswer = sizeof(AudioClassID);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceIsRunning:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyLatency:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyStreams:
|
||||
theAnswer = 0;
|
||||
break;
|
||||
|
||||
case kAudioObjectPropertyControlList:
|
||||
theAnswer = 0;
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertySafetyOffset:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
theAnswer = sizeof(Float64);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
theAnswer = 0;
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyIsHidden:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
void BGM_AbstractDevice::GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const
|
||||
{
|
||||
UInt32 theNumberItemsToFetch;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioObjectPropertyName:
|
||||
case kAudioObjectPropertyManufacturer:
|
||||
case kAudioDevicePropertyDeviceUID:
|
||||
case kAudioDevicePropertyModelUID:
|
||||
case kAudioDevicePropertyDeviceIsRunning:
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
// Crash debug builds if a concrete device delegates a required property that can't be
|
||||
// handled here or in BGM_Object (the parent of this class).
|
||||
//
|
||||
// See BGM_Device for info about these properties.
|
||||
//
|
||||
// TODO: Write a test that checks all required properties for each subclass.
|
||||
BGMAssert(false,
|
||||
"BGM_AbstractDevice::GetPropertyData: Property %u not handled in subclass",
|
||||
inAddress.mSelector);
|
||||
|
||||
|
||||
case kAudioDevicePropertyTransportType:
|
||||
// This value represents how the device is attached to the system. This can be
|
||||
// any 32 bit integer, but common values for this property are defined in
|
||||
// <CoreAudio/AudioHardwareBase.h>.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyTransportType for the device");
|
||||
// Default to virtual device.
|
||||
*reinterpret_cast<UInt32*>(outData) = kAudioDeviceTransportTypeVirtual;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyRelatedDevices:
|
||||
// The related devices property identifies device objects that are very closely
|
||||
// related. Generally, this is for relating devices that are packaged together
|
||||
// in the hardware such as when the input side and the output side of a piece
|
||||
// of hardware can be clocked separately and therefore need to be represented
|
||||
// as separate AudioDevice objects. In such case, both devices would report
|
||||
// that they are related to each other. Note that at minimum, a device is
|
||||
// related to itself, so this list will always be at least one item long.
|
||||
|
||||
// Calculate the number of items that have been requested. Note that this
|
||||
// number is allowed to be smaller than the actual size of the list. In such
|
||||
// case, only that number of items will be returned
|
||||
theNumberItemsToFetch = inDataSize / sizeof(AudioObjectID);
|
||||
|
||||
// Default to only have the one device.
|
||||
if(theNumberItemsToFetch > 1)
|
||||
{
|
||||
theNumberItemsToFetch = 1;
|
||||
}
|
||||
|
||||
// Write the devices' object IDs into the return value.
|
||||
if(theNumberItemsToFetch > 0)
|
||||
{
|
||||
reinterpret_cast<AudioObjectID*>(outData)[0] = GetObjectID();
|
||||
}
|
||||
|
||||
// Report how much we wrote.
|
||||
outDataSize = theNumberItemsToFetch * sizeof(AudioObjectID);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyClockDomain:
|
||||
// This property allows the device to declare what other devices it is
|
||||
// synchronized with in hardware. The way it works is that if two devices have
|
||||
// the same value for this property and the value is not zero, then the two
|
||||
// devices are synchronized in hardware. Note that a device that either can't
|
||||
// be synchronized with others or doesn't know should return 0 for this
|
||||
// property.
|
||||
//
|
||||
// Default to 0.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyClockDomain for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceIsAlive:
|
||||
// Default to alive.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceIsAlive for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 1;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
|
||||
// This property returns whether or not the device wants to be able to be the
|
||||
// default device for content. This is the device that iTunes and QuickTime
|
||||
// will use to play their content on and FaceTime will use as it's microphone.
|
||||
// Nearly all devices should allow for this.
|
||||
//
|
||||
// Default to true.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceCanBeDefaultDevice for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 1;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
|
||||
// This property returns whether or not the device wants to be the system
|
||||
// default device. This is the device that is used to play interface sounds and
|
||||
// other incidental or UI-related sounds on. Most devices should allow this
|
||||
// although devices with lots of latency may not want to.
|
||||
//
|
||||
// Default to true.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceCanBeDefaultSystemDevice for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 1;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyLatency:
|
||||
// This property returns the presentation latency of the device. Default to 0.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyLatency for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyStreams:
|
||||
// Default to not having any streams.
|
||||
outDataSize = 0;
|
||||
break;
|
||||
|
||||
case kAudioObjectPropertyControlList:
|
||||
// Default to not having any controls.
|
||||
outDataSize = 0;
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertySafetyOffset:
|
||||
// This property returns the how close to now the HAL can read and write. Default to 0.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertySafetyOffset for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyIsHidden:
|
||||
// This returns whether or not the device is visible to clients. Default to not hidden.
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyIsHidden for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Object::GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_AbstractDevice.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef BGM_Driver__BGM_AbstractDevice
|
||||
#define BGM_Driver__BGM_AbstractDevice
|
||||
|
||||
// SuperClass Includes
|
||||
#include "BGM_Object.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_AbstractDevice
|
||||
:
|
||||
public BGM_Object
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
protected:
|
||||
BGM_AbstractDevice(AudioObjectID inObjectID,
|
||||
AudioObjectID inOwnerObjectID);
|
||||
virtual ~BGM_AbstractDevice();
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
public:
|
||||
virtual bool HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual bool IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData) const;
|
||||
virtual void GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const;
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
public:
|
||||
virtual void StartIO(UInt32 inClientID) = 0;
|
||||
virtual void StopIO(UInt32 inClientID) = 0;
|
||||
|
||||
virtual void GetZeroTimeStamp(Float64& outSampleTime,
|
||||
UInt64& outHostTime,
|
||||
UInt64& outSeed) = 0;
|
||||
|
||||
virtual void WillDoIOOperation(UInt32 inOperationID,
|
||||
bool& outWillDo,
|
||||
bool& outWillDoInPlace) const = 0;
|
||||
virtual void BeginIOOperation(UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
UInt32 inClientID) = 0;
|
||||
virtual void DoIOOperation(AudioObjectID inStreamObjectID,
|
||||
UInt32 inClientID, UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
void* ioMainBuffer,
|
||||
void* __nullable ioSecondaryBuffer) = 0;
|
||||
virtual void EndIOOperation(UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
UInt32 inClientID) = 0;
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
public:
|
||||
virtual CFStringRef CopyDeviceUID() const = 0;
|
||||
virtual void AddClient(const AudioServerPlugInClientInfo* inClientInfo) = 0;
|
||||
virtual void RemoveClient(const AudioServerPlugInClientInfo* inClientInfo) = 0;
|
||||
virtual void PerformConfigChange(UInt64 inChangeAction,
|
||||
void* __nullable inChangeInfo) = 0;
|
||||
virtual void AbortConfigChange(UInt64 inChangeAction,
|
||||
void* __nullable inChangeInfo) = 0;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGM_Driver__BGM_AbstractDevice */
|
||||
|
||||
+509
-703
File diff suppressed because it is too large
Load Diff
@@ -17,24 +17,25 @@
|
||||
// BGM_Device.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
// Based largely on SA_Device.h from Apple's SimpleAudioPlugin sample code.
|
||||
// Based largely on SA_Device.h from Apple's SimpleAudioDriver Plug-In sample code.
|
||||
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
|
||||
//
|
||||
|
||||
#ifndef __BGMDriver__BGM_Device__
|
||||
#define __BGMDriver__BGM_Device__
|
||||
#ifndef BGMDriver__BGM_Device
|
||||
#define BGMDriver__BGM_Device
|
||||
|
||||
// SuperClass Includes
|
||||
#include "BGM_Object.h"
|
||||
#include "BGM_AbstractDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_WrappedAudioEngine.h"
|
||||
#include "BGM_Clients.h"
|
||||
#include "BGM_TaskQueue.h"
|
||||
#include "BGM_Stream.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAMutex.h"
|
||||
@@ -42,11 +43,12 @@
|
||||
|
||||
// System Includes
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <pthread.h>
|
||||
|
||||
|
||||
class BGM_Device
|
||||
:
|
||||
public BGM_Object
|
||||
public BGM_AbstractDevice
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
@@ -85,15 +87,6 @@ private:
|
||||
void Device_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
|
||||
void Device_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
|
||||
|
||||
#pragma mark Stream Property Operations
|
||||
|
||||
private:
|
||||
bool Stream_HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
|
||||
bool Stream_IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
|
||||
UInt32 Stream_GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData) const;
|
||||
void Stream_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
|
||||
void Stream_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
|
||||
|
||||
#pragma mark Control Property Operations
|
||||
|
||||
private:
|
||||
@@ -125,6 +118,50 @@ private:
|
||||
void UpdateAudibleStateSampleTimes_PostMix(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
|
||||
void UpdateDeviceAudibleState(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime);
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
public:
|
||||
/*!
|
||||
Enable or disable the device's volume and/or mute controls. This function is async because it
|
||||
has to ask the host to stop IO for the device before the controls can be enabled/disabled.
|
||||
|
||||
See BGM_Device::PerformConfigChange and RequestDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
*/
|
||||
void RequestEnabledControls(bool inVolumeEnabled, bool inMuteEnabled);
|
||||
|
||||
Float64 GetSampleRate() const;
|
||||
void RequestSampleRate(Float64 inRequestedSampleRate);
|
||||
|
||||
private:
|
||||
/*!
|
||||
Enable or disable the device's volume and/or mute controls.
|
||||
|
||||
Private because (after initialisation) this can only be called after asking the host to stop IO
|
||||
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
|
||||
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
*/
|
||||
void SetEnabledControls(bool inVolumeEnabled, bool inMuteEnabled);
|
||||
/*!
|
||||
Set the device's sample rate.
|
||||
|
||||
Private because (after initialisation) this can only be called after asking the host to stop IO
|
||||
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
|
||||
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
|
||||
@throws CAException if inNewSampleRate < 1 or if applying the sample rate to one of the streams
|
||||
fails.
|
||||
*/
|
||||
void SetSampleRate(Float64 inNewSampleRate);
|
||||
|
||||
/*! @return True if inObjectID is the ID of one of this device's streams. */
|
||||
bool IsStreamID(AudioObjectID inObjectID) const noexcept;
|
||||
/*!
|
||||
@return The stream that has the ID inObjectID and belongs to this device.
|
||||
@throws CAException if there is no such stream (i.e. if inObjectID is neither
|
||||
kObjectID_Stream_Input nor kObjectID_Stream_Output.)
|
||||
*/
|
||||
const BGM_Stream& GetStreamByID(AudioObjectID inObjectID) const;
|
||||
|
||||
#pragma mark Hardware Accessors
|
||||
|
||||
private:
|
||||
@@ -135,8 +172,8 @@ private:
|
||||
Float64 _HW_GetSampleRate() const;
|
||||
kern_return_t _HW_SetSampleRate(Float64 inNewSampleRate);
|
||||
UInt32 _HW_GetRingBufferFrameSize() const;
|
||||
SInt32 _HW_GetVolumeControlValue(int inObjectID) const;
|
||||
kern_return_t _HW_SetVolumeControlValue(int inObjectID, SInt32 inNewControlValue);
|
||||
SInt32 _HW_GetVolumeControlValue(AudioObjectID inObjectID) const;
|
||||
kern_return_t _HW_SetVolumeControlValue(AudioObjectID inObjectID, SInt32 inNewControlValue);
|
||||
UInt32 _HW_GetMuteControlValue(AudioObjectID inObjectID) const;
|
||||
kern_return_t _HW_SetMuteControlValue(AudioObjectID inObjectID, UInt32 inValue);
|
||||
|
||||
@@ -146,7 +183,12 @@ public:
|
||||
CFStringRef __nonnull CopyDeviceUID() const { return CFSTR(kBGMDeviceUID); }
|
||||
void AddClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
|
||||
void RemoveClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
|
||||
/*!
|
||||
Apply a change requested with BGM_PlugIn::Host_RequestDeviceConfigurationChange. See
|
||||
PerformDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
*/
|
||||
void PerformConfigChange(UInt64 inChangeAction, void* __nullable inChangeInfo);
|
||||
/*! Cancel a change requested with BGM_PlugIn::Host_RequestDeviceConfigurationChange. */
|
||||
void AbortConfigChange(UInt64 inChangeAction, void* __nullable inChangeInfo);
|
||||
|
||||
private:
|
||||
@@ -164,17 +206,16 @@ private:
|
||||
|
||||
kNumberOfStreams = 2,
|
||||
kNumberOfInputStreams = 1,
|
||||
kNumberOfOutputStreams = 1,
|
||||
|
||||
kNumberOfControls = 2
|
||||
kNumberOfOutputStreams = 1
|
||||
};
|
||||
|
||||
CAMutex mStateMutex;
|
||||
CAMutex mIOMutex;
|
||||
|
||||
UInt64 mSampleRateShadow; // Currently unused
|
||||
UInt64 __unused mSampleRateShadow; // Currently unused.
|
||||
const Float64 kSampleRateDefault = 44100.0;
|
||||
// Before we can change sample rate, the host has to stop the device. The new sample rate is stored here while it does.
|
||||
// Before we can change sample rate, the host has to stop the device. The new sample rate is
|
||||
// stored here while it does.
|
||||
Float64 mPendingSampleRate;
|
||||
|
||||
BGM_WrappedAudioEngine* __nullable mWrappedAudioEngine;
|
||||
@@ -193,8 +234,8 @@ private:
|
||||
UInt64 anchorHostTime = 0;
|
||||
} mLoopbackTime;
|
||||
|
||||
bool mInputStreamIsActive;
|
||||
bool mOutputStreamIsActive;
|
||||
BGM_Stream mInputStream;
|
||||
BGM_Stream mOutputStream;
|
||||
|
||||
SInt32 mDeviceAudibleState;
|
||||
struct
|
||||
@@ -204,13 +245,24 @@ private:
|
||||
Float64 latestAudibleMusic;
|
||||
Float64 latestSilentMusic;
|
||||
} mAudibleStateSampleTimes;
|
||||
|
||||
|
||||
enum class ChangeAction : UInt64
|
||||
{
|
||||
SetSampleRate,
|
||||
SetEnabledControls
|
||||
};
|
||||
|
||||
// This volume range will be used when the BGMDevice isn't wrapping another device (or we fail to
|
||||
// get the range of the wrapped device for some reason).
|
||||
#define kDefaultMinRawVolumeValue 0
|
||||
#define kDefaultMaxRawVolumeValue 96
|
||||
#define kDefaultMinDbVolumeValue -96.0f
|
||||
#define kDefaultMaxDbVolumeValue 0.0f
|
||||
|
||||
bool mOutputVolumeControlEnabled = true;
|
||||
bool mOutputMuteControlEnabled = true;
|
||||
bool mPendingOutputVolumeControlEnabled = true;
|
||||
bool mPendingOutputMuteControlEnabled = true;
|
||||
|
||||
SInt32 mOutputMasterVolumeControlRawValueShadow;
|
||||
SInt32 mOutputMasterMinRawVolumeShadow;
|
||||
@@ -222,5 +274,5 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif /* __BGMDriver__BGM_Device__ */
|
||||
#endif /* BGMDriver__BGM_Device */
|
||||
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_NullDevice.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_NullDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_PlugIn.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAException.h"
|
||||
#include "CAPropertyAddress.h"
|
||||
#include "CADispatchQueue.h"
|
||||
|
||||
// System Includes
|
||||
#include <mach/mach_time.h> // For mach_absolute_time
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static const Float64 kSampleRate = 44100.0;
|
||||
static const UInt32 kZeroTimeStampPeriod = 10000; // Arbitrary.
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
pthread_once_t BGM_NullDevice::sStaticInitializer = PTHREAD_ONCE_INIT;
|
||||
BGM_NullDevice* BGM_NullDevice::sInstance = nullptr;
|
||||
|
||||
BGM_NullDevice& BGM_NullDevice::GetInstance()
|
||||
{
|
||||
pthread_once(&sStaticInitializer, StaticInitializer);
|
||||
return *sInstance;
|
||||
}
|
||||
|
||||
void BGM_NullDevice::StaticInitializer()
|
||||
{
|
||||
try
|
||||
{
|
||||
sInstance = new BGM_NullDevice;
|
||||
// Note that we leave the device inactive initially. BGMApp will activate it when needed.
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
DebugMsg("BGM_NullDevice::StaticInitializer: Failed to create the device");
|
||||
delete sInstance;
|
||||
sInstance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
BGM_NullDevice::BGM_NullDevice()
|
||||
:
|
||||
BGM_AbstractDevice(kObjectID_Device_Null, kAudioObjectPlugInObject),
|
||||
mStateMutex("Null Device State"),
|
||||
mIOMutex("Null Device IO"),
|
||||
mStream(kObjectID_Stream_Null, kObjectID_Device_Null, false, kSampleRate)
|
||||
{
|
||||
}
|
||||
|
||||
BGM_NullDevice::~BGM_NullDevice()
|
||||
{
|
||||
}
|
||||
|
||||
void BGM_NullDevice::Activate()
|
||||
{
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
if(!IsActive())
|
||||
{
|
||||
// Call the super-class, which just marks the object as active.
|
||||
BGM_AbstractDevice::Activate();
|
||||
|
||||
// Calculate the host ticks per frame for the clock.
|
||||
struct mach_timebase_info theTimeBaseInfo;
|
||||
mach_timebase_info(&theTimeBaseInfo);
|
||||
Float64 theHostClockFrequency = theTimeBaseInfo.denom / theTimeBaseInfo.numer;
|
||||
theHostClockFrequency *= 1000000000.0;
|
||||
mHostTicksPerFrame = theHostClockFrequency / kSampleRate;
|
||||
|
||||
SendDeviceIsAlivePropertyNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
void BGM_NullDevice::Deactivate()
|
||||
{
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
if(IsActive())
|
||||
{
|
||||
CAMutex::Locker theIOLocker(mIOMutex);
|
||||
|
||||
// Mark the object inactive by calling the super-class.
|
||||
BGM_AbstractDevice::Deactivate();
|
||||
|
||||
SendDeviceIsAlivePropertyNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
void BGM_NullDevice::SendDeviceIsAlivePropertyNotifications()
|
||||
{
|
||||
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
|
||||
AudioObjectPropertyAddress theChangedProperties[] = {
|
||||
CAPropertyAddress(kAudioDevicePropertyDeviceIsAlive)
|
||||
};
|
||||
|
||||
BGM_PlugIn::Host_PropertiesChanged(GetObjectID(), 1, theChangedProperties);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
bool BGM_NullDevice::HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
if(inObjectID == mStream.GetObjectID())
|
||||
{
|
||||
return mStream.HasProperty(inObjectID, inClientPID, inAddress);
|
||||
}
|
||||
|
||||
bool theAnswer = false;
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
|
||||
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_AbstractDevice::HasProperty(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
bool BGM_NullDevice::IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
// Forward stream properties.
|
||||
if(inObjectID == mStream.GetObjectID())
|
||||
{
|
||||
return mStream.IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
}
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
default:
|
||||
theAnswer = BGM_AbstractDevice::IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
UInt32 BGM_NullDevice::GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData) const
|
||||
{
|
||||
// Forward stream properties.
|
||||
if(inObjectID == mStream.GetObjectID())
|
||||
{
|
||||
return mStream.GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
}
|
||||
|
||||
UInt32 theAnswer = 0;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioDevicePropertyStreams:
|
||||
theAnswer = 1 * sizeof(AudioObjectID);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
theAnswer = 1 * sizeof(AudioValueRange);
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_AbstractDevice::GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
void BGM_NullDevice::GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const
|
||||
{
|
||||
// Forward stream properties.
|
||||
if(inObjectID == mStream.GetObjectID())
|
||||
{
|
||||
return mStream.GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
}
|
||||
|
||||
// See BGM_Device::Device_GetPropertyData for more information about these properties.
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioObjectPropertyName:
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectID),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioObjectPropertyName for the device");
|
||||
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kNullDeviceName);
|
||||
outDataSize = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioObjectPropertyManufacturer:
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectID),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioObjectPropertyManufacturer for the device");
|
||||
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kNullDeviceManufacturerName);
|
||||
outDataSize = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceUID:
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectID),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceUID for the device");
|
||||
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kBGMNullDeviceUID);
|
||||
outDataSize = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyModelUID:
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectID),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyModelUID for the device");
|
||||
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kBGMNullDeviceModelUID);
|
||||
outDataSize = sizeof(CFStringRef);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceIsAlive:
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceIsAlive for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = IsActive() ? 1 : 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyDeviceIsRunning:
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyDeviceIsRunning for the device");
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
// 1 means the device is running, i.e. doing IO.
|
||||
*reinterpret_cast<UInt32*>(outData) = (mClientsDoingIO > 0) ? 1 : 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyStreams:
|
||||
if(inDataSize >= sizeof(AudioObjectID) &&
|
||||
(inAddress.mScope == kAudioObjectPropertyScopeGlobal ||
|
||||
inAddress.mScope == kAudioObjectPropertyScopeOutput))
|
||||
{
|
||||
// Return the ID of this device's stream.
|
||||
reinterpret_cast<AudioObjectID*>(outData)[0] = kObjectID_Stream_Null;
|
||||
// Report how much we wrote.
|
||||
outDataSize = 1 * sizeof(AudioObjectID);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return nothing if we don't have a stream of the given scope or there's no room
|
||||
// for the response.
|
||||
outDataSize = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
ThrowIf(inDataSize < sizeof(Float64),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyNominalSampleRate for the device");
|
||||
*reinterpret_cast<Float64*>(outData) = kSampleRate;
|
||||
outDataSize = sizeof(Float64);
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
// Check we were given space to return something.
|
||||
if((inDataSize / sizeof(AudioValueRange)) >= 1)
|
||||
{
|
||||
// This device doesn't support changing the sample rate.
|
||||
reinterpret_cast<AudioValueRange*>(outData)[0].mMinimum = kSampleRate;
|
||||
reinterpret_cast<AudioValueRange*>(outData)[0].mMaximum = kSampleRate;
|
||||
outDataSize = sizeof(AudioValueRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
outDataSize = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioDevicePropertyZeroTimeStampPeriod for the device");
|
||||
*reinterpret_cast<UInt32*>(outData) = kZeroTimeStampPeriod;
|
||||
outDataSize = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_AbstractDevice::GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void BGM_NullDevice::SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData)
|
||||
{
|
||||
// This device doesn't have any settable properties, so just pass stream properties along.
|
||||
if(inObjectID == mStream.GetObjectID())
|
||||
{
|
||||
mStream.SetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
inData);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
|
||||
void BGM_NullDevice::StartIO(UInt32 inClientID)
|
||||
{
|
||||
#pragma unused (inClientID)
|
||||
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
if(mClientsDoingIO == 0)
|
||||
{
|
||||
// Reset the clock.
|
||||
mNumberTimeStamps = 0;
|
||||
mAnchorHostTime = mach_absolute_time();
|
||||
|
||||
// Send notifications.
|
||||
DebugMsg("BGM_NullDevice::StartIO: Sending kAudioDevicePropertyDeviceIsRunning");
|
||||
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
|
||||
AudioObjectPropertyAddress theChangedProperty[] = {
|
||||
CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning)
|
||||
};
|
||||
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device_Null, 1, theChangedProperty);
|
||||
});
|
||||
}
|
||||
|
||||
mClientsDoingIO++;
|
||||
}
|
||||
|
||||
void BGM_NullDevice::StopIO(UInt32 inClientID)
|
||||
{
|
||||
#pragma unused (inClientID)
|
||||
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
ThrowIf(mClientsDoingIO == 0,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_NullDevice::StopIO: Underflowed mClientsDoingIO");
|
||||
|
||||
mClientsDoingIO--;
|
||||
|
||||
if(mClientsDoingIO == 0)
|
||||
{
|
||||
// Send notifications.
|
||||
DebugMsg("BGM_NullDevice::StopIO: Sending kAudioDevicePropertyDeviceIsRunning");
|
||||
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
|
||||
AudioObjectPropertyAddress theChangedProperty[] = {
|
||||
CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning)
|
||||
};
|
||||
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device_Null, 1, theChangedProperty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void BGM_NullDevice::GetZeroTimeStamp(Float64& outSampleTime,
|
||||
UInt64& outHostTime,
|
||||
UInt64& outSeed)
|
||||
{
|
||||
CAMutex::Locker theIOLocker(mIOMutex);
|
||||
|
||||
// Not sure whether there's actually any point to implementing this. The documentation says that
|
||||
// clockless devices don't need to, but if the device doesn't have
|
||||
// kAudioDevicePropertyZeroTimeStampPeriod the HAL seems to reject it. So we give it a simple
|
||||
// clock similar to the loopback clock in BGM_Device.
|
||||
UInt64 theCurrentHostTime = mach_absolute_time();
|
||||
|
||||
// Calculate the next host time.
|
||||
Float64 theHostTicksPerPeriod = mHostTicksPerFrame * static_cast<Float64>(kZeroTimeStampPeriod);
|
||||
Float64 theHostTickOffset = static_cast<Float64>(mNumberTimeStamps + 1) * theHostTicksPerPeriod;
|
||||
UInt64 theNextHostTime = mAnchorHostTime + static_cast<UInt64>(theHostTickOffset);
|
||||
|
||||
// Go to the next period if the next host time is less than the current time.
|
||||
if(theNextHostTime <= theCurrentHostTime)
|
||||
{
|
||||
mNumberTimeStamps++;
|
||||
}
|
||||
|
||||
Float64 theHostTicksSinceAnchor =
|
||||
(static_cast<Float64>(mNumberTimeStamps) * theHostTicksPerPeriod);
|
||||
|
||||
// Set the return values.
|
||||
outSampleTime = mNumberTimeStamps * kZeroTimeStampPeriod;
|
||||
outHostTime = static_cast<UInt64>(mAnchorHostTime + theHostTicksSinceAnchor);
|
||||
outSeed = 1;
|
||||
}
|
||||
|
||||
void BGM_NullDevice::WillDoIOOperation(UInt32 inOperationID,
|
||||
bool& outWillDo,
|
||||
bool& outWillDoInPlace) const
|
||||
{
|
||||
switch(inOperationID)
|
||||
{
|
||||
case kAudioServerPlugInIOOperationWriteMix:
|
||||
outWillDo = true;
|
||||
outWillDoInPlace = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
outWillDo = false;
|
||||
outWillDoInPlace = true;
|
||||
break;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
void BGM_NullDevice::DoIOOperation(AudioObjectID inStreamObjectID,
|
||||
UInt32 inClientID,
|
||||
UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
void* ioMainBuffer,
|
||||
void* __nullable ioSecondaryBuffer)
|
||||
{
|
||||
#pragma unused (inStreamObjectID, inClientID, inOperationID, inIOCycleInfo, inIOBufferFrameSize)
|
||||
#pragma unused (ioMainBuffer, ioSecondaryBuffer)
|
||||
// Ignore the audio data.
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_NullDevice.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// A device with one output stream that ignores any audio played on that stream.
|
||||
//
|
||||
// If we change BGMDevice's controls list, to match the output device set in BGMApp, we need to
|
||||
// change the OS X default device so other programs (including the OS X audio UI) will update
|
||||
// themselves. We could just change to the real output device and change back, but that could have
|
||||
// side effects the user wouldn't expect. For example, an app the user had muted might be unmuted
|
||||
// for a short period.
|
||||
//
|
||||
// Instead, BGMApp temporarily enables this device and uses it to toggle the default device. This
|
||||
// device is disabled at all other times so it can be hidden from the user. (We can't just use
|
||||
// kAudioDevicePropertyIsHidden because hidden devices can't be default and the HAL doesn't seem to
|
||||
// let devices change kAudioDevicePropertyIsHidden after setting it initially.)
|
||||
//
|
||||
// It might be worth eventually having a virtual device for each real output device, but this is
|
||||
// simpler and seems to work well enough for now.
|
||||
//
|
||||
|
||||
#ifndef BGMDriver__BGM_NullDevice
|
||||
#define BGMDriver__BGM_NullDevice
|
||||
|
||||
// SuperClass Includes
|
||||
#include "BGM_AbstractDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Stream.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAMutex.h"
|
||||
|
||||
// System Includes
|
||||
#include <pthread.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_NullDevice
|
||||
:
|
||||
public BGM_AbstractDevice
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
static BGM_NullDevice& GetInstance();
|
||||
|
||||
private:
|
||||
static void StaticInitializer();
|
||||
|
||||
protected:
|
||||
BGM_NullDevice();
|
||||
virtual ~BGM_NullDevice();
|
||||
|
||||
public:
|
||||
virtual void Activate();
|
||||
virtual void Deactivate();
|
||||
|
||||
private:
|
||||
void SendDeviceIsAlivePropertyNotifications();
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
public:
|
||||
bool HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
bool IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData) const;
|
||||
void GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* __nullable inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const;
|
||||
void SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData);
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
public:
|
||||
void StartIO(UInt32 inClientID);
|
||||
void StopIO(UInt32 inClientID);
|
||||
|
||||
void GetZeroTimeStamp(Float64& outSampleTime,
|
||||
UInt64& outHostTime,
|
||||
UInt64& outSeed);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
void WillDoIOOperation(UInt32 inOperationID,
|
||||
bool& outWillDo,
|
||||
bool& outWillDoInPlace) const;
|
||||
void BeginIOOperation(UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
UInt32 inClientID)
|
||||
{ /* No-op */ };
|
||||
void DoIOOperation(AudioObjectID inStreamObjectID,
|
||||
UInt32 inClientID,
|
||||
UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
void* ioMainBuffer,
|
||||
void* __nullable ioSecondaryBuffer);
|
||||
void EndIOOperation(UInt32 inOperationID,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
|
||||
UInt32 inClientID)
|
||||
{ /* No-op */ };
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
public:
|
||||
CFStringRef CopyDeviceUID() const
|
||||
{ return CFSTR(kBGMNullDeviceUID); };
|
||||
|
||||
void AddClient(const AudioServerPlugInClientInfo* inClientInfo)
|
||||
{ /* No-op */ };
|
||||
void RemoveClient(const AudioServerPlugInClientInfo* inClientInfo)
|
||||
{ /* No-op */ };
|
||||
void PerformConfigChange(UInt64 inChangeAction,
|
||||
void* __nullable inChangeInfo)
|
||||
{ /* No-op */ };
|
||||
void AbortConfigChange(UInt64 inChangeAction,
|
||||
void* __nullable inChangeInfo)
|
||||
{ /* No-op */ };
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
private:
|
||||
static pthread_once_t sStaticInitializer;
|
||||
static BGM_NullDevice* sInstance;
|
||||
|
||||
#define kNullDeviceName "Background Music Null Device"
|
||||
#define kNullDeviceManufacturerName \
|
||||
"Background Music contributors"
|
||||
|
||||
CAMutex mStateMutex;
|
||||
CAMutex mIOMutex;
|
||||
|
||||
BGM_Stream mStream;
|
||||
|
||||
UInt32 mClientsDoingIO = 0;
|
||||
|
||||
Float64 mHostTicksPerFrame = 0.0;
|
||||
UInt64 mNumberTimeStamps = 0;
|
||||
UInt64 mAnchorHostTime = 0;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMDriver__BGM_NullDevice */
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
// Based largely on SA_Object.cpp from Apple's SimpleAudioPlugin sample code.
|
||||
// Based largely on SA_Object.cpp from Apple's SimpleAudioDriver Plug-In sample code.
|
||||
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
|
||||
//
|
||||
// Similarly to BGM_Object.h, this file hasn't been changed much from SA_Object.cpp, except to
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
// Based largely on SA_Object.h from Apple's SimpleAudioPlugin sample code.
|
||||
// Based largely on SA_Object.h from Apple's SimpleAudioDriver Plug-In sample code.
|
||||
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
|
||||
//
|
||||
// The base class for our classes that represent audio objects. (See AudioServerPlugin.h for a
|
||||
// The base class for our classes that represent audio objects. (See AudioServerPlugIn.h for a
|
||||
// quick explanation of audio objects.)
|
||||
//
|
||||
// This is a stripped down version of SA_Object.h. Unlike the sample code plugin that SA_Object.h
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user