Compare commits

...

110 Commits

Author SHA1 Message Date
Kyle Neideck d322c3ac9b Never show warning dialog box in command-line pkg installs.
Also,
 - add Background Music Device.driver to the array of bundles in
   pkgbuild.plist, and
 - don't warn about the permissions of the install dir for BGMXPCHelper
   in build_and_install.sh if it's only building.
2017-06-13 23:18:10 +10:00
Kyle Neideck 45519a4d52 Fix BGMDriver's version number. 2017-06-12 17:22:34 +10:00
Kyle Neideck ed2f356570 Try to fix Travis not finding the files to release.
Also, make sure commands in uninstall.sh use the binaries that ship with
OS X and add the release files to .gitignore.
2017-06-12 14:38:26 +10:00
Scott Humphries 4d80e1f38f Fix deploy script 2017-06-12 14:04:50 +10:00
Kyle Neideck ca56d8d0b2 travis setup releases 2017-06-12 13:52:59 +10:00
Kyle Neideck 5e4556b49d Add packaging script and (possibly) support for OS X 10.9. 2017-06-11 19:19:31 +10:00
Kyle Neideck 8d1adf25bb BGM_Device: Fix over-releasing custom property data.
BGM_Device::Device_SetPropertyData was releasing the CFArray it gets
from the host (i.e. coreaudiod) when BGMApp sets
kAudioDeviceCustomPropertyEnabledOutputControls, which would deallocate
it, but coreaudiod also releases that CFArray.

Found with AddressSanitizer.
2017-06-04 23:31:56 +10:00
Kyle Neideck c33941a22b Disable -Wpartial-availability in BGMDriver tests. 2017-06-04 01:31:31 +10:00
Kyle Neideck ec7128495f Skip setting NSMenuItem.accessibilityTitle on OS X < 10.12.
This should also fix compilation with the 10.11 SDK.

Also enabled -Wpartial-availability and raised the deployment target to
OS X 10.9.
2017-06-03 23:17:34 +10:00
Kyle Neideck 6fc46c7943 uninstall.sh: Increase the filesize limit for deletion.
As a safety check, uninstall.sh refuses to delete a file if it's over a
certain size. With debug symbols, Background Music.app was just over the
previous 5MB limit.
2017-06-01 23:54:36 +10:00
Kyle Neideck b986b687ea Fix exception in Mock_CAHALAudioObject during BGMApp unit tests.
Also, avoid initialising BGMDeviceControlsList in
BGMMockAudioDeviceManager::init.
2017-06-01 20:10:35 +10:00
Kyle Neideck c617d98f9d BGMDevice: Only enable volume/mute if the output device also has them.
BGMApp now disables BGMDevice's volume and/or mute controls if the
output device selected in BGMApp doesn't have matching controls. This
prevents the controls from being presented to the user when they don't
do anything.

In BGMPlayThrough, wait much longer for our IOProcs to stop themselves
before assuming something's gone wrong. In testing, rapidly changing
between output devices with and without controls while playing audio
would occasionally cause one of the IOProcs to take too long to stop
itself.

Also adds some basic scriptability, mainly so UI tests can use
AppleScript to check BGMApp's state that would be complicated to check
otherwise. (In this case, to check which output device is selected.)

Fixes #101.
2017-05-30 23:22:48 +10:00
Kyle Neideck 4839ea8a4b Remove Xcode 6 from the Travis build matrix. 2017-05-30 23:22:48 +10:00
Kyle Neideck 612e249e1b Fix BGMApp crash when BGMDriver isn't installed. 2017-05-07 16:02:50 +10:00
Kyle Neideck d4df6107bd Add "Sound Control" to Related Projects in README.md 2017-05-07 15:17:48 +10:00
Kyle Neideck f5628e78a9 Fix some bugs in GitHub's rendering of README.md.
Not sure why these parts stopped rendering correctly on GitHub. Might be on their end.
2017-04-09 18:25:27 +10:00
Kyle Neideck 728a3a7331 build_and_install.sh: Offer fix when xcodebuild can't find Xcode.app.
If the Xcode command line tools were set to use a "command line tools
instance", which can be installed without having Xcode installed,
build_and_install.sh would fail. It prints an error message with a
command that can fix it if you do have Xcode installed, but the message
was kind of confusing and the command would fail if you didn't run it as
root.

build_and_install.sh now offers to run the command for you and then
continues the installation. I've also tried to make the message a bit
clearer and cleaned up some of the code.

Also fixes another bug that occurred with this configuration problem,
where the error from xcodebuild would be printed at an unintended (and
confusing) point in the script.

Fixes #108.
2017-04-09 17:35:27 +10:00
Kyle Neideck f254e8c58d uninstall.sh: Open System Preferences pane by ID instead of by name.
The Applescript that opened System Preferences at the end of the process
was failing to find the "Sound" pane. It might have been because I don't
have OS X set to English, but it's always worked for me in the past.
Either way, it's less fragile to use the ID and it fixes the problem (on
my machine, at least).

Also, added a short pause before restarting coreaudiod because the step
before that makes Finder to play a short sound. It probably wouldn't
cause any problems, but why risk it?
2017-04-09 15:44:46 +10:00
Kyle Neideck 5f9487deb0 Add PublicUtility to manual build instructions and...
...make them less likely to fail because of permissions errors.
2017-04-09 15:40:25 +10:00
Kyle Neideck 6a26afe47a Fix nullability warning in BGMXPCHelper. Also add Xcode 8.3 to .travis.yml.
Fixes #107.
2017-04-05 22:15:54 +10:00
Kyle Neideck 78e2813af1 Fix encoding issue on Travis caused by "©" char in Python script. 2017-02-19 20:50:11 +11:00
Kyle Neideck 87af15d290 Skip the UI tests on Travis by directly editing BGMApp's Xcode scheme.
Skipping them by overriding runTest didn't work and this is the only other way
I can think of. xcodebuild's -skip-testing option would work, but only with
recent versions of Xcode.
2017-02-19 20:25:54 +11:00
Kyle Neideck 7b32b6ef66 Skip the UI tests on Travis because it doesn't support UI testing. 2017-02-19 17:43:45 +11:00
Kyle Neideck 32723ff04b Append the git HEAD short ID to the build version for snapshot builds. 2017-02-19 15:14:34 +11:00
Kyle Neideck 60e1b3564b Add a UI tests target for BGMApp. Only has one test so far.
The UI tests run with clean user defaults, but BGMDevice and Scripting Bridge
still need to be mocked/stubbed out. That also means that the UI tests can only
run if BGMDriver is installed and that changes to BGMDriver's state made during
the tests will persist.
2017-02-19 13:39:34 +11:00
Kyle Neideck d49ff20820 Add builds with Xcode 8.1 and 6.4 to .travis.yml.
6.4 should already have been retired according to their documentation, so it
will probably need to be removed. But it would be nice to have because it's the
only OSX 10.10 image.
2017-02-19 12:55:20 +11:00
Kyle Neideck 9b5d5bf921 Remove BGMAppTests, which was essentially empty. 2017-02-18 18:59:32 +11:00
Kyle Neideck 07c1c2320b Reorganise the BGMApp test dirs slightly. 2017-02-18 18:12:40 +11:00
Kyle Neideck 523ad02761 Add -w option to build_and_install.sh, which passes -Wno-error to the compiler.
-Wno-error tells the compiler not to treat warnings as errors.

Using the option in the one-liner install command in README.md, since it's
mostly used by users rather than developers.

Also, log the options passed to build_and_install.sh in build_and_install.log.
2017-02-16 22:54:38 +11:00
Kyle Neideck 3ee563e4c5 Fix Travis again.
It seemed to think commands starting with ! were tags.
2017-02-16 00:26:04 +11:00
Kyle Neideck e5c406da16 Fix ls command in .travis.yml failing the macOS 10.12 build.
Also, add some simple tests for build_and_install.sh and uninstall.sh to
.travis.yml.
2017-02-15 23:53:14 +11:00
Kyle Neideck b5cf6de2ac Update OSX image versions in .travis.yml. Add some files to the Xcode project. 2017-02-15 22:53:02 +11:00
Kyle Neideck 8257f49b46 Merge pull request #98 from rakslice/pan 2017-02-14 23:32:21 +11:00
Kyle Neideck cdea147010 Move the pan sliders into an "extra controls" section of the menu items.
Also add centre tick marks and "L"/"R" (left/right) labels to them.

The idea is to eventually include extra controls like an equalizer, recording
apps, hiding/ignoring apps, routing apps, etc.

Also, remove the left margin from the App Volumes menu items. Even macOS isn't
consistent about including that margin, as far as I can tell.
2017-02-11 16:47:52 +11:00
Kyle Neideck a91615fc5e Fix a deadlock when changing output device while IO is running.
BGM_Device::StartIO blocks on
BGMAudioDeviceManager::waitForOutputDeviceToStart, which could be blocked by
HAL requests that the HAL wouldn't return until BGM_Device::StartIO returned.

Also:
 - Replace BGMPlayThrough's move constructor with a SetDevices function for
   simplicity.
 - Pause/abort debug builds if an error is logged.
2017-02-01 09:09:00 +11:00
Kyle Neideck 467b072a9d Don't throw in BGMPlayThrough::DestroyIOProcIDs if the device has been removed.
Also, default to only aborting debug builds when they log and swallow an
exception if the exception was unexpected. That is, the developer didn't
realise the code could throw.
2017-01-27 00:33:34 +11:00
Kyle Neideck a62fae6fd1 Merge branch 'pan' of https://github.com/rakslice/BackgroundMusic into rakslice-pan 2017-01-18 21:49:20 +11:00
Kyle Neideck 129c21a180 Add BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS preprocessor flag. Also, add...
an option to build_and_install.sh for passing extra options to xcodebuild.
2017-01-16 23:58:19 +11:00
Kyle Neideck 2d135838fa Add more error handling and logging to BGMPlayThrough. 2017-01-16 23:54:09 +11:00
Andrew Tonner acf3976b9b remove suprious setCellClass call; cleanup 2017-01-09 14:58:05 -08:00
rakslice 61ce9ef165 better custom slider hack 2017-01-02 04:21:56 -08:00
rakslice f6ac51a334 fixed typo 2017-01-02 02:43:40 -08:00
rakslice 94c594b342 added per-app pan sliders 2017-01-02 02:30:00 -08:00
Kyle Neideck d7e3980af8 Warn if uninstall.sh is run as root.
Also, add some fallback commands so it will work correctly, more or less, when
run as root in case someone ever wants to do that.

Fixes #95.
2016-12-31 21:56:00 +11:00
Kyle Neideck 2cb572ecf4 Revert "Fix default icon shown for BGMApp bundle."
This reverts commit a4160d370d.

I've tested the icon bug on a couple of other machines now and haven't been
able to reproduce it. My "fix" did cause the bug on those machines, though,
despite fixing it on mine. Still not sure exactly what's going on, but it seems
to be a problem with my development environment.
2016-12-31 21:05:04 +11:00
Kyle Neideck 8d5ea9c67c Auto-pause: make the unpause delay proportional to the pause duration.
We only unpause the music player, after auto-pausing it, if it's been
paused for longer than some minimum length of time. This commit reduces
that time if the music player hasn't been paused for long.
2016-12-29 02:25:44 +11:00
Kyle Neideck 651b91aeee Fix a link in the Install section of README.md. 2016-12-28 05:04:54 +11:00
Kyle Neideck 12840cbf31 Superficial fixes in the Install section of README.md. 2016-12-28 05:00:52 +11:00
Kyle Neideck f15708461d Add some info about logging to CONTRIBUTING.md. (Also, some clean up.) 2016-12-28 04:43:43 +11:00
Kyle Neideck a4160d370d Fix default icon shown for BGMApp bundle.
I'm not sure why this is necessary or why it works. Probably an Xcode
bug or something. This is with Xcode 8.2.1 on macOS 10.12.3 Beta
(16D17a).
2016-12-26 21:46:30 +11:00
Kyle Neideck 847313a174 Log debug messages to syslog and include debug symbols in BGMApp bundle.
In BGMApp, messages logged with the DebugMsg macro now go to syslog
instead of stdout.

People running standalone BGMApp debug builds (i.e. not in Xcode) should
be able the find the debug logs more easily. Xcode still shows the debug
logs normally when running BGMApp in Xcode.

Also, debug symbols (the .dSYM directory) are now included in the
Background Music.app bundle. (In both debug and release builds.)
CrashReporter is able to find these and use them to symbolicate BGMApp
crash logs.
2016-12-26 21:27:41 +11:00
Kyle Neideck 7992a5708c Use data source names instead of device names in the output device menu.
The names of the data source(s) for a device are generally the names
intended to be shown to the user, since the OS X volume menu, System
Preferences, etc. use them.

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

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

Resolves #59.
2016-12-23 01:46:27 +11:00
Kyle Neideck 31b501e832 Make BGMPlayThrough::WaitForOutputDeviceToStart noexcept. 2016-12-16 21:54:08 +11:00
Kyle Neideck ab9d4cdc2b Add more exception handling to BGMApp...
And other reliability improvements. Mostly in BGMPlayThrough and the
classes that use it. Trying to catch C++ exceptions as early as possible
in the Objective-C++ code and, if necessary, convert them to NSErrors.

More errors are logged in release builds now, which will hopefully help
with debugging issues the developers can't reproduce themselves.
2016-12-15 03:20:07 +11:00
Kyle Neideck e0acb34f29 Assume non-null in BGMDeviceControlSync.cpp. 2016-11-14 22:13:46 +11:00
Kyle Neideck ec87adb6e9 Allow MainMenu.xib to open in Xcode 7. Mostly to fix the tests in 7. 2016-11-14 21:58:23 +11:00
Kyle Neideck 810b2ed462 Fix BGMApp unit tests. 2016-11-13 01:32:26 +11:00
Kyle Neideck 035faa615f Fix link in README. 2016-11-13 00:28:26 +11:00
Kyle Neideck c8f6790274 Add a link to the about panel and refactor its code.
Link to the project website (GitHub) in the About Background Music
window, and move its code into its own class.

Also, update the copyright notices in the UI and README.
2016-11-13 00:06:58 +11:00
Kyle Neideck b0bfebedc4 Add Hermes music player. (A Pandora client.)
Closes #83.
2016-11-12 20:54:04 +11:00
Kyle Neideck da74e5ea1d Check that the user's accepted the Xcode license before installing.
If they haven't, xcodebuild would fail.
2016-10-08 19:33:38 +11:00
Kyle Neideck a5fb68ad2b Fix permissions error in Travis build. 2016-10-07 07:41:08 +11:00
Kyle Neideck 61a9d89ef9 Do Travis builds on a case-sensitive .dmg to catch failures.
Compiling on case-sensitive filesystems has been broken a couple of
times, so hopefully this will let us catch some of those bugs early.
2016-10-06 19:20:17 +11:00
Kyle Neideck b2a12b3e37 Change "Vox" to "VOX" in filenames.
Fixes #77: build fails on case-sensitive filesystems.
2016-10-05 22:07:58 +11:00
Kyle Neideck d38ea256cd Don't quit BGMApp if BGMXPCHelper is missing. See #76. 2016-09-26 11:08:19 +10:00
Kyle Neideck e31f2b1c29 Refactor and clean up BGMApp's music player code and add tests. Also...
- Destroy the Scripting Bridge application object for a music player
  when that music player isn't running.
- Move the UI code for the auto-pause menu item into its own class.
- Add a User Defaults class to BGMApp.
- Enable some more warnings for the BGMApp project.
2016-09-17 18:24:19 +10:00
Kyle Neideck 59aa04c9bc Use dot notation for properties in BGMAppVolumes.mm 2016-09-11 15:33:23 +10:00
Kyle Neideck cbbd48dcee Show Finder in the app volumes menu. Fixes #45. 2016-09-08 00:25:03 +10:00
Kyle Neideck c91af08d54 Enable more warnings in BGMDriver.
The PublicUtility classes are now built as a separate target so we can
disable some of those warnings for it.
2016-09-08 00:24:57 +10:00
Kyle Neideck 484ffa16f3 BGMDriver: Clean up constructor initializer lists.
See https://isocpp.org/wiki/faq/ctors#ctor-initializer-order.
2016-09-06 22:18:11 +10:00
Kyle Neideck 4e091c7398 Merge pull request #73 from Qix-/patch-1
Short circuit audible loop
2016-09-04 00:56:51 +10:00
Josh Junon 808fe1b6b3 Short circuit audible loop
Slight micro-opt that will short circuit the `BufferIsAudible` loop if we've already found something audible.

Was looking through your IO layout and noticed this.
2016-09-02 12:14:29 -07:00
Kyle Neideck 758fe02c7a Add OS X 10.10 and Xcode 8 builds to .travis.yml. 2016-08-23 19:42:07 +10:00
Kyle Neideck 23fd57713d Fix nullability warnings in builds using the macOS 10.12 SDK.
Fixes #70.
2016-08-22 00:42:55 +10:00
Kyle Neideck dad87b57b6 Add a summary to DEVELOPING.md and update a few sections.
The summary is originally from #66.
2016-08-17 23:22:18 +10:00
Kyle Neideck 679d624860 Add BGM_Driver tests: get/set the music player bundle ID property. 2016-07-04 16:32:59 +10:00
Kyle Neideck 55e9f60774 Merge branch 'hoke-t-Decibel'. Resolves #17. 2016-06-30 09:32:12 +10:00
Kyle Neideck e3fcbdb37e Add BGMDecibel files to the Xcode project. 2016-06-30 09:30:05 +10:00
Kyle Neideck 1ee9fa348e Fix build failure on case-sensitive file systems. Fixes #64.
It seems that BGMDriver was failing to compile on case-sensitive file
systems because BGM_Types.h included "AudioServerPlugin.h" instead of
"AudioServerPlugIn.h". (Lowercase "i".)

I tried building with the project and Xcode on a case-sensitive disk
image and it would fail without this patch. So I figure it should at
least build now. I haven't had time to test Background Music on a system
running on a case-insensitive file system yet, so I added a TODO about
it in TODO.md.

Also, some unrelated tidying up.
2016-06-16 18:38:29 +10:00
Kyle Neideck 3684483543 Merge branch 'Decibel' of https://github.com/hoke-t/BackgroundMusic into hoke-t-Decibel 2016-05-14 10:42:50 +10:00
Tanner Hoke 30a1735346 git rm extraneous Decibel files and use HTTPS links in README.md 2016-05-12 17:24:55 -05:00
Kyle Neideck 4dba9412fb Install NullAudio driver before tests run on Travis. 2016-05-11 10:02:31 +10:00
Kyle Neideck 90bceb9887 On Travis, quit BGMApp before running the tests.
Also, log the installed audio devices and their audio IDs.

I think the tests are probably failing because the Travis VMs don't have
any audio devices. If so, this won't actually fix the tests, but it
should help us narrow it down.
2016-05-11 02:34:54 +10:00
Kyle Neideck 6c9ca6e85c Merge pull request #53 from Piccirello/master
Add shortcut for changing output device from menu bar
2016-05-11 01:27:29 +10:00
Kyle Neideck 82ea2e4803 Merge pull request #27 from hoke-t/master
Disable the Auto-pause Music menu item if the selected music player isn't
running
2016-05-11 00:19:49 +10:00
Kyle Neideck bbe65a2431 Only pretend to disable the auto-pause menu item.
Instead of disabling the menu item when the music player isn't running,
just make it appear disabled. That way you can always disable auto-pause
without having to open your music player, but the UI still indicates
when it thinks the music player isn't running.
2016-05-11 00:08:46 +10:00
Kyle Neideck a229791ade Merge branch 'master' of https://github.com/hoke-t/BackgroundMusic into hoke-t-master 2016-05-10 07:06:57 +10:00
Piccirello be4135523e Add shortcut for changing output device from menu bar 2016-05-03 16:36:53 -04:00
Kyle Neideck b58ad2a1f8 Fix possible deadlock when starting IO.
BGM_Device::StartIO was holding the state mutex longer than it needed
to, which meant HasProperty, GetProperty, etc. couldn't return. If
BGMPlayThrough was notified about IO starting after StartIO locked the
mutex, BGMPlayThrough would get stuck trying to get one of BGMDevice's
properties.

Fixes #46.
2016-04-30 21:28:16 +10:00
Kyle Neideck 960fe0d28d Fix rare race condition in BGM_TaskQueue (hopefully).
Also enable a few more warnings in the BGMDriver project.
2016-04-30 20:50:29 +10:00
Kyle Neideck d827e7e0d8 Fix build script printing a git error when run outside of a git repo. 2016-04-29 13:29:19 +10:00
Kyle Neideck 34071e633f Add an install "one-liner" to the README.
Also fix a bug when running build_and_install.sh from a directory other
than the root of the project.
2016-04-29 13:18:52 +10:00
Kyle Neideck d021dff7a6 Merge pull request #50 from ZV95/patch-1
Addition of Wiki Install Instructions
2016-04-29 12:20:27 +10:00
Kyle Neideck e95f371305 Add debug build option to build script.
Also add info about logging to CONTRIBUTING.md.
2016-04-29 12:09:37 +10:00
Kyle Neideck 6fb281c3ce Merge pull request #30 from IgorMarques/improve/readme
Improve install instructions
2016-04-29 10:57:07 +10:00
Kyle Neideck 3f62b012c3 Fix build script failure if Xcode check finishes before install starts. 2016-04-29 07:41:06 +10:00
ZV95 c70a22dd24 Addition of Wiki Install Instructions
Tried to make a simple install instructional Wiki for those who are confused about installation. Simply adding a link to that wiki in this ReadMe.
2016-04-28 13:22:38 -04:00
Kyle Neideck ccb709fc02 Fix "sudo -v" in uninstall.sh causing Travis builds to fail. 2016-04-28 10:11:19 +10:00
Kyle Neideck 480d769c26 Fix race condition in build script. Also avoid 'sudo -v' on Travis CI. 2016-04-28 09:33:23 +10:00
Kyle Neideck 3eac1f5dab Allow sudo on Travis CI. 2016-04-28 08:49:35 +10:00
Kyle Neideck eda3505f1a Add initial .travis.yml to see if it works. 2016-04-27 02:49:35 +10:00
Kyle Neideck 44082ac920 Add manual installation instructions (mostly for troubleshooting) 2016-04-27 02:31:14 +10:00
Kyle Neideck f2a0898590 Build script: Check Xcode version in the background to launch quicker. 2016-04-27 02:28:47 +10:00
Kyle Neideck b707513e49 Lots of small improvements to the build script.
- Clean before installing. (Mostly to get full logs every time.)
- Clearer error messages.
- Better checking for Xcode/xcodebuild.
- Log extra system info.
- A number of minor bug fixes.
2016-04-26 21:04:13 +10:00
Kyle Neideck 8acc5d4c9e Change the min and max sample rates in BGM_Driver. 2016-04-25 08:38:45 +10:00
Tanner Hoke ad5fe4ecbb Update README.md 2016-04-19 10:53:10 -05:00
Tanner Hoke 7cc0d19182 Put music files in the correct group. 2016-04-19 10:43:24 -05:00
Igor Marques 9a5ed7c2b5 Improve install instructions 2016-04-19 10:06:27 -03:00
Tanner Hoke 5981e05bb1 Add support for Decibel 2016-04-18 21:37:29 -05:00
Tanner Hoke e87c43dc52 Disable the Auto-pause Music menu item if the selected music player isn't running 2016-04-18 20:15:28 -05:00
136 changed files with 18158 additions and 2996 deletions
+4
View File
@@ -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
View File
@@ -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
+18
View File
@@ -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>
-178
View File
@@ -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
+335
View File
@@ -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
+14 -5
View File
@@ -35,23 +35,32 @@
// Protocol for the UI custom classes
@protocol BGMAppVolumeSubview <NSObject>
@protocol BGMAppVolumeMenuItemSubview <NSObject>
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx;
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)item;
@end
// Custom classes for the UI elements in the app volume menu items
@interface BGMAVM_AppIcon : NSImageView <BGMAppVolumeSubview>
@interface BGMAVM_AppIcon : NSImageView <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_AppNameLabel : NSTextField <BGMAppVolumeSubview>
@interface BGMAVM_AppNameLabel : NSTextField <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_VolumeSlider : NSSlider <BGMAppVolumeSubview>
@interface BGMAVM_ShowMoreControlsButton : NSButton <BGMAppVolumeMenuItemSubview>
@end
@interface BGMAVM_VolumeSlider : NSSlider <BGMAppVolumeMenuItemSubview>
- (void) setRelativeVolume:(NSNumber*)relativeVolume;
@end
@interface BGMAVM_PanSlider : NSSlider <BGMAppVolumeMenuItemSubview>
- (void) setPanPosition:(NSNumber*)panPosition;
@end
+233 -52
View File
@@ -17,7 +17,8 @@
// BGMAppVolumes.m
// BGMApp
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
// Copyright © 2017 Andrew Tonner
//
// Self Include
@@ -25,6 +26,7 @@
// BGM Includes
#include "BGM_Types.h"
#include "BGM_Utils.h"
// PublicUtility Includes
#include "CACFDictionary.h"
@@ -32,14 +34,20 @@
#include "CACFString.h"
static NSInteger const kAppVolumesMenuItemTag = 3;
// Tags for UI elements in MainMenu.xib
static NSInteger const kAppVolumesHeadingMenuItemTag = 3;
static NSInteger const kSeparatorBelowAppVolumesMenuItemTag = 4;
static float const kSlidersSnapWithin = 5;
static CGFloat const kAppVolumeViewInitialHeight = 20;
@implementation BGMAppVolumes {
NSMenu* bgmMenu;
NSView* appVolumeView;
CGFloat appVolumeViewFullHeight;
BGMAudioDeviceManager* audioDevices;
}
@@ -47,6 +55,7 @@ static float const kSlidersSnapWithin = 5;
if ((self = [super init])) {
bgmMenu = menu;
appVolumeView = view;
appVolumeViewFullHeight = appVolumeView.frame.size.height;
audioDevices = devices;
// Create the menu items for controlling app volumes
@@ -66,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
+366
View File
@@ -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
}
+93
View File
@@ -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 */
+43 -11
View File
@@ -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
+324 -70
View File
@@ -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 ? &currentDeviceID : nullptr)];
} catch (...) {
return [self failedToSetOutputDevice:newDeviceID
errorCode:kAudioHardwareUnspecifiedError
revertTo:(revertOnFailure ? &currentDeviceID : 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
+48
View File
@@ -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
+201
View File
@@ -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
+7 -1
View File
@@ -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
+79 -22
View File
@@ -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];
}
});
}
+139 -259
View File
@@ -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
+73 -33
View File
@@ -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 */
+507
View File
@@ -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
+137
View File
@@ -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 */
File diff suppressed because it is too large Load Diff
+81 -28
View File
@@ -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 */
+46
View File
@@ -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
+106
View File
@@ -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
+24 -2
View File
@@ -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;
+170 -49
View File
@@ -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"
}
}
+9 -1
View File
@@ -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>
+30
View File
@@ -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
+103
View File
@@ -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
+30
View File
@@ -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
+102
View File
@@ -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
+80 -55
View File
@@ -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
+40 -115
View File
@@ -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
+1 -1
View File
@@ -24,7 +24,7 @@
#import "BGMMusicPlayer.h"
@interface BGMSpotify : BGMMusicPlayer
@interface BGMSpotify : BGMMusicPlayerBase<BGMMusicPlayer>
@end
+33 -23
View File
@@ -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
+1 -1
View File
@@ -24,7 +24,7 @@
#import "BGMMusicPlayer.h"
@interface BGMVLC : BGMMusicPlayer
@interface BGMVLC : BGMMusicPlayerBase<BGMMusicPlayer>
@end
+35 -27
View File
@@ -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
+5 -2
View File
@@ -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
+38 -29
View File
@@ -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
+181
View File
@@ -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
+78
View File
@@ -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 (0100).
@property HermesPlayerStates playbackState; // The current playback state.
@property (readonly) double playbackPosition; // The current songs 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 songs 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 dont like the current song.
- (void) tiredOfSong; // Tell Pandora youre 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 songs title.
@property (copy, readonly) NSString *artist; // The songs artist.
@property (copy, readonly) NSString *album; // The songs album.
@property (copy, readonly) NSString *artworkURL; // An image URL for the albums cover artwork.
@property (readonly) NSInteger rating; // The songs 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 stations name.
@property (copy, readonly) NSString *stationID; // The stations 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
+144
View File
@@ -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
+191 -33
View File
@@ -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;
+13 -62
View File
@@ -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
+59
View File
@@ -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
+258
View File
@@ -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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -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
+6
View File
@@ -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.
+31
View File
@@ -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
@@ -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>
+173
View File
@@ -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
+49
View File
@@ -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));
}
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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;
+34 -17
View File
@@ -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" \
+33 -17
View File
@@ -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
@@ -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");
}
}
+35 -9
View File
@@ -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);
}
+3 -2
View File
@@ -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
+249 -100
View File
@@ -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,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>
+409
View File
@@ -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
+111
View File
@@ -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 */
File diff suppressed because it is too large Load Diff
+78 -26
View File
@@ -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 */
+504
View File
@@ -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
+186
View File
@@ -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 */
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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