Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e50e2348b | |||
| 0388b26aa1 | |||
| 002afc0cd1 | |||
| a45335b65d | |||
| 0a7be7d32c | |||
| d26e9ee3d1 | |||
| 94fc1259e3 | |||
| a97529e812 | |||
| 0231e131df | |||
| 9951a82879 | |||
| 3ac7221cb1 | |||
| 2be4bab54f | |||
| 04f17301a1 | |||
| e616718eab | |||
| 503d1a92ec | |||
| 2939dbe28c | |||
| a40dfde439 | |||
| df9815a4be | |||
| 26dd2ee1ab | |||
| d89d1e7813 | |||
| e093e7d3b2 | |||
| 14df80da24 | |||
| a4c93c050b | |||
| 5257b4c94d | |||
| 30185633ac | |||
| 624369f297 | |||
| d1bf34e741 | |||
| 475d141ae4 | |||
| d1f5492a47 | |||
| 64b7ca9fd9 | |||
| 7b8d1a0e0d | |||
| 5e12f9fc01 | |||
| 94f13e747c | |||
| 4c0c656538 | |||
| 871bb97a52 | |||
| ffe7406025 | |||
| 29642da1cf | |||
| 1bb3873a53 | |||
| a4c849dcb2 | |||
| b0282706df | |||
| 94a5f37c2b | |||
| 797d2f14f5 | |||
| b3b4482bda | |||
| 9b33fffd23 | |||
| ac33909b51 | |||
| 1a49802675 | |||
| 75e8d5ceac | |||
| 08fdef6084 | |||
| 1e5d625d64 | |||
| ffa86bbcd9 | |||
| 5f31f54a85 | |||
| 2a41204fc0 | |||
| cb9cdb00b6 | |||
| ed06a257a8 | |||
| 7171cfcb78 | |||
| 3ba53a50ac | |||
| 944fc11212 | |||
| 287bae0923 | |||
| 6117bc285c | |||
| 18aa97f055 | |||
| ac2130c3c3 |
@@ -6,6 +6,9 @@
|
||||
tags
|
||||
cmake-build-debug/
|
||||
/Background-Music-*/
|
||||
BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
Images/*.aux
|
||||
Images/*.log
|
||||
|
||||
# Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore
|
||||
|
||||
|
||||
+35
-9
@@ -2,22 +2,35 @@ language: objective-c
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode9.2
|
||||
xcode_sdk: macosx10.13
|
||||
osx_image: xcode11
|
||||
xcode_sdk: macosx10.14
|
||||
sudo: required
|
||||
env: DEPLOY=true
|
||||
- os: osx
|
||||
osx_image: xcode9.1
|
||||
osx_image: xcode10.1
|
||||
xcode_sdk: macosx10.14
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode10
|
||||
xcode_sdk: macosx10.14
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode9.4
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
osx_image: xcode9.3
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
xcode_sdk: macosx10.12
|
||||
osx_image: xcode9.2
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
# Fails to compile in 8.3, but it isn't clear from the logs why it fails.
|
||||
# - os: osx
|
||||
# osx_image: xcode8.3
|
||||
# xcode_sdk: macosx10.12
|
||||
# sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode7.3
|
||||
xcode_sdk: macosx10.11
|
||||
@@ -32,12 +45,15 @@ 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
|
||||
- hdiutil create -type SPARSEBUNDLE -fs 'Case-sensitive Journaled HFS+' -volname bgmbuild -nospotlight -verbose -attach -size 100m bgmbuild.dmg
|
||||
- sudo cp -r . /Volumes/bgmbuild
|
||||
- cd /Volumes/bgmbuild
|
||||
# Install Background Music.
|
||||
- yes | ./build_and_install.sh
|
||||
# Print the log file, but put it in a fold because it's so long.
|
||||
- echo -en 'build_and_install.log\ntravis_fold:start:build.log\\r'
|
||||
- cat build_and_install.log
|
||||
- echo -en 'travis_fold:end:build.log\\r'
|
||||
- find */build/Release/*/ -type f -exec md5 {} \;
|
||||
# Log the installed audio devices...
|
||||
- system_profiler SPAudioDataType
|
||||
@@ -56,9 +72,12 @@ script:
|
||||
# Skip the UI tests until Travis has support for them.
|
||||
- BGMApp/BGMAppTests/UITests/travis-skip.py
|
||||
# Run the tests.
|
||||
# The echo commands put the output into a fold in the Travis logs.
|
||||
- echo -en 'Unit Tests\ntravis_fold:start:tests\\r'
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'Background Music Device' test
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'Background Music' test
|
||||
- xcodebuild -workspace BGM.xcworkspace -scheme 'BGMXPCHelper' test
|
||||
- echo -en 'travis_fold:end:tests\\r'
|
||||
# Uninstall Background Music.
|
||||
- yes | ./uninstall.sh
|
||||
# Check the BGM dirs and files were removed.
|
||||
@@ -69,8 +88,13 @@ script:
|
||||
- if ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"; then false; fi
|
||||
# Return early if we're not testing packaging on this OS X version.
|
||||
- if [[ "$PACKAGE" == "false" ]]; then exit 0; fi
|
||||
# Build the .pkg installer. (Print the logs if it fails.)
|
||||
- ./package.sh || (cat build_and_install.log && false)
|
||||
# Build the .pkg installer. Print the build logs if it fails. If this build is for a tag with
|
||||
# "DEBUG" in its name, build a debug package. (More detailed logging, no optimization, etc.)
|
||||
- if [[ "$TRAVIS_TAG" =~ .*DEBUG.* ]]; then
|
||||
./package.sh -d || (cat build_and_install.log && travis_terminate 1);
|
||||
else
|
||||
./package.sh || (cat build_and_install.log && travis_terminate 1);
|
||||
fi
|
||||
# Install the .pkg.
|
||||
- sudo installer -pkg Background-Music-*/BackgroundMusic-*.pkg -target / -verbose -dumplog
|
||||
# Check the BGM dirs and files were installed again.
|
||||
@@ -89,6 +113,8 @@ deploy:
|
||||
file_glob: true
|
||||
file: Background-Music-*/*
|
||||
skip_cleanup: true
|
||||
name: $TRAVIS_TAG
|
||||
prerelease: true
|
||||
on:
|
||||
repo: kyleneideck/BackgroundMusic
|
||||
tags: true
|
||||
|
||||
Generated
+6
@@ -37,6 +37,12 @@
|
||||
<FileRef
|
||||
location = "group:uninstall.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:package.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:pkg">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Images">
|
||||
</FileRef>
|
||||
|
||||
@@ -7,60 +7,88 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; };
|
||||
1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; };
|
||||
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; };
|
||||
1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; };
|
||||
1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; };
|
||||
1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; };
|
||||
1C1962F41BCABFC5008A4DF7 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; };
|
||||
1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; };
|
||||
1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; };
|
||||
1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; };
|
||||
1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; };
|
||||
1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; };
|
||||
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
|
||||
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVolumeChangeListener.cpp"; }; };
|
||||
19FE72566BCEB11BD1F3D487 /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; };
|
||||
19FE76F614F260F3F65AF550 /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; };
|
||||
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
|
||||
19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; };
|
||||
19FE7B32E1214BA0E8166A9E /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; };
|
||||
19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; };
|
||||
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMStatusBarItem.mm"; }; };
|
||||
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusicPrefs.mm"; }; };
|
||||
1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferencesMenu.mm"; }; };
|
||||
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusic.mm"; }; };
|
||||
1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CARingBuffer.cpp"; }; };
|
||||
1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPlayThrough.cpp"; }; };
|
||||
1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioDevice.cpp"; }; };
|
||||
1C1962F41BCABFC5008A4DF7 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioObject.cpp"; }; };
|
||||
1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioStream.cpp"; }; };
|
||||
1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioSystemObject.cpp"; }; };
|
||||
1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugMacros.cpp"; }; };
|
||||
1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugPrintf.cpp"; }; };
|
||||
1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFString.cpp"; }; };
|
||||
1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; };
|
||||
1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; };
|
||||
1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; };
|
||||
1C1AA4B01F9C673000BCFB22 /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; };
|
||||
1C1AA4B11F9DE3B700BCFB22 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
1C1AA4B21F9DE3B700BCFB22 /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAMutex.cpp"; }; };
|
||||
1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHostTimeBase.cpp"; }; };
|
||||
1C1AA4B01F9C673000BCFB22 /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGM_Utils.cpp"; }; };
|
||||
1C1AA4B11F9DE3B700BCFB22 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMAudioDevice.cpp"; }; };
|
||||
1C1AA4B21F9DE3B700BCFB22 /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMBackgroundMusicDevice.cpp"; }; };
|
||||
1C1AA4B31F9DE40000BCFB22 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
|
||||
1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; };
|
||||
1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; };
|
||||
1C2336DF1BEAE10C004C1C4E /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; };
|
||||
1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMMusicPlayer.m"; }; };
|
||||
1C2336DF1BEAE10C004C1C4E /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSpotify.m"; }; };
|
||||
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */; };
|
||||
1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; };
|
||||
1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppDelegate+AppleScript.mm"; }; };
|
||||
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; };
|
||||
1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; };
|
||||
1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMASOutputDevice.mm"; }; };
|
||||
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; };
|
||||
1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
|
||||
1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDeviceControlsList.cpp"; }; };
|
||||
1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
|
||||
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
|
||||
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; };
|
||||
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; };
|
||||
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; };
|
||||
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppVolumes.m"; }; };
|
||||
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMiTunes.m"; }; };
|
||||
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDeviceControlSync.cpp"; }; };
|
||||
1C4D1A1D217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferredOutputDevices.mm"; }; };
|
||||
1C4D1A1E217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */; };
|
||||
1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
1C533C7A1EED28B700270802 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C533C791EED28B700270802 /* uninstall.sh */; };
|
||||
1C533C7B1EED2F6200270802 /* safe_install_dir.sh in Resources */ = {isa = PBXBuildFile; fileRef = 276972901CB16008007A2F7C /* safe_install_dir.sh */; };
|
||||
1C533C7C1EED2F8A00270802 /* com.bearisdriving.BGM.XPCHelper.plist.template in Resources */ = {isa = PBXBuildFile; fileRef = 2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */; };
|
||||
1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */; };
|
||||
1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; };
|
||||
1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSystemSoundsVolume.mm"; }; };
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; };
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C8034D520B0347A004BC50C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8034D420B0347A004BC50C /* Security.framework */; };
|
||||
1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppWatcher.m"; }; };
|
||||
1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; };
|
||||
1C8104AF22AD07E200B35517 /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; };
|
||||
1C8104B022AD082E00B35517 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; };
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMOutputVolumeMenuItem.mm"; }; };
|
||||
1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C837DDA1F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAPThread.cpp"; }; };
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; };
|
||||
1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; };
|
||||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSwinsian.m"; }; };
|
||||
1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayer.m"; }; };
|
||||
1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; };
|
||||
1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; };
|
||||
1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; };
|
||||
1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayerConnection.m"; }; };
|
||||
1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; };
|
||||
1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; };
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMBackgroundMusicDevice.cpp"; }; };
|
||||
1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; };
|
||||
1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33E1BBA75EF000E2DD1 /* main.m */; };
|
||||
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; };
|
||||
1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; };
|
||||
1CC1DF911BE5891300FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; };
|
||||
1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppDelegate.mm"; }; };
|
||||
1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33E1BBA75EF000E2DD1 /* main.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-main.m"; }; };
|
||||
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFArray.cpp"; }; };
|
||||
1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFDictionary.cpp"; }; };
|
||||
1CC1DF911BE5891300FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugger.cpp"; }; };
|
||||
1CC1DF961BE8607700FB8FE4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1CC1DF951BE8607700FB8FE4 /* Images.xcassets */; };
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; };
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMTermination.mm"; }; };
|
||||
1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; };
|
||||
1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; };
|
||||
1CCC4F3E1E58196C008053E4 /* BGMXPCHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F3C1E58196C008053E4 /* BGMXPCHelperTests.m */; };
|
||||
@@ -68,7 +96,7 @@
|
||||
1CCC4F4E1E581C40008053E4 /* Mock_CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F4C1E581C40008053E4 /* Mock_CAHALAudioObject.cpp */; };
|
||||
1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F611E584100008053E4 /* BGMAppUITests.mm */; };
|
||||
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
|
||||
1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; };
|
||||
1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppVolumesController.mm"; }; };
|
||||
1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; };
|
||||
1CD410D61F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; };
|
||||
1CD989341ECFFC9E0014BBBF /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; };
|
||||
@@ -101,7 +129,7 @@
|
||||
1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; };
|
||||
1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; };
|
||||
1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; };
|
||||
1CD989521ECFFCFC0014BBBF /* BGMOutputDevicePrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */; };
|
||||
1CD989521ECFFCFC0014BBBF /* BGMOutputDeviceMenuSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */; };
|
||||
1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; };
|
||||
1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; };
|
||||
1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; };
|
||||
@@ -110,36 +138,36 @@
|
||||
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; };
|
||||
1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
|
||||
1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; };
|
||||
1CE7064C1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */; };
|
||||
1CE7064C1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMOutputDeviceMenuSection.mm"; }; };
|
||||
1CEACF4D1F34793700FEC143 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; };
|
||||
1CEACF4F1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CEACF4E1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp */; };
|
||||
1CED61691C3081C2002CAFCF /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 1CED61681C3081C2002CAFCF /* LICENSE */; };
|
||||
1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; };
|
||||
1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAudioDeviceManager.mm"; }; };
|
||||
1CF2D58F1F944773008B6E35 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; };
|
||||
1CF2D5901F944789008B6E35 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; };
|
||||
1CF2D5911F944789008B6E35 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; };
|
||||
1CF2D5921F944789008B6E35 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; };
|
||||
1CF2D5931F94479A008B6E35 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; };
|
||||
1CF2D5941F9447AE008B6E35 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; };
|
||||
1CF2D5951F9447AE008B6E35 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; };
|
||||
1CF2D5961F9447AE008B6E35 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; };
|
||||
1CF2D5971F9447AE008B6E35 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; };
|
||||
1CF2D5981F9447AE008B6E35 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; };
|
||||
1CF2D5991F9447AE008B6E35 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; };
|
||||
1CF2D59A1F9447AE008B6E35 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; };
|
||||
1CF2D59B1F9447AE008B6E35 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; };
|
||||
1CF2D59C1F9447AE008B6E35 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; };
|
||||
1CF2D59D1F9447AE008B6E35 /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
|
||||
1CF2D59E1F9447AE008B6E35 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; };
|
||||
1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
1CF2D5901F944789008B6E35 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioDevice.cpp"; }; };
|
||||
1CF2D5911F944789008B6E35 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioObject.cpp"; }; };
|
||||
1CF2D5921F944789008B6E35 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioSystemObject.cpp"; }; };
|
||||
1CF2D5931F94479A008B6E35 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioStream.cpp"; }; };
|
||||
1CF2D5941F9447AE008B6E35 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFArray.cpp"; }; };
|
||||
1CF2D5951F9447AE008B6E35 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFDictionary.cpp"; }; };
|
||||
1CF2D5961F9447AE008B6E35 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFNumber.cpp"; }; };
|
||||
1CF2D5971F9447AE008B6E35 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFString.cpp"; }; };
|
||||
1CF2D5981F9447AE008B6E35 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugger.cpp"; }; };
|
||||
1CF2D5991F9447AE008B6E35 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugMacros.cpp"; }; };
|
||||
1CF2D59A1F9447AE008B6E35 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugPrintf.cpp"; }; };
|
||||
1CF2D59B1F9447AE008B6E35 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHostTimeBase.cpp"; }; };
|
||||
1CF2D59C1F9447AE008B6E35 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAMutex.cpp"; }; };
|
||||
1CF2D59D1F9447AE008B6E35 /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAPThread.cpp"; }; };
|
||||
1CF2D59E1F9447AE008B6E35 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CARingBuffer.cpp"; }; };
|
||||
1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAudioDevice.cpp"; }; };
|
||||
1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */; };
|
||||
271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; };
|
||||
27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; };
|
||||
273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; };
|
||||
2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; };
|
||||
2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; };
|
||||
2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; };
|
||||
271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFNumber.cpp"; }; };
|
||||
27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVLC.m"; }; };
|
||||
273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVOX.m"; }; };
|
||||
2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMMusicPlayers.mm"; }; };
|
||||
2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMScriptingBridge.m"; }; };
|
||||
2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMUserDefaults.m"; }; };
|
||||
2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; };
|
||||
2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; };
|
||||
2743CA031D86D41C0089613B /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; };
|
||||
@@ -167,16 +195,16 @@
|
||||
2743CA221D86DE960089613B /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; };
|
||||
2743CA231D86DEA70089613B /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
|
||||
274827951E11052500B31D8D /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1CB8B3421BBA75EF000E2DD1 /* MainMenu.xib */; };
|
||||
277170161CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */; };
|
||||
2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */; };
|
||||
279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; };
|
||||
27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */; };
|
||||
27D1D6BB1DD7226C0049E707 /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; };
|
||||
27D643C01C9FB99200737F6E /* BGMXPCHelperService.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */; };
|
||||
27D643C11C9FB99200737F6E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BC1C9FB84C00737F6E /* main.m */; };
|
||||
27F7D4901D2483B100821C4B /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; };
|
||||
277170161CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMXPCListenerDelegate.m"; }; };
|
||||
2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMXPCListener.mm"; }; };
|
||||
279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMHermes.m"; }; };
|
||||
27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMenuItem.m"; }; };
|
||||
27D1D6BB1DD7226C0049E707 /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAboutPanel.m"; }; };
|
||||
27D643C01C9FB99200737F6E /* BGMXPCHelperService.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMXPCHelperService.mm"; }; };
|
||||
27D643C11C9FB99200737F6E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BC1C9FB84C00737F6E /* main.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-main.m"; }; };
|
||||
27F7D4901D2483B100821C4B /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDecibel.m"; }; };
|
||||
27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; };
|
||||
27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; };
|
||||
27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGM_Utils.cpp"; }; };
|
||||
27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; };
|
||||
27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -199,6 +227,12 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
19FE70CF6C93F5007940CE91 /* BGMMusic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMMusic.h; path = "Music Players/BGMMusic.h"; sourceTree = "<group>"; };
|
||||
19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMVolumeChangeListener.cpp; sourceTree = "<group>"; };
|
||||
19FE73822ADD50BA9120AB05 /* BGMMusic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMMusic.m; path = "Music Players/BGMMusic.m"; sourceTree = "<group>"; };
|
||||
19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMStatusBarItem.mm; sourceTree = "<group>"; };
|
||||
19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMStatusBarItem.h; sourceTree = "<group>"; };
|
||||
19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMVolumeChangeListener.h; sourceTree = "<group>"; };
|
||||
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMAutoPauseMusicPrefs.h; path = Preferences/BGMAutoPauseMusicPrefs.h; sourceTree = "<group>"; };
|
||||
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMAutoPauseMusicPrefs.mm; path = Preferences/BGMAutoPauseMusicPrefs.mm; sourceTree = "<group>"; };
|
||||
1C0BD0A61BF1B029004F4CF5 /* BGMPreferencesMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMPreferencesMenu.h; path = Preferences/BGMPreferencesMenu.h; sourceTree = "<group>"; };
|
||||
@@ -248,17 +282,32 @@
|
||||
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlsList.h; sourceTree = "<group>"; };
|
||||
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BGMAppVolumes.m; sourceTree = "<group>"; };
|
||||
1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumes.h; sourceTree = "<group>"; };
|
||||
1C43DABE22F582780004AF35 /* Background Music.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Background Music.entitlements"; sourceTree = "<group>"; };
|
||||
1C4699461BD5C0E400F78043 /* BGMiTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMiTunes.m; path = "Music Players/BGMiTunes.m"; sourceTree = "<group>"; };
|
||||
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlSync.cpp; sourceTree = "<group>"; };
|
||||
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlSync.h; sourceTree = "<group>"; };
|
||||
1C4D1A1B217C7D6400A1ACD0 /* BGMPreferredOutputDevices.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMPreferredOutputDevices.h; sourceTree = "<group>"; };
|
||||
1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMPreferredOutputDevices.mm; sourceTree = "<group>"; };
|
||||
1C533C791EED28B700270802 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = uninstall.sh; path = ../../uninstall.sh; sourceTree = "<group>"; };
|
||||
1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "_uninstall-non-interactive.sh"; sourceTree = "<group>"; };
|
||||
1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMSystemSoundsVolume.h; sourceTree = "<group>"; };
|
||||
1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMSystemSoundsVolume.mm; sourceTree = "<group>"; };
|
||||
1C8034C21BDAFD5700668E00 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
|
||||
1C8034C31BDAFD5700668E00 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
|
||||
1C8034D420B0347A004BC50C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
1C80DED120A6718600045BBE /* BGMAppWatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppWatcher.h; sourceTree = "<group>"; };
|
||||
1C80DED220A6718600045BBE /* BGMAppWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BGMAppWatcher.m; sourceTree = "<group>"; };
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputVolumeMenuItem.h; sourceTree = "<group>"; };
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputVolumeMenuItem.mm; sourceTree = "<group>"; };
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Swinsian.h; path = "Music Players/Swinsian.h"; sourceTree = "<group>"; };
|
||||
1C8D8302204238DB00A838F2 /* BGMSwinsian.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSwinsian.m; path = "Music Players/BGMSwinsian.m"; sourceTree = "<group>"; };
|
||||
1C8D8303204238DB00A838F2 /* BGMSwinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSwinsian.h; path = "Music Players/BGMSwinsian.h"; sourceTree = "<group>"; };
|
||||
1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayer.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.h"; sourceTree = "<group>"; };
|
||||
1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayer.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.m"; sourceTree = "<group>"; };
|
||||
1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = GooglePlayMusicDesktopPlayer.js; path = "Music Players/GooglePlayMusicDesktopPlayer.js"; sourceTree = "<group>"; };
|
||||
1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayerConnection.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.h"; sourceTree = "<group>"; };
|
||||
1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayerConnection.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.m"; sourceTree = "<group>"; };
|
||||
1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMBackgroundMusicDevice.cpp; sourceTree = "<group>"; };
|
||||
1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMBackgroundMusicDevice.h; sourceTree = "<group>"; };
|
||||
1CB8B3361BBA75EF000E2DD1 /* Background Music.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Background Music.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -287,8 +336,9 @@
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
|
||||
1CD410D21F9EDDAD0070A094 /* BGMAppVolumesController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumesController.h; sourceTree = "<group>"; };
|
||||
1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumesController.mm; sourceTree = "<group>"; };
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMOutputDevicePrefs.h; path = Preferences/BGMOutputDevicePrefs.h; sourceTree = "<group>"; };
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMOutputDevicePrefs.mm; path = Preferences/BGMOutputDevicePrefs.mm; sourceTree = "<group>"; };
|
||||
1CDE224022CBB95B0008E3AC /* Music.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Music.h; path = "Music Players/Music.h"; sourceTree = "<group>"; };
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputDeviceMenuSection.h; sourceTree = "<group>"; };
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputDeviceMenuSection.mm; sourceTree = "<group>"; };
|
||||
1CEACF4E1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioSystemObject.cpp; path = UnitTests/Mock_CAHALAudioSystemObject.cpp; sourceTree = "<group>"; };
|
||||
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = "<group>"; };
|
||||
@@ -352,6 +402,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
1C8034D520B0347A004BC50C /* Security.framework in Frameworks */,
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */,
|
||||
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */,
|
||||
1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */,
|
||||
@@ -362,6 +414,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -404,8 +457,6 @@
|
||||
27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */,
|
||||
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */,
|
||||
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */,
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
||||
);
|
||||
name = "Preferences Menu";
|
||||
sourceTree = "<group>";
|
||||
@@ -487,12 +538,20 @@
|
||||
1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */,
|
||||
27F7D48E1D2483B100821C4B /* BGMDecibel.h */,
|
||||
27F7D48F1D2483B100821C4B /* BGMDecibel.m */,
|
||||
1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */,
|
||||
1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */,
|
||||
1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */,
|
||||
1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */,
|
||||
1C2336DB1BEAB73F004C1C4E /* BGMiTunes.h */,
|
||||
1C4699461BD5C0E400F78043 /* BGMiTunes.m */,
|
||||
19FE70CF6C93F5007940CE91 /* BGMMusic.h */,
|
||||
19FE73822ADD50BA9120AB05 /* BGMMusic.m */,
|
||||
1C2336DD1BEAE10C004C1C4E /* BGMSpotify.h */,
|
||||
1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */,
|
||||
279F48751DD6D73900768A85 /* BGMHermes.h */,
|
||||
279F48761DD6D73900768A85 /* BGMHermes.m */,
|
||||
1C8D8303204238DB00A838F2 /* BGMSwinsian.h */,
|
||||
1C8D8302204238DB00A838F2 /* BGMSwinsian.m */,
|
||||
27379B881C7C562D0084A24C /* BGMVLC.h */,
|
||||
27379B891C7C562D0084A24C /* BGMVLC.m */,
|
||||
273F10DD1CC3D0B900C1C6DA /* BGMVOX.h */,
|
||||
@@ -505,6 +564,7 @@
|
||||
1CB8B32D1BBA75EF000E2DD1 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C43DABE22F582780004AF35 /* Background Music.entitlements */,
|
||||
1CB8B3381BBA75EF000E2DD1 /* BGMApp */,
|
||||
1CB8B34C1BBA75F0000E2DD1 /* BGMApp Tests */,
|
||||
27379B901C7F57DB0084A24C /* BGMXPCHelper */,
|
||||
@@ -534,6 +594,8 @@
|
||||
children = (
|
||||
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */,
|
||||
1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */,
|
||||
1C80DED120A6718600045BBE /* BGMAppWatcher.h */,
|
||||
1C80DED220A6718600045BBE /* BGMAppWatcher.m */,
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */,
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */,
|
||||
1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */,
|
||||
@@ -544,6 +606,8 @@
|
||||
1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */,
|
||||
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */,
|
||||
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */,
|
||||
1C4D1A1B217C7D6400A1ACD0 /* BGMPreferredOutputDevices.h */,
|
||||
1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */,
|
||||
1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */,
|
||||
1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */,
|
||||
1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */,
|
||||
@@ -554,16 +618,22 @@
|
||||
1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */,
|
||||
1C4699451BD5BF2E00F78043 /* Music Players */,
|
||||
1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */,
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.h */,
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */,
|
||||
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */,
|
||||
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */,
|
||||
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */,
|
||||
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */,
|
||||
1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */,
|
||||
1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */,
|
||||
19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */,
|
||||
19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */,
|
||||
1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */,
|
||||
1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */,
|
||||
2743C9ED1D8538700089613B /* BGMUserDefaults.h */,
|
||||
2743C9F01D853FBB0089613B /* BGMUserDefaults.m */,
|
||||
19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */,
|
||||
19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */,
|
||||
2795973C1C982E8C00A002FB /* BGMXPCListener.h */,
|
||||
2795973A1C982E4E00A002FB /* BGMXPCListener.mm */,
|
||||
1C2FC3161EC7078F00A76592 /* Scripting */,
|
||||
@@ -657,9 +727,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27F7D4911D2484A300821C4B /* Decibel.h */,
|
||||
27379B851C7C54870084A24C /* iTunes.h */,
|
||||
27379B861C7C54870084A24C /* Spotify.h */,
|
||||
279F48781DD6D94000768A85 /* Hermes.h */,
|
||||
27379B851C7C54870084A24C /* iTunes.h */,
|
||||
1CDE224022CBB95B0008E3AC /* Music.h */,
|
||||
1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */,
|
||||
27379B861C7C54870084A24C /* Spotify.h */,
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */,
|
||||
27379B871C7C552A0084A24C /* VLC.h */,
|
||||
273F10DC1CC3CF9C00C1C6DA /* VOX.h */,
|
||||
);
|
||||
@@ -683,6 +756,8 @@
|
||||
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C8034D420B0347A004BC50C /* Security.framework */,
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */,
|
||||
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
|
||||
1C1963021BCAC160008A4DF7 /* CoreAudio.framework */,
|
||||
@@ -836,6 +911,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@@ -860,6 +936,7 @@
|
||||
files = (
|
||||
274827951E11052500B31D8D /* MainMenu.xib in Resources */,
|
||||
1C533C7A1EED28B700270802 /* uninstall.sh in Resources */,
|
||||
1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */,
|
||||
1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */,
|
||||
1CED61691C3081C2002CAFCF /* LICENSE in Resources */,
|
||||
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */,
|
||||
@@ -887,6 +964,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -911,7 +989,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Append the git HEAD short ID to the build version for snapshot builds. Thanks to\n# Václav Slavík for the initial version of this: http://stackoverflow.com/a/26354117/1091063\n# TODO: Update CFBundleVersion as well?\nTAG=$(/usr/bin/git tag --points-at HEAD 2>/dev/null)\nif [[ $? -eq 0 ]] && [[ \"${TAG}\" == \"\" ]]; then # If HEAD isn't tagged, this is a snapshot build.\n HEAD=$(/usr/bin/git rev-list HEAD --max-count=1 --abbrev-commit)\n INFO_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n if [[ \"${CONFIGURATION}\" != \"Release\" ]]; then\n TYPE=\"DEBUG\"\n else\n TYPE=\"SNAPSHOT\"\n fi\n if [[ -f \"$INFO_PLIST\" ]]; then\n CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"${INFO_PLIST}\")\n BASE_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :BGMBundleVersionBase\" \"${INFO_PLIST}\" 2>/dev/null)\n if [[ $? -ne 0 ]] || [[ \"${BASE_VERSION}\" == \"\" ]]; then\n BASE_VERSION=\"${CURRENT_VERSION}\"\n /usr/libexec/PlistBuddy -c \"Add :BGMBundleVersionBase string ${BASE_VERSION}\" \"${INFO_PLIST}\"\n fi\n NEW_VERSION=\"${BASE_VERSION}-${TYPE}-${HEAD}\"\n if [[ \"${NEW_VERSION}\" != \"${CURRENT_VERSION}\" ]]; then # Only touch the file if we need to.\n /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString ${NEW_VERSION}\" \"${INFO_PLIST}\"\n fi\n fi\nfi";
|
||||
shellScript = "# Append the git HEAD short ID to the build version for snapshot builds. Thanks to\n# Václav Slavík for the initial version of this: http://stackoverflow.com/a/26354117/1091063\n# TODO: Update CFBundleVersion as well?\n\n# If HEAD isn't tagged, or has \"SNAPSHOT\" or \"DEBUG\" in the tag name, this is a snapshot build.\n# If HEAD is tagged more than once, use the most recent.\nTAG=$(/usr/bin/git tag --points-at HEAD --sort='-taggerdate' 2>/dev/null | head -n 1)\nif [[ $? -eq 0 ]] && ( [[ \"${TAG}\" == \"\" ]] || \\\n [[ \"${TAG}\" =~ .*SNAPSHOT.* ]] || \\\n [[ \"${TAG}\" =~ .*DEBUG.* ]] ); then\n HEAD=$(/usr/bin/git rev-list HEAD --max-count=1 --abbrev-commit)\n INFO_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n if [[ \"${CONFIGURATION}\" != \"Release\" ]]; then\n TYPE=\"DEBUG\"\n else\n TYPE=\"SNAPSHOT\"\n fi\n if [[ -f \"$INFO_PLIST\" ]]; then\n CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"${INFO_PLIST}\")\n BASE_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :BGMBundleVersionBase\" \"${INFO_PLIST}\" 2>/dev/null)\n if [[ $? -ne 0 ]] || [[ \"${BASE_VERSION}\" == \"\" ]]; then\n BASE_VERSION=\"${CURRENT_VERSION}\"\n /usr/libexec/PlistBuddy -c \"Add :BGMBundleVersionBase string ${BASE_VERSION}\" \"${INFO_PLIST}\"\n fi\n NEW_VERSION=\"${BASE_VERSION}-${TYPE}-${HEAD}\"\n if [[ \"${NEW_VERSION}\" != \"${CURRENT_VERSION}\" ]]; then # Only touch the file if we need to.\n /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString ${NEW_VERSION}\" \"${INFO_PLIST}\"\n fi\n fi\nfi\n";
|
||||
};
|
||||
276972891CAFCE91007A2F7C /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -924,7 +1002,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
shellPath = /bin/bash;
|
||||
shellScript = "/bin/bash $SRCROOT/BGMXPCHelper/post_install.sh";
|
||||
shellScript = "/bin/bash \"$SRCROOT/BGMXPCHelper/post_install.sh\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -938,19 +1016,21 @@
|
||||
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */,
|
||||
1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */,
|
||||
1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */,
|
||||
273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */,
|
||||
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */,
|
||||
279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */,
|
||||
1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */,
|
||||
1C1962F41BCABFC5008A4DF7 /* CAHALAudioObject.cpp in Sources */,
|
||||
27F7D4901D2483B100821C4B /* BGMDecibel.m in Sources */,
|
||||
1C4D1A1D217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */,
|
||||
1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */,
|
||||
1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */,
|
||||
1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */,
|
||||
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */,
|
||||
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */,
|
||||
1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */,
|
||||
1CE7064C1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm in Sources */,
|
||||
1CE7064C1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm in Sources */,
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */,
|
||||
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */,
|
||||
@@ -967,11 +1047,14 @@
|
||||
1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */,
|
||||
2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */,
|
||||
1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */,
|
||||
1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */,
|
||||
2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */,
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */,
|
||||
1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */,
|
||||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */,
|
||||
27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */,
|
||||
1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */,
|
||||
@@ -981,6 +1064,9 @@
|
||||
2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */,
|
||||
27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */,
|
||||
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */,
|
||||
19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */,
|
||||
19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */,
|
||||
19FE72566BCEB11BD1F3D487 /* BGMMusic.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -988,9 +1074,12 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8104B022AD082E00B35517 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */,
|
||||
1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */,
|
||||
1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */,
|
||||
1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */,
|
||||
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */,
|
||||
@@ -998,6 +1087,7 @@
|
||||
1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */,
|
||||
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.m in Sources */,
|
||||
1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */,
|
||||
1C4D1A1E217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */,
|
||||
1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */,
|
||||
1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */,
|
||||
1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
@@ -1006,6 +1096,7 @@
|
||||
1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */,
|
||||
1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */,
|
||||
1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
|
||||
1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */,
|
||||
1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */,
|
||||
1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */,
|
||||
@@ -1014,7 +1105,7 @@
|
||||
1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */,
|
||||
1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */,
|
||||
1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */,
|
||||
1CD989521ECFFCFC0014BBBF /* BGMOutputDevicePrefs.mm in Sources */,
|
||||
1CD989521ECFFCFC0014BBBF /* BGMOutputDeviceMenuSection.mm in Sources */,
|
||||
1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */,
|
||||
1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */,
|
||||
1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */,
|
||||
@@ -1036,6 +1127,9 @@
|
||||
1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */,
|
||||
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */,
|
||||
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */,
|
||||
19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */,
|
||||
19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */,
|
||||
19FE7B32E1214BA0E8166A9E /* BGMMusic.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1071,9 +1165,12 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8104AF22AD07E200B35517 /* BGMAppWatcher.m in Sources */,
|
||||
1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */,
|
||||
1CEACF4D1F34793700FEC143 /* CAHALAudioDevice.cpp in Sources */,
|
||||
1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */,
|
||||
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
|
||||
27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */,
|
||||
27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */,
|
||||
@@ -1108,6 +1205,10 @@
|
||||
1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */,
|
||||
2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */,
|
||||
19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */,
|
||||
19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */,
|
||||
1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */,
|
||||
19FE76F614F260F3F65AF550 /* BGMMusic.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1186,6 +1287,7 @@
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_NS_ASSERTIONS = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -1234,16 +1336,19 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_USE_OPTIMIZATION_PROFILE = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Background Music.entitlements";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM";
|
||||
DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
INFOPLIST_FILE = BGMApp/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LLVM_LTO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_STYLE = "non-global";
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
};
|
||||
name = DebugOpt;
|
||||
@@ -1288,6 +1393,7 @@
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
@@ -1371,6 +1477,7 @@
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
@@ -1415,16 +1522,19 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_USE_OPTIMIZATION_PROFILE = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Background Music.entitlements";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM";
|
||||
DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
INFOPLIST_FILE = BGMApp/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LLVM_LTO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_STYLE = "non-global";
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1434,16 +1544,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_USE_OPTIMIZATION_PROFILE = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Background Music.entitlements";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEPLOYMENT_POSTPROCESSING = YES;
|
||||
DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM";
|
||||
DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
INFOPLIST_FILE = BGMApp/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LLVM_LTO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_STYLE = "non-global";
|
||||
WARNING_CFLAGS = (
|
||||
"-Wno-profile-instr-out-of-date",
|
||||
"-Wpartial-availability",
|
||||
@@ -1501,6 +1615,7 @@
|
||||
27379B991C7F57DB0084A24C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"CoreAudio_Debug=1",
|
||||
@@ -1519,10 +1634,12 @@
|
||||
27379B9A1C7F57DB0084A24C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEPLOYMENT_POSTPROCESSING = YES;
|
||||
INFOPLIST_FILE = BGMXPCHelper/Info.plist;
|
||||
INSTALL_GROUP = wheel;
|
||||
INSTALL_OWNER = root;
|
||||
INSTALL_PATH = /usr/local/libexec;
|
||||
LLVM_LTO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelper;
|
||||
PRODUCT_NAME = BGMXPCHelper;
|
||||
};
|
||||
@@ -1531,6 +1648,7 @@
|
||||
27379B9B1C7F57DB0084A24C /* DebugOpt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"CoreAudio_Debug=1",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -68,7 +67,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
+212
-126
@@ -17,116 +17,85 @@
|
||||
// BGMAppDelegate.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
// Self Include
|
||||
#import "BGMAppDelegate.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMOutputDeviceMenuSection.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMPreferredOutputDevices.h"
|
||||
#import "BGMStatusBarItem.h"
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
#import "BGMTermination.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "SystemPreferences.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAPropertyAddress.h"
|
||||
// System Includes
|
||||
#import <AVFoundation/AVCaptureDevice.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static float const kStatusBarIconPadding = 0.25;
|
||||
static NSString* const kOptNoPersistentData = @"--no-persistent-data";
|
||||
static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
|
||||
@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;
|
||||
// The button in the system status bar that shows the main menu.
|
||||
BGMStatusBarItem* statusBarItem;
|
||||
|
||||
// Only show the 'BGMXPCHelper is missing' error dialog once.
|
||||
BOOL haveShownXPCHelperErrorMessage;
|
||||
|
||||
|
||||
// Persistently stores user settings and data.
|
||||
BGMUserDefaults* userDefaults;
|
||||
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMAutoPauseMenuItem* autoPauseMenuItem;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
BGMSystemSoundsVolume* systemSoundsVolume;
|
||||
BGMAppVolumesController* appVolumes;
|
||||
BGMOutputDeviceMenuSection* outputDeviceMenuSection;
|
||||
BGMPreferencesMenu* prefsMenu;
|
||||
BGMXPCListener* xpcListener;
|
||||
BGMPreferredOutputDevices* preferredOutputDevices;
|
||||
}
|
||||
|
||||
@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) {
|
||||
[super 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:kOptShowDockIcon] != 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 up audioDevices, which coordinates BGMDevice and the output device. It manages
|
||||
// playthrough, volume/mute controls, etc.
|
||||
if (![self initAudioDeviceManager]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the main menu
|
||||
statusBarItem.menu = self.bgmMenu;
|
||||
|
||||
// Stored user settings
|
||||
userDefaults = [self createUserDefaults];
|
||||
|
||||
// Add the status bar item. (The thing you click to show BGMApp's main menu.)
|
||||
statusBarItem = [[BGMStatusBarItem alloc] initWithMenu:self.bgmMenu
|
||||
audioDevices:audioDevices
|
||||
userDefaults:userDefaults];
|
||||
}
|
||||
|
||||
- (void) applicationDidFinishLaunching:(NSNotification*)aNotification {
|
||||
@@ -141,79 +110,158 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
|
||||
NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]);
|
||||
|
||||
// Set up audioDevices, which coordinates BGMDevice and the output device. It manages
|
||||
// playthrough, volume/mute controls, etc.
|
||||
if (![self initAudioDeviceManager]) {
|
||||
// Handles changing (or not changing) the output device when devices are added or removed. Must
|
||||
// be initialised before calling setBGMDeviceAsDefault.
|
||||
preferredOutputDevices =
|
||||
[[BGMPreferredOutputDevices alloc] initWithDevices:audioDevices userDefaults:userDefaults];
|
||||
|
||||
// Choose an output device for BGMApp to use to play audio.
|
||||
if (![self setInitialOutputDevice]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make BGMDevice the default device.
|
||||
[self setBGMDeviceAsDefault];
|
||||
|
||||
// Handle some of the unusual reasons BGMApp might have to exit, mostly crashes.
|
||||
BGMTermination::SetUpTerminationCleanUp(audioDevices);
|
||||
|
||||
// Set up the rest of the UI and other external interfaces.
|
||||
|
||||
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];
|
||||
[self setUpMainMenu];
|
||||
|
||||
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
|
||||
helperConnectionErrorHandler:^(NSError* error) {
|
||||
NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) "
|
||||
"BGMXPCHelper connection error: %@",
|
||||
error);
|
||||
|
||||
[self showXPCHelperErrorMessage:error];
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
|
||||
- (BOOL) initAudioDeviceManager {
|
||||
audioDevices = [BGMAudioDeviceManager new];
|
||||
|
||||
if (!audioDevices) {
|
||||
[self showBGMDeviceNotFoundErrorMessageAndExit];
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
|
||||
- (BOOL) setInitialOutputDevice {
|
||||
AudioObjectID preferredDevice = [preferredOutputDevices findPreferredDevice];
|
||||
|
||||
if (preferredDevice != kAudioObjectUnknown) {
|
||||
NSError* __nullable error = [audioDevices setOutputDeviceWithID:preferredDevice
|
||||
revertOnFailure:NO];
|
||||
if (error) {
|
||||
// Show the error message.
|
||||
[self showFailedToSetOutputDeviceErrorMessage:BGMNN(error)
|
||||
preferredDevice:preferredDevice];
|
||||
}
|
||||
} else {
|
||||
// We couldn't find a device to use, so show an error message and quit.
|
||||
[self showOutputDeviceNotFoundErrorMessageAndExit];
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Sets the "Background Music" virtual audio device (BGMDevice) as the user's default audio device.
|
||||
- (void) setBGMDeviceAsDefault {
|
||||
void (^setDefaultDevice)() = ^{
|
||||
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set the Background Music device as your"
|
||||
"default audio device."
|
||||
informativeText:@"You might be able to change it yourself."];
|
||||
}
|
||||
};
|
||||
|
||||
// Skip this if we're compiling on a version of macOS before 10.14 as won't compile and it
|
||||
// isn't needed.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
|
||||
if (@available(macOS 10.14, *)) {
|
||||
// On macOS 10.14+ we need to get the user's permission to use input devices before we can
|
||||
// use BGMDevice for playthrough (see BGMPlayThrough), so we wait until they've given it
|
||||
// before making BGMDevice the default device. This way, if the user is playing audio when
|
||||
// they open Background Music, we won't interrupt it while we're waiting for them to click
|
||||
// OK.
|
||||
//
|
||||
// TODO: This isn't a perfect solution because, if the user takes too long to accept,
|
||||
// BGMPlayThrough will try to use BGMDevice again and log some errors.
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
|
||||
completionHandler:^(BOOL granted) {
|
||||
if (granted) {
|
||||
DebugMsg("BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission granted");
|
||||
setDefaultDevice();
|
||||
} else {
|
||||
NSLog(@"BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission denied");
|
||||
// TODO: If they don't accept, Background Music won't work
|
||||
// at all and the only way to fix it is in System
|
||||
// Preferences, so we should show an error dialog
|
||||
// with instructions.
|
||||
}
|
||||
}];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// We can change the device immediately on older versions of macOS because they don't
|
||||
// require user permission for input devices.
|
||||
setDefaultDevice();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setUpMainMenu {
|
||||
autoPauseMenuItem =
|
||||
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
autoPauseMusic:autoPauseMusic
|
||||
musicPlayers:musicPlayers
|
||||
userDefaults:userDefaults];
|
||||
|
||||
[self initVolumesMenuSection];
|
||||
|
||||
// Output device selection.
|
||||
outputDeviceMenuSection =
|
||||
[[BGMOutputDeviceMenuSection alloc] initWithBGMMenu:self.bgmMenu
|
||||
audioDevices:audioDevices
|
||||
preferredDevices:preferredOutputDevices];
|
||||
[audioDevices setOutputDeviceMenuSection:outputDeviceMenuSection];
|
||||
|
||||
// Preferences submenu.
|
||||
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
|
||||
audioDevices:audioDevices
|
||||
musicPlayers:musicPlayers
|
||||
statusBarItem:statusBarItem
|
||||
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;
|
||||
BOOL persistentDefaults =
|
||||
[NSProcessInfo.processInfo.arguments indexOfObject:kOptNoPersistentData] == NSNotFound;
|
||||
NSUserDefaults* wrappedDefaults = persistentDefaults ? [NSUserDefaults standardUserDefaults] : nil;
|
||||
return [[BGMUserDefaults alloc] initWithDefaults:wrappedDefaults];
|
||||
}
|
||||
|
||||
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
|
||||
- (BOOL) initAudioDeviceManager {
|
||||
NSError* error;
|
||||
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&error];
|
||||
|
||||
if (!audioDevices) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:error.code];
|
||||
return NO;
|
||||
}
|
||||
|
||||
error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set the Background Music device as your"
|
||||
"default audio device."
|
||||
informativeText:@"You might be able to set it yourself."];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) initVolumesMenuSection {
|
||||
// Create the menu item with the (main) output volume slider.
|
||||
BGMOutputVolumeMenuItem* outputVolume =
|
||||
@@ -249,6 +297,7 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
|
||||
DebugMsg("BGMAppDelegate::applicationWillTerminate");
|
||||
|
||||
// Change the user's default output device back.
|
||||
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
@@ -260,30 +309,48 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
|
||||
#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 Device.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."];
|
||||
}
|
||||
- (void) showBGMDeviceNotFoundErrorMessageAndExit {
|
||||
// BGMDevice wasn't found on the system. Most likely, BGMDriver isn't installed. Show an error
|
||||
// dialog and exit.
|
||||
//
|
||||
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL? Might even want to
|
||||
// offer to install them if not.
|
||||
[self showErrorMessage:@"Could not find the Background Music virtual audio device."
|
||||
informativeText:@"Make sure you've installed Background Music Device.driver to "
|
||||
"/Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo "
|
||||
"killall coreaudiod\")."
|
||||
exitAfterMessageDismissed:YES];
|
||||
}
|
||||
|
||||
- (void) showFailedToSetOutputDeviceErrorMessage:(NSError*)error
|
||||
preferredDevice:(BGMAudioDevice)device {
|
||||
NSLog(@"Failed to set initial output device. Error: %@", error);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert alertWithError:BGMNN(error)];
|
||||
alert.messageText = @"Failed to set the output device.";
|
||||
|
||||
NSString* __nullable name = nil;
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
name = (__bridge NSString* __nullable)device.CopyName();
|
||||
});
|
||||
|
||||
alert.informativeText =
|
||||
[NSString stringWithFormat:@"Could not start the device '%@'. (Error: %ld)",
|
||||
name, error.code];
|
||||
|
||||
// This crashes if built with Xcode 9.0.1, but works with versions of Xcode before 9 and
|
||||
// with 9.1.
|
||||
[alert runModal];
|
||||
[NSApp terminate:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showOutputDeviceNotFoundErrorMessageAndExit {
|
||||
// We couldn't find any output devices. Show an error dialog and exit.
|
||||
[self showErrorMessage:@"Could not find an audio output device."
|
||||
informativeText:@"If you do have one installed, this is probably a bug. Sorry about "
|
||||
"that. Feel free to file an issue on GitHub."
|
||||
exitAfterMessageDismissed:YES];
|
||||
}
|
||||
|
||||
- (void) showXPCHelperErrorMessage:(NSError*)error {
|
||||
if (!haveShownXPCHelperErrorMessage) {
|
||||
haveShownXPCHelperErrorMessage = YES;
|
||||
@@ -307,6 +374,25 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showErrorMessage:(NSString*)message
|
||||
informativeText:(NSString*)informativeText
|
||||
exitAfterMessageDismissed:(BOOL)fatal {
|
||||
// NSAlert should only be used on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert* alert = [NSAlert new];
|
||||
[alert setMessageText:message];
|
||||
[alert setInformativeText:informativeText];
|
||||
|
||||
// This crashes if built with Xcode 9.0.1, but works with versions of Xcode before 9 and
|
||||
// with 9.1.
|
||||
[alert runModal];
|
||||
|
||||
if (fatal) {
|
||||
[NSApp terminate:self];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showSetDeviceAsDefaultError:(NSError*)error
|
||||
message:(NSString*)msg
|
||||
informativeText:(NSString*)info {
|
||||
|
||||
@@ -21,18 +21,19 @@
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppVolumes : NSObject
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)audioDevices;
|
||||
- (id) initWithController:(BGMAppVolumesController*)inController
|
||||
bgmMenu:(NSMenu*)inMenu
|
||||
appVolumeView:(NSView*)inView;
|
||||
|
||||
// Pass -1 for initialVolume or initialPan to leave the volume/pan at its default level.
|
||||
- (void) insertMenuItemForApp:(NSRunningApplication*)app
|
||||
@@ -51,6 +52,7 @@
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)item;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAppVolumes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
@@ -38,27 +38,27 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
|
||||
static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
@implementation BGMAppVolumes {
|
||||
BGMAppVolumesController* controller;
|
||||
|
||||
NSMenu* bgmMenu;
|
||||
NSMenu* moreAppsMenu;
|
||||
|
||||
NSView* appVolumeView;
|
||||
CGFloat appVolumeViewFullHeight;
|
||||
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
|
||||
// The number of menu items this class has added to bgmMenu. Doesn't include the More Apps menu.
|
||||
NSInteger numMenuItems;
|
||||
}
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)devices {
|
||||
- (id) initWithController:(BGMAppVolumesController*)inController
|
||||
bgmMenu:(NSMenu*)inMenu
|
||||
appVolumeView:(NSView*)inView {
|
||||
if ((self = [super init])) {
|
||||
bgmMenu = menu;
|
||||
controller = inController;
|
||||
bgmMenu = inMenu;
|
||||
moreAppsMenu = [[NSMenu alloc] initWithTitle:kMoreAppsMenuTitle];
|
||||
appVolumeView = view;
|
||||
appVolumeView = inView;
|
||||
appVolumeViewFullHeight = appVolumeView.frame.size.height;
|
||||
audioDevices = devices;
|
||||
numMenuItems = 0;
|
||||
|
||||
// Add the More Apps menu to the main menu.
|
||||
@@ -81,12 +81,6 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
return self;
|
||||
}
|
||||
|
||||
// This method allows the Interface Builder Custom Classes for controls (below) to send their values
|
||||
// directly to BGMDevice. Not public to other classes.
|
||||
- (BGMAudioDeviceManager*) audioDevices {
|
||||
return audioDevices;
|
||||
}
|
||||
|
||||
#pragma mark UI Modifications
|
||||
|
||||
- (void) insertMenuItemForApp:(NSRunningApplication*)app
|
||||
@@ -99,6 +93,7 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) {
|
||||
[(NSView<BGMAppVolumeMenuItemSubview>*)subview setUpWithApp:app
|
||||
context:self
|
||||
controller:controller
|
||||
menuItem:appVolItem];
|
||||
}
|
||||
}
|
||||
@@ -276,8 +271,11 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
@implementation BGMAVM_AppIcon
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, ctrl, menuItem)
|
||||
|
||||
self.image = app.icon;
|
||||
|
||||
@@ -296,8 +294,11 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
@implementation BGMAVM_AppNameLabel
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, ctrl, menuItem)
|
||||
|
||||
NSString* name = app.localizedName ? (NSString*)app.localizedName : @"";
|
||||
self.stringValue = name;
|
||||
@@ -307,8 +308,11 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
@implementation BGMAVM_ShowMoreControlsButton
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (app)
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (app, ctrl)
|
||||
|
||||
// Set up the button that show/hide the extra controls (currently only a pan slider) for the app.
|
||||
self.cell.representedObject = menuItem;
|
||||
@@ -336,13 +340,16 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* __nullable appBundleID;
|
||||
BGMAppVolumes* context;
|
||||
BGMAppVolumesController* controller;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (menuItem)
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
context = ctx;
|
||||
controller = ctrl;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appVolumeChanged);
|
||||
@@ -388,9 +395,7 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
// The values from our sliders are in
|
||||
// [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already.
|
||||
[context.audioDevices setVolume:self.intValue
|
||||
forAppWithProcessID:appProcessID
|
||||
bundleID:appBundleID];
|
||||
[controller setVolume:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -399,13 +404,16 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* __nullable appBundleID;
|
||||
BGMAppVolumes* context;
|
||||
BGMAppVolumesController* controller;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (menuItem)
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
context = ctx;
|
||||
controller = ctrl;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appPanPositionChanged);
|
||||
@@ -434,9 +442,7 @@ static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue);
|
||||
|
||||
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already.
|
||||
[context.audioDevices setPanPosition:self.intValue
|
||||
forAppWithProcessID:appProcessID
|
||||
bundleID:appBundleID];
|
||||
[controller setPanPosition:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -35,6 +35,16 @@
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)audioDevices;
|
||||
|
||||
// See BGMBackgroundMusicDevice::SetAppVolume.
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
// See BGMBackgroundMusicDevice::SetPanVolume.
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAppVolumesController.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
// Copyright © 2017, 2018 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
#import "CACFDictionary.h"
|
||||
#import "CACFString.h"
|
||||
|
||||
// System Includes
|
||||
#include <libproc.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@@ -48,14 +51,16 @@ typedef struct BGMAppVolumeAndPan {
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
}
|
||||
|
||||
#pragma mark Initialisation
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)devices {
|
||||
if ((self = [super init])) {
|
||||
audioDevices = devices;
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithMenu:menu
|
||||
appVolumeView:view
|
||||
audioDevices:devices];
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithController:self
|
||||
bgmMenu:menu
|
||||
appVolumeView:view];
|
||||
|
||||
// Create the menu items for controlling app volumes.
|
||||
NSArray<NSRunningApplication*>* apps = [[NSWorkspace sharedWorkspace] runningApplications];
|
||||
@@ -150,6 +155,55 @@ typedef struct BGMAppVolumeAndPan {
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
// Update the app's volume.
|
||||
audioDevices.bgmDevice.SetAppVolume(volume, processID, (__bridge_retained CFStringRef)bundleID);
|
||||
|
||||
// If this volume is for FaceTime, set the volume for the avconferenced process as well. This
|
||||
// works around FaceTime not playing its own audio. It plays UI sounds through
|
||||
// systemsoundserverd and call audio through avconferenced.
|
||||
//
|
||||
// This isn't ideal because other apps might play audio through avconferenced, but I don't see a
|
||||
// good way we could find out which app is actually playing the audio. We could probably figure
|
||||
// it out from reading avconferenced's logs, at least, if it turns out to be important. See
|
||||
// https://github.com/kyleneideck/BackgroundMusic/issues/139.
|
||||
if ([bundleID isEqual:@"com.apple.FaceTime"]) {
|
||||
[self setAvconferencedVolume:volume];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setAvconferencedVolume:(SInt32)volume {
|
||||
// TODO: This volume will be lost if avconferenced is restarted.
|
||||
pid_t pids[1024];
|
||||
size_t procCount = proc_listallpids(pids, 1024);
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
|
||||
for (int i = 0; i < procCount; i++) {
|
||||
pid_t pid = pids[i];
|
||||
|
||||
if (proc_pidpath(pid, path, sizeof(path)) > 0 &&
|
||||
strncmp(path, "/usr/libexec/avconferenced", sizeof(path)) == 0) {
|
||||
DebugMsg("Setting avconferenced volume: %d", volume);
|
||||
audioDevices.bgmDevice.SetAppVolume(volume, pid, nullptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LogWarning("Failed to set avconferenced volume.");
|
||||
}
|
||||
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
audioDevices.bgmDevice.SetAppPanPosition(pan,
|
||||
processID,
|
||||
(__bridge_retained CFStringRef)bundleID);
|
||||
}
|
||||
|
||||
#pragma mark KVO
|
||||
|
||||
- (void) observeValueForKeyPath:(NSString* __nullable)keyPath
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMAppWatcher.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
// Calls callback functions when a given application is launched or terminated. Starts watching
|
||||
// after being initialised, stops after being destroyed.
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppWatcher : NSObject
|
||||
|
||||
// appLaunched will be called when the application is launched and appTerminated will be called when
|
||||
// it's terminated. Background apps, status bar apps, etc. are ignored.
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID
|
||||
appLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated;
|
||||
|
||||
// With this constructor, when an application is launched or terminated, isMatchingBundleID will be
|
||||
// called first to decide whether or not the callback should be called.
|
||||
- (instancetype) initWithAppLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated
|
||||
isMatchingBundleID:(BOOL(^)(NSString* appBundleID))isMatchingBundleID;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMAppWatcher.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMAppWatcher {
|
||||
// Tokens for the notification observers so we can remove them in dealloc.
|
||||
id<NSObject> didLaunchToken;
|
||||
id<NSObject> didTerminateToken;
|
||||
}
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID
|
||||
appLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated {
|
||||
return [self initWithAppLaunched:appLaunched
|
||||
appTerminated:appTerminated
|
||||
isMatchingBundleID:^BOOL(NSString* appBundleID) {
|
||||
return [bundleID isEqualToString:appBundleID];
|
||||
}];
|
||||
}
|
||||
|
||||
- (instancetype) initWithAppLaunched:(void(^)(void))appLaunched
|
||||
appTerminated:(void(^)(void))appTerminated
|
||||
isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
didLaunchToken =
|
||||
[center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* notification) {
|
||||
if ([BGMAppWatcher shouldBeHandled:notification
|
||||
isMatchingBundleID:isMatchingBundleID]) {
|
||||
appLaunched();
|
||||
}
|
||||
}];
|
||||
|
||||
didTerminateToken =
|
||||
[center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* notification) {
|
||||
if ([BGMAppWatcher shouldBeHandled:notification
|
||||
isMatchingBundleID:isMatchingBundleID]) {
|
||||
appTerminated();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Returns YES if we should call the app launch/termination callback for this NSNotification.
|
||||
+ (BOOL) shouldBeHandled:(NSNotification*)notification
|
||||
isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID {
|
||||
NSString* __nullable notifiedBundleID =
|
||||
[notification.userInfo[NSWorkspaceApplicationKey] bundleIdentifier];
|
||||
|
||||
// Ignore the notification if the app doesn't have a bundle ID or isMatchingBundleID returns NO.
|
||||
return notifiedBundleID && isMatchingBundleID((NSString*)notifiedBundleID);
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
didLaunchToken = nil;
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
didTerminateToken = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAudioDeviceManager.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
// Manages 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.
|
||||
@@ -39,21 +39,25 @@
|
||||
|
||||
// Forward Declarations
|
||||
@class BGMOutputVolumeMenuItem;
|
||||
@class BGMOutputDeviceMenuSection;
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static const int kBGMErrorCode_BGMDeviceNotFound = 1;
|
||||
static const int kBGMErrorCode_OutputDeviceNotFound = 2;
|
||||
static const int kBGMErrorCode_ReturningEarly = 3;
|
||||
static const int kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
static const int kBGMErrorCode_ReturningEarly = 2;
|
||||
|
||||
@interface BGMAudioDeviceManager : NSObject
|
||||
|
||||
- (instancetype) initWithError:(NSError**)error;
|
||||
// Returns nil if BGMDevice isn't installed.
|
||||
- (instancetype) init;
|
||||
|
||||
// Set the BGMOutputVolumeMenuItem to be notified when the output device is changed.
|
||||
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item;
|
||||
|
||||
// Set the BGMOutputDeviceMenuSection to be notified when the output device is changed.
|
||||
- (void) setOutputDeviceMenuSection:(BGMOutputDeviceMenuSection*)menuSection;
|
||||
|
||||
// Set BGMDevice as the default audio device for all processes
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
||||
// Replace BGMDevice as the default device with the output device
|
||||
@@ -68,13 +72,6 @@ static const int kBGMErrorCode_ReturningEarly = 3;
|
||||
- (CAHALAudioDevice) outputDevice;
|
||||
#endif
|
||||
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
- (BOOL) isOutputDevice:(AudioObjectID)deviceID;
|
||||
- (BOOL) isOutputDataSource:(UInt32)dataSourceID;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAudioDeviceManager.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -26,21 +26,31 @@
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMDeviceControlSync.h"
|
||||
#import "BGMPlayThrough.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
#import "BGMXPCProtocols.h"
|
||||
#import "BGMDeviceControlSync.h"
|
||||
#import "BGMOutputDeviceMenuSection.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMPlayThrough.h"
|
||||
#import "BGMXPCProtocols.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAAtomic.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMAudioDeviceManager {
|
||||
BGMBackgroundMusicDevice bgmDevice;
|
||||
// This ivar is a pointer so that BGMBackgroundMusicDevice's constructor doesn't get called
|
||||
// during [BGMAudioDeviceManager alloc] when the ivars are initialised. It queries the HAL for
|
||||
// BGMDevice's AudioObject ID, which might throw a CAException, most likely because BGMDevice
|
||||
// isn't installed.
|
||||
//
|
||||
// That would be the only way for [BGMAudioDeviceManager alloc] to throw a CAException, so we
|
||||
// could wrap that call in a try/catch block instead, but it would make the code a bit
|
||||
// confusing.
|
||||
BGMBackgroundMusicDevice* bgmDevice;
|
||||
BGMAudioDevice outputDevice;
|
||||
|
||||
BGMDeviceControlSync deviceControlSync;
|
||||
@@ -51,46 +61,25 @@
|
||||
NSXPCConnection* __nullable bgmXPCHelperConnection;
|
||||
|
||||
BGMOutputVolumeMenuItem* __nullable outputVolumeMenuItem;
|
||||
BGMOutputDeviceMenuSection* __nullable outputDeviceMenuSection;
|
||||
|
||||
NSRecursiveLock* stateLock;
|
||||
}
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
- (instancetype) initWithError:(NSError** __nullable)error {
|
||||
- (instancetype) init {
|
||||
if ((self = [super init])) {
|
||||
stateLock = [NSRecursiveLock new];
|
||||
bgmXPCHelperConnection = nil;
|
||||
outputVolumeMenuItem = nil;
|
||||
outputDeviceMenuSection = nil;
|
||||
outputDevice = kAudioObjectUnknown;
|
||||
|
||||
try {
|
||||
bgmDevice = BGMBackgroundMusicDevice();
|
||||
bgmDevice = new BGMBackgroundMusicDevice;
|
||||
} catch (const CAException& e) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found. (%d)", e.GetError());
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
try {
|
||||
[self initOutputDevice];
|
||||
} catch (const CAException& e) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: failed to init output device (%d)",
|
||||
e.GetError());
|
||||
outputDevice.SetObjectID(kAudioObjectUnknown);
|
||||
}
|
||||
|
||||
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: output device not found");
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
LogError("BGMAudioDeviceManager::init: BGMDevice not found. (%d)", e.GetError());
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
@@ -99,83 +88,16 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
// Throws a CAException if it fails to set the output device.
|
||||
- (void) initOutputDevice {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
|
||||
BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
- (void) dealloc {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
if (defaultDevice.IsBGMDeviceInstance()) {
|
||||
// BGMDevice is already the default (it could have been set manually or BGMApp could have
|
||||
// failed to change it back the last time it closed), so just pick the device with the
|
||||
// lowest latency.
|
||||
//
|
||||
// TODO: Temporarily disable BGMDevice so we can find out what the previous default was and
|
||||
// use that instead.
|
||||
[self setOutputDeviceByLatency];
|
||||
} else {
|
||||
// TODO: Return the error from setOutputDeviceWithID so it can be returned by initWithError.
|
||||
[self setOutputDeviceWithID:defaultDevice revertOnFailure:NO];
|
||||
}
|
||||
|
||||
if (outputDevice == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initOutputDevice: Failed to set output device");
|
||||
Throw(CAException(kAudioHardwareUnspecifiedError));
|
||||
}
|
||||
|
||||
if (outputDevice.IsBGMDeviceInstance()) {
|
||||
LogError("BGMAudioDeviceManager::initOutputDevice: Failed to change output device from "
|
||||
"BGMDevice");
|
||||
Throw(CAException(kAudioHardwareUnspecifiedError));
|
||||
}
|
||||
|
||||
// Log message
|
||||
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
|
||||
DebugMsg("BGMAudioDeviceManager::initOutputDevice: Set output device to %s",
|
||||
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
|
||||
CFRelease(outputDeviceUID);
|
||||
}
|
||||
|
||||
- (void) setOutputDeviceByLatency {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
|
||||
|
||||
if (numDevices > 0) {
|
||||
BGMAudioDevice minLatencyDevice = kAudioObjectUnknown;
|
||||
UInt32 minLatency = UINT32_MAX;
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
BGMAudioDevice device(devices[i]);
|
||||
|
||||
if (!device.IsBGMDeviceInstance()) {
|
||||
BOOL hasOutputChannels = NO;
|
||||
|
||||
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::setOutputDeviceByLatency",
|
||||
"GetTotalNumberChannels", ([&] {
|
||||
hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
|
||||
}));
|
||||
|
||||
if (hasOutputChannels) {
|
||||
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::setOutputDeviceByLatency",
|
||||
"GetLatency", ([&] {
|
||||
UInt32 latency = device.GetLatency(false);
|
||||
|
||||
if (latency < minLatency) {
|
||||
minLatencyDevice = devices[i];
|
||||
minLatency = latency;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minLatencyDevice != kAudioObjectUnknown) {
|
||||
// TODO: On error, try a different output device.
|
||||
[self setOutputDeviceWithID:minLatencyDevice revertOnFailure:NO];
|
||||
if (bgmDevice) {
|
||||
delete bgmDevice;
|
||||
bgmDevice = nullptr;
|
||||
}
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,26 +105,23 @@
|
||||
outputVolumeMenuItem = item;
|
||||
}
|
||||
|
||||
- (void) setOutputDeviceMenuSection:(BGMOutputDeviceMenuSection*)menuSection {
|
||||
outputDeviceMenuSection = menuSection;
|
||||
}
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
// Note that there are two different "default" output devices on OS X: "output" and "system output". See
|
||||
// kAudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h.
|
||||
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault {
|
||||
// Copy bgmDevice so we can call the HAL without holding stateLock. See startPlayThroughSync.
|
||||
BGMBackgroundMusicDevice bgmDev;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
bgmDev = bgmDevice;
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
try {
|
||||
bgmDev.SetAsOSDefault();
|
||||
// Intentionally avoid taking stateLock before making calls to the HAL. See
|
||||
// startPlayThroughSync.
|
||||
CAMemoryBarrier();
|
||||
bgmDevice->SetAsOSDefault();
|
||||
} catch (const CAException& e) {
|
||||
NSLog(@"SetAsOSDefault threw CAException (%d)", e.GetError());
|
||||
BGMLogExceptionIn("BGMAudioDeviceManager::setBGMDeviceAsOSDefault", e);
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
}
|
||||
|
||||
@@ -211,25 +130,25 @@
|
||||
|
||||
- (NSError* __nullable) unsetBGMDeviceAsOSDefault {
|
||||
// Copy the devices so we can call the HAL without holding stateLock. See startPlayThroughSync.
|
||||
BGMBackgroundMusicDevice* bgmDeviceCopy;
|
||||
AudioDeviceID outputDeviceID;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
bgmDeviceCopy = bgmDevice;
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID
|
||||
code:kBGMErrorCode_OutputDeviceNotFound
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
try {
|
||||
BGMBackgroundMusicDevice bgmDev;
|
||||
AudioDeviceID outputDeviceID;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
bgmDev = bgmDevice;
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID
|
||||
code:kBGMErrorCode_OutputDeviceNotFound
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
bgmDev.UnsetAsOSDefault(outputDeviceID);
|
||||
bgmDeviceCopy->UnsetAsOSDefault(outputDeviceID);
|
||||
} catch (const CAException& e) {
|
||||
BGMLogExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault", e);
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
@@ -241,25 +160,13 @@
|
||||
#pragma mark Accessors
|
||||
|
||||
- (BGMBackgroundMusicDevice) bgmDevice {
|
||||
return bgmDevice;
|
||||
return *bgmDevice;
|
||||
}
|
||||
|
||||
- (CAHALAudioDevice) outputDevice {
|
||||
return outputDevice;
|
||||
}
|
||||
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
bgmDevice.SetAppVolume(volume, processID, (__bridge_retained CFStringRef)bundleID);
|
||||
}
|
||||
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
bgmDevice.SetAppPanPosition(pan, processID, (__bridge_retained CFStringRef)bundleID);
|
||||
}
|
||||
|
||||
- (BOOL) isOutputDevice:(AudioObjectID)deviceID {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
@@ -315,40 +222,16 @@ forAppWithProcessID:(pid_t)processID
|
||||
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Setting output device. newDeviceID=%u",
|
||||
newDeviceID);
|
||||
|
||||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (GetObjectID doesn't throw.)
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
|
||||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
|
||||
|
||||
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();
|
||||
|
||||
if (newDeviceID != currentDeviceID) {
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
|
||||
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();
|
||||
playThrough_UISounds.Start();
|
||||
// But stop playthrough if audio isn't playing, since it uses CPU.
|
||||
playThrough.StopIfIdle();
|
||||
playThrough_UISounds.StopIfIdle();
|
||||
}
|
||||
} catch (CAException e) {
|
||||
[self setOutputDeviceWithIDImpl:newDeviceID
|
||||
dataSourceID:dataSourceID
|
||||
currentDeviceID:currentDeviceID];
|
||||
} catch (const CAException& e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
@@ -361,6 +244,7 @@ forAppWithProcessID:(pid_t)processID
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
|
||||
// Tell other classes and BGMXPCHelper that we changed the output device.
|
||||
[self propagateOutputDeviceChange];
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
@@ -369,6 +253,41 @@ forAppWithProcessID:(pid_t)processID
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Throws CAException.
|
||||
- (void) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
|
||||
dataSourceID:(UInt32* __nullable)dataSourceID
|
||||
currentDeviceID:(AudioObjectID)currentDeviceID {
|
||||
if (newDeviceID != currentDeviceID) {
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
|
||||
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();
|
||||
playThrough_UISounds.Start();
|
||||
// But stop playthrough if audio isn't playing, since it uses CPU.
|
||||
playThrough.StopIfIdle();
|
||||
playThrough_UISounds.StopIfIdle();
|
||||
}
|
||||
|
||||
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
|
||||
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Set output device to %s (%d)",
|
||||
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8),
|
||||
outputDevice.GetObjectID());
|
||||
CFRelease(outputDeviceUID);
|
||||
}
|
||||
|
||||
// Changes the output device that playthrough plays audio to and that BGMDevice's controls are
|
||||
// kept in sync with. Throws CAException.
|
||||
- (void) setOutputDeviceForPlaythroughAndControlSync:(const BGMAudioDevice&)newOutputDevice {
|
||||
@@ -377,23 +296,23 @@ forAppWithProcessID:(pid_t)processID
|
||||
playThrough.Deactivate();
|
||||
playThrough_UISounds.Deactivate();
|
||||
|
||||
deviceControlSync.SetDevices(bgmDevice, newOutputDevice);
|
||||
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.SetDevices(bgmDevice, &newOutputDevice);
|
||||
playThrough.Activate();
|
||||
|
||||
// TODO: Support setting different devices as the default output device and the default system
|
||||
// output device the way OS X does?
|
||||
BGMAudioDevice uiSoundsDevice = bgmDevice.GetUISoundsBGMDeviceInstance();
|
||||
BGMAudioDevice uiSoundsDevice = bgmDevice->GetUISoundsBGMDeviceInstance();
|
||||
playThrough_UISounds.SetDevices(&uiSoundsDevice, &newOutputDevice);
|
||||
playThrough_UISounds.Activate();
|
||||
}
|
||||
|
||||
- (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice&)device {
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", [&] {
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", ([&] {
|
||||
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
|
||||
UInt32 channel = 0;
|
||||
|
||||
@@ -403,7 +322,7 @@ forAppWithProcessID:(pid_t)processID
|
||||
|
||||
device.SetCurrentDataSourceByID(scope, channel, dataSourceID);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) propagateOutputDeviceChange {
|
||||
@@ -412,12 +331,14 @@ forAppWithProcessID:(pid_t)processID
|
||||
|
||||
// Update the menu item for the volume of the output device.
|
||||
[outputVolumeMenuItem outputDeviceDidChange];
|
||||
[outputDeviceMenuSection outputDeviceDidChange];
|
||||
}
|
||||
|
||||
- (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.
|
||||
// TODO: Possibly caused by a bug in CADebugMacros.cpp. See commit ab9d4cd.
|
||||
NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Couldn't set device with ID %u as output device. "
|
||||
"%s%d. %@",
|
||||
deviceID,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAutoPauseMenuItem.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -41,7 +41,7 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
||||
NSMenuItem* menuItem;
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
id<NSObject> didLaunchToken, didTerminateToken;
|
||||
BGMAppWatcher* appWatcher;
|
||||
}
|
||||
|
||||
- (instancetype) initWithMenuItem:(NSMenuItem*)item
|
||||
@@ -66,53 +66,39 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
||||
// Toggle auto-pause when the menu item is clicked.
|
||||
menuItem.target = self;
|
||||
menuItem.action = @selector(toggleAutoPauseMusic);
|
||||
|
||||
[self updateMenuItemTitle];
|
||||
[self initMusicPlayerObservers];
|
||||
|
||||
[self initMenuItemTitle];
|
||||
}
|
||||
|
||||
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) initMenuItemTitle {
|
||||
// Set the initial text, tool-tip, state, etc.
|
||||
[self updateMenuItemTitle];
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
}
|
||||
// Avoid retain cycles in case we ever want to destroy instances of this class.
|
||||
BGMAutoPauseMenuItem* __weak weakSelf = self;
|
||||
|
||||
// Add a callback that enables/disables the Auto-pause Music menu item when the music player
|
||||
// is launched/terminated.
|
||||
void (^callback)(void) = ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kMenuItemUpdateWaitTime * NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
BGMAutoPauseMenuItem* strongSelf = weakSelf;
|
||||
[strongSelf updateMenuItemTitle];
|
||||
});
|
||||
};
|
||||
|
||||
appWatcher = [[BGMAppWatcher alloc] initWithAppLaunched:callback
|
||||
appTerminated:callback
|
||||
isMatchingBundleID:^BOOL(NSString* appBundleID) {
|
||||
BGMAutoPauseMenuItem* strongSelf = weakSelf;
|
||||
NSString* __nullable playerBundleID =
|
||||
strongSelf->musicPlayers.selectedMusicPlayer.bundleID;
|
||||
return playerBundleID && [appBundleID isEqualToString:(NSString*)playerBundleID];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) toggleAutoPauseMusic {
|
||||
@@ -143,7 +129,7 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
|
||||
//
|
||||
// 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) {
|
||||
if (musicPlayers.selectedMusicPlayer.running) {
|
||||
menuItem.attributedTitle = nil;
|
||||
menuItem.toolTip = nil;
|
||||
} else {
|
||||
|
||||
@@ -49,6 +49,7 @@ BGMBackgroundMusicDevice::BGMBackgroundMusicDevice()
|
||||
{
|
||||
if((GetObjectID() == kAudioObjectUnknown) || (mUISoundsBGMDevice == kAudioObjectUnknown))
|
||||
{
|
||||
LogError("BGMBackgroundMusicDevice::BGMBackgroundMusicDevice: Error getting BGMDevice ID");
|
||||
Throw(CAException(kAudioHardwareIllegalOperationError));
|
||||
}
|
||||
};
|
||||
@@ -131,7 +132,7 @@ CFArrayRef BGMBackgroundMusicDevice::GetAppVolumes() const
|
||||
|
||||
void BGMBackgroundMusicDevice::SetAppVolume(SInt32 inVolume,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef inAppBundleID)
|
||||
CFStringRef __nullable inAppBundleID)
|
||||
{
|
||||
BGMAssert((kAppRelativeVolumeMinRawValue <= inVolume) &&
|
||||
(inVolume <= kAppRelativeVolumeMaxRawValue),
|
||||
@@ -234,7 +235,11 @@ BGMBackgroundMusicDevice::ResponsibleBundleIDsOf(CACFString inParentBundleID)
|
||||
{ "com.parallels.desktop.console", { "com.parallels.vm" } },
|
||||
// MPlayer OSX Extended
|
||||
{ "hu.mplayerhq.mplayerosx.extended",
|
||||
{ "ch.sttz.mplayerosx.extended.binaries.officialsvn" } }
|
||||
{ "ch.sttz.mplayerosx.extended.binaries.officialsvn" } },
|
||||
// Discord
|
||||
{ "com.hnc.Discord", { "com.hnc.Discord.helper" } },
|
||||
// Skype
|
||||
{ "com.skype.skype", { "com.skype.skype.Helper" } }
|
||||
};
|
||||
|
||||
// Parallels' VM "dock helper" apps have bundle IDs like
|
||||
|
||||
@@ -103,7 +103,7 @@ public:
|
||||
*/
|
||||
void SetAppVolume(SInt32 inVolume,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef inAppBundleID);
|
||||
CFStringRef __nullable inAppBundleID);
|
||||
/*!
|
||||
@param inPanPosition A value between kAppPanLeftRawValue and kAppPanRightRawValue from
|
||||
BGM_Types.h. A negative value has a higher proportion of left channel, and
|
||||
@@ -150,7 +150,7 @@ public:
|
||||
@throws CAException If the HAL returns an error or an invalid PID when queried.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerProcessID in BGM_Types.h.
|
||||
*/
|
||||
pid_t GetMusicPlayerProcessID() const;
|
||||
virtual pid_t GetMusicPlayerProcessID() const;
|
||||
/*!
|
||||
Set the value of BGMDevice's property for the selected music player's process ID. Pass zero to
|
||||
unset the property. Setting this property will unset the bundle ID version of the property.
|
||||
@@ -158,7 +158,7 @@ public:
|
||||
@throws CAException If the HAL returns an error.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerProcessID in BGM_Types.h.
|
||||
*/
|
||||
void SetMusicPlayerProcessID(CFNumberRef inProcessID) {
|
||||
virtual void SetMusicPlayerProcessID(CFNumberRef inProcessID) {
|
||||
SetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress, inProcessID); }
|
||||
/*!
|
||||
@return The value of BGMDevice's property for the selected music player's bundle ID. The empty
|
||||
@@ -166,7 +166,7 @@ public:
|
||||
@throws CAException If the HAL returns an error or an invalid bundle ID when queried.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerBundleID in BGM_Types.h.
|
||||
*/
|
||||
CFStringRef GetMusicPlayerBundleID() const;
|
||||
virtual CFStringRef GetMusicPlayerBundleID() const;
|
||||
/*!
|
||||
Set the value of BGMDevice's property for the selected music player's bundle ID. Pass the empty
|
||||
string to unset the property. Setting this property will unset the process ID version of the
|
||||
@@ -175,7 +175,7 @@ public:
|
||||
@throws CAException If the HAL returns an error.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerBundleID in BGM_Types.h.
|
||||
*/
|
||||
void SetMusicPlayerBundleID(CFStringRef inBundleID) {
|
||||
virtual void SetMusicPlayerBundleID(CFStringRef inBundleID) {
|
||||
SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress, inBundleID); }
|
||||
|
||||
#pragma mark UI Sounds Instance
|
||||
|
||||
@@ -68,11 +68,15 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
return;
|
||||
}
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::~BGMDeviceControlsList", [&] {
|
||||
mAudioSystem.RemovePropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
mListenerBlock);
|
||||
});
|
||||
if(mListenerQueue && mListenerBlock)
|
||||
{
|
||||
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)
|
||||
@@ -89,7 +93,7 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
// 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);
|
||||
long timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
if(timedOut)
|
||||
@@ -104,8 +108,15 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
DestroyBlock(mDeviceToggleBackBlock);
|
||||
DestroyBlock(mDisableNullDeviceBlock);
|
||||
|
||||
Block_release(mListenerBlock);
|
||||
dispatch_release(mListenerQueue);
|
||||
if(mListenerBlock)
|
||||
{
|
||||
Block_release(mListenerBlock);
|
||||
}
|
||||
|
||||
if(mListenerQueue)
|
||||
{
|
||||
dispatch_release(BGM_Utils::NN(mListenerQueue));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
@@ -294,10 +305,6 @@ void BGMDeviceControlsList::InitDeviceToggling()
|
||||
"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
|
||||
@@ -335,9 +342,13 @@ void BGMDeviceControlsList::InitDeviceToggling()
|
||||
// seems to cause problems with some programs. Not sure why.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceInitialDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBlock);
|
||||
if(mDeviceToggleBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
kToggleDeviceInitialDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
BGM_Utils::NN(mDeviceToggleBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
break;
|
||||
@@ -389,9 +400,12 @@ void BGMDeviceControlsList::ToggleDefaultDevice()
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBackBlock);
|
||||
if(mDeviceToggleBackBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
BGM_Utils::NN(mDeviceToggleBackBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
@@ -415,11 +429,11 @@ void BGMDeviceControlsList::SetNullDeviceEnabled(bool inEnabled)
|
||||
(inEnabled ? kCFBooleanTrue : kCFBooleanFalse));
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
dispatch_block_t __nullable toggleBlock = dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
@@ -431,13 +445,22 @@ dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if(!toggleBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDeviceToggleBlock: !toggleBlock");
|
||||
}
|
||||
|
||||
return toggleBlock;
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
dispatch_block_t __nullable toggleBackBlock =
|
||||
dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
@@ -461,18 +484,30 @@ dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
mDisableNullDeviceBlock);
|
||||
if(mDisableNullDeviceBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
BGM_Utils::NN(mDisableNullDeviceBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
});
|
||||
|
||||
if(!toggleBackBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDeviceToggleBackBlock: !toggleBackBlock");
|
||||
}
|
||||
|
||||
return toggleBackBlock;
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
dispatch_block_t __nullable disableNullDeviceBlock =
|
||||
dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
@@ -492,6 +527,14 @@ dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
|
||||
BGMAssert(mBGMDevice.IsBGMDevice(), "BGMDevice's AudioObjectID changed");
|
||||
});
|
||||
|
||||
if(!disableNullDeviceBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDisableNullDeviceBlock: !disableNullDeviceBlock");
|
||||
}
|
||||
|
||||
return disableNullDeviceBlock;
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
|
||||
|
||||
@@ -101,9 +101,9 @@ private:
|
||||
*/
|
||||
void SetNullDeviceEnabled(bool inEnabled);
|
||||
|
||||
dispatch_block_t CreateDeviceToggleBlock();
|
||||
dispatch_block_t CreateDeviceToggleBackBlock();
|
||||
dispatch_block_t CreateDisableNullDeviceBlock();
|
||||
dispatch_block_t __nullable CreateDeviceToggleBlock();
|
||||
dispatch_block_t __nullable CreateDeviceToggleBackBlock();
|
||||
dispatch_block_t __nullable CreateDisableNullDeviceBlock();
|
||||
|
||||
void DestroyBlock(dispatch_block_t __nullable & block);
|
||||
|
||||
@@ -122,12 +122,13 @@ private:
|
||||
};
|
||||
BGMDeviceControlsList::ToggleState mDeviceToggleState = ToggleState::NotToggling;
|
||||
|
||||
dispatch_block_t mDeviceToggleBlock;
|
||||
dispatch_block_t mDeviceToggleBackBlock;
|
||||
dispatch_block_t mDisableNullDeviceBlock;
|
||||
dispatch_block_t __nullable mDeviceToggleBlock = nullptr;
|
||||
dispatch_block_t __nullable mDeviceToggleBackBlock = nullptr;
|
||||
dispatch_block_t __nullable mDisableNullDeviceBlock = nullptr;
|
||||
|
||||
dispatch_queue_t mListenerQueue;
|
||||
AudioObjectPropertyListenerBlock mListenerBlock;
|
||||
// These will only ever be null after construction on 10.9, since toggling will be disabled.
|
||||
dispatch_queue_t __nullable mListenerQueue = nullptr;
|
||||
AudioObjectPropertyListenerBlock __nullable mListenerBlock = nullptr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
//
|
||||
// BGMOutputDeviceMenuSection.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMPreferredOutputDevices.h"
|
||||
|
||||
// System Includes
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMOutputDeviceMenuSection : NSObject
|
||||
|
||||
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
|
||||
|
||||
// To be called when BGMApp has been set to use a different output device. For example, when a new
|
||||
// device is connected and BGMPreferredOutputDevices decides BGMApp should switch to it.
|
||||
- (void) outputDeviceDidChange;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
+161
-33
@@ -14,14 +14,14 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMOutputDevicePrefs.mm
|
||||
// BGMOutputDeviceMenuSection.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMOutputDeviceMenuSection.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
@@ -29,33 +29,112 @@
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAAutoDisposer.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioDevice.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
// STL Includes
|
||||
#import <set>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
static NSInteger const kOutputDeviceMenuItemTag = 5;
|
||||
|
||||
@implementation BGMOutputDevicePrefs {
|
||||
@implementation BGMOutputDeviceMenuSection {
|
||||
NSMenu* bgmMenu;
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
BGMPreferredOutputDevices* preferredDevices;
|
||||
NSMutableArray<NSMenuItem*>* outputDeviceMenuItems;
|
||||
// Called when a CoreAudio property has changed and we might need to update the menu. For
|
||||
// example, when a device is connected or disconnected.
|
||||
AudioObjectPropertyListenerBlock refreshNeededListener;
|
||||
// The devices we've added refreshNeededListener to. Used to avoid adding it to a device twice
|
||||
// for the same property and to remove it from all devices in dealloc.
|
||||
std::set<BGMAudioDevice> listenedDevices_kAudioDevicePropertyDataSources;
|
||||
std::set<BGMAudioDevice> listenedDevices_kAudioDevicePropertyDataSource;
|
||||
}
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices {
|
||||
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
|
||||
if ((self = [super init])) {
|
||||
bgmMenu = inBGMMenu;
|
||||
audioDevices = inAudioDevices;
|
||||
preferredDevices = inPreferredDevices;
|
||||
outputDeviceMenuItems = [NSMutableArray new];
|
||||
|
||||
[self listenForDevicesAddedOrRemoved];
|
||||
[self populateBGMMenu];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu {
|
||||
- (void) dealloc {
|
||||
// Tell CoreAudio not to call the listener block anymore. This probably isn't necessary.
|
||||
//
|
||||
// I think it's safe to do this without dispatching to the main queue because dealloc and
|
||||
// refreshNeededListener should be essentially mutually exclusive. If refreshNeededListener is
|
||||
// invoked and gets a value for weakSelf, it holds the strong ref until it returns, so dealloc
|
||||
// won't be called. If refreshNeededListener is invoked and deallocation has started, it will
|
||||
// get nil for weakSelf and just return.
|
||||
auto removeListener = [&] (CAHALAudioObject audioObject, AudioObjectPropertySelector prop) {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
// Check the object still exists first to reduce unnecessary error logs.
|
||||
if (CAHALAudioObject::ObjectExists(audioObject.GetObjectID())) {
|
||||
audioObject.RemovePropertyListenerBlock(CAPropertyAddress(prop),
|
||||
dispatch_get_main_queue(),
|
||||
refreshNeededListener);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Remove the listener from each audio object we added it to.
|
||||
removeListener(CAHALAudioSystemObject(), kAudioHardwarePropertyDevices);
|
||||
|
||||
for (auto device : listenedDevices_kAudioDevicePropertyDataSources) {
|
||||
removeListener(device, kAudioDevicePropertyDataSources);
|
||||
}
|
||||
|
||||
for (auto device : listenedDevices_kAudioDevicePropertyDataSource) {
|
||||
removeListener(device, kAudioDevicePropertyDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) listenForDevicesAddedOrRemoved {
|
||||
// Create the block that will run when a device is added or removed.
|
||||
BGMOutputDeviceMenuSection* __weak weakSelf = self;
|
||||
|
||||
refreshNeededListener = ^(UInt32 inNumberAddresses,
|
||||
const AudioObjectPropertyAddress* inAddresses) {
|
||||
#pragma unused (inNumberAddresses, inAddresses)
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
[weakSelf populateBGMMenu];
|
||||
});
|
||||
};
|
||||
|
||||
// Register the listener block to be called when devices are connected or disconnected.
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
CAHALAudioSystemObject().AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
dispatch_get_main_queue(),
|
||||
refreshNeededListener);
|
||||
});
|
||||
}
|
||||
|
||||
- (void) populateBGMMenu {
|
||||
// TODO: Technically, we should assert we're on the main queue rather than just the main thread.
|
||||
BGMAssert([NSThread isMainThread],
|
||||
"BGMOutputDeviceMenuSection::populateBGMMenu called on non-main thread");
|
||||
|
||||
// Remove existing menu items
|
||||
for (NSMenuItem* item in outputDeviceMenuItems) {
|
||||
[prefsMenu removeItem:item];
|
||||
DebugMsg("BGMOutputDeviceMenuSection::populateBGMMenu: Removing %s",
|
||||
item.description.UTF8String);
|
||||
[bgmMenu removeItem:item];
|
||||
}
|
||||
|
||||
[outputDeviceMenuItems removeAllObjects];
|
||||
@@ -69,25 +148,51 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
[self insertMenuItemsForDevice:devices[i] preferencesMenu:prefsMenu];
|
||||
[self insertMenuItemsForDevice:devices[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
|
||||
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device {
|
||||
// Insert menu items after the item for the "Output Device" heading.
|
||||
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||
const NSInteger menuItemsIdx = [bgmMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||
|
||||
BOOL canBeOutputDevice = YES;
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", ([&] {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
|
||||
}));
|
||||
});
|
||||
|
||||
if (canBeOutputDevice) {
|
||||
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
|
||||
[prefsMenu insertItem:item atIndex:menuItemsIdx];
|
||||
DebugMsg("BGMOutputDeviceMenuSection::insertMenuItemsForDevice: Inserting %s",
|
||||
item.description.UTF8String);
|
||||
[bgmMenu insertItem:item atIndex:menuItemsIdx];
|
||||
[outputDeviceMenuItems addObject:item];
|
||||
}
|
||||
|
||||
// Add listeners to update the menu when the device's data source changes or it changes its
|
||||
// list of data sources. We do this so that, for example, when you plug headphones into the
|
||||
// built-in jack, the menu item for the built-in device will change from "Internal Speakers"
|
||||
// to "Headphones".
|
||||
if (listenedDevices_kAudioDevicePropertyDataSources.count(device) == 0) {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSources,
|
||||
kAudioDevicePropertyScopeOutput),
|
||||
dispatch_get_main_queue(),
|
||||
refreshNeededListener);
|
||||
listenedDevices_kAudioDevicePropertyDataSources.insert(device);
|
||||
});
|
||||
};
|
||||
|
||||
if (listenedDevices_kAudioDevicePropertyDataSource.count(device) == 0) {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSource,
|
||||
kAudioDevicePropertyScopeOutput),
|
||||
dispatch_get_main_queue(),
|
||||
refreshNeededListener);
|
||||
listenedDevices_kAudioDevicePropertyDataSource.insert(device);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +202,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
NSMutableArray<NSMenuItem*>* items = [NSMutableArray new];
|
||||
|
||||
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
|
||||
UInt32 channel = 0; // 0 is the master channel.
|
||||
UInt32 channel = kAudioObjectPropertyElementMaster;
|
||||
|
||||
// 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
|
||||
@@ -106,8 +211,8 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
// 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", [&] {
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
if (device.HasDataSourceControl(scope, channel) &&
|
||||
device.DataSourceControlIsSettable(scope, channel)) {
|
||||
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
|
||||
@@ -120,32 +225,32 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
device.GetAvailableDataSources(scope, channel, numDataSources, dataSourceIDs);
|
||||
|
||||
for (UInt32 i = 0; i < numDataSources; i++) {
|
||||
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u %s%u",
|
||||
DebugMsg("BGMOutputDeviceMenuSection::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]];
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "(DS)", [&] {
|
||||
NSString* dataSourceName =
|
||||
CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i]));
|
||||
NSString* deviceName = CFBridgingRelease(device.CopyName());
|
||||
|
||||
[items addObject:[self createMenuItemForDevice:device
|
||||
dataSourceID:dataSourceID
|
||||
dataSourceID:@(dataSourceIDs[i])
|
||||
title:dataSourceName
|
||||
toolTip:deviceName]];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
|
||||
DebugMsg("BGMOutputDeviceMenuSection::createMenuItemsForDevice: Creating item. %s%u",
|
||||
"Device ID:", device.GetObjectID());
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", ([&] {
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
[items addObject:[self createMenuItemForDevice:device
|
||||
dataSourceID:nil
|
||||
title:CFBridgingRelease(device.CopyName())
|
||||
toolTip:nil]];
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -161,13 +266,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
}
|
||||
|
||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title)
|
||||
action:@selector(outputDeviceWasChanged:)
|
||||
action:@selector(outputDeviceMenuItemSelected:)
|
||||
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", [&] {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) {
|
||||
item.image = [NSImage imageNamed:@"AirPlayIcon"];
|
||||
|
||||
@@ -189,12 +294,30 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
item.indentationLevel = 1;
|
||||
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
|
||||
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
|
||||
|
||||
#if __clang_major__ >= 9
|
||||
if (@available(macOS 10.10, *)) {
|
||||
// Used for UI tests.
|
||||
item.accessibilityIdentifier = @"output-device";
|
||||
}
|
||||
#endif
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
- (void) outputDeviceWasChanged:(NSMenuItem*)menuItem {
|
||||
DebugMsg("BGMOutputDevicePrefs::outputDeviceWasChanged: '%s' menu item selected",
|
||||
// Called by BGMAudioDeviceManager to tell us a different device has been set as the output device.
|
||||
- (void) outputDeviceDidChange {
|
||||
BGMOutputDeviceMenuSection* __weak weakSelf = self;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
[weakSelf populateBGMMenu];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void) outputDeviceMenuItemSelected:(NSMenuItem*)menuItem {
|
||||
DebugMsg("BGMOutputDeviceMenuSection::outputDeviceMenuItemSelected: '%s' menu item selected",
|
||||
[menuItem.title UTF8String]);
|
||||
|
||||
// Make sure the menu item is actually for an output device.
|
||||
@@ -217,6 +340,11 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
[NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] :
|
||||
menuItem.title;
|
||||
|
||||
if (changingDevice) {
|
||||
// Add the new output device to the list of preferred devices.
|
||||
[preferredDevices userChangedOutputDeviceTo:newDeviceID];
|
||||
}
|
||||
|
||||
// Dispatched because it usually blocks. (Note that we're using
|
||||
// DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.)
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMOutputVolumeMenuItem.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
// Copyright © 2017-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -26,6 +26,7 @@
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
#import "BGMVolumeChangeListener.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAException.h"
|
||||
@@ -45,6 +46,9 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
NSTextField* deviceLabel;
|
||||
NSSlider* volumeSlider;
|
||||
BGMAudioDevice outputDevice;
|
||||
BGMVolumeChangeListener* volumeChangeListener;
|
||||
AudioObjectPropertyListenerBlock updateLabelListenerBlock;
|
||||
}
|
||||
|
||||
// TODO: Show the output device's icon next to its name.
|
||||
@@ -61,17 +65,32 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
audioDevices = devices;
|
||||
deviceLabel = label;
|
||||
volumeSlider = slider;
|
||||
outputDevice = audioDevices.outputDevice;
|
||||
|
||||
// volumeChangeListener and updateLabelListenerBlock are initialised in the methods called
|
||||
// below.
|
||||
|
||||
// Apply our custom view from MainMenu.xib.
|
||||
self.view = view;
|
||||
|
||||
// Set up the UI components in the view.
|
||||
[self initSlider];
|
||||
[self updateLabelAndToolTip];
|
||||
|
||||
// Register a listener so we can update if the output device's data source changes.
|
||||
[self addOutputDeviceDataSourceListener];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the audio property listeners.
|
||||
// TODO: This call isn't thread safe. (But currently this dealloc method is only called if
|
||||
// there's an error.)
|
||||
[self removeOutputDeviceDataSourceListener];
|
||||
}
|
||||
|
||||
- (void) initSlider {
|
||||
BGMAssert([NSThread isMainThread],
|
||||
"initSlider must be called from the main thread because it calls UI functions.");
|
||||
@@ -84,33 +103,10 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
|
||||
// Register a listener that will update the slider when the user changes the volume or
|
||||
// mutes/unmutes their audio.
|
||||
AudioObjectPropertyListenerBlock updateSlider =
|
||||
^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
|
||||
// at least one property the block is listening to, so there's no need to check
|
||||
// inAddresses.
|
||||
#pragma unused (inNumberAddresses, inAddresses)
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateVolumeSlider];
|
||||
});
|
||||
};
|
||||
|
||||
// Instead of swallowing exceptions, we could try again later, but I doubt it would be worth the
|
||||
// effort. And the documentation doesn't actually explain what could cause this to fail.
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] {
|
||||
// Register the listener to receive volume notifications.
|
||||
audioDevices.bgmDevice.AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateSlider);
|
||||
|
||||
// Register the same listener for mute/unmute notifications.
|
||||
audioDevices.bgmDevice.AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateSlider);
|
||||
}));
|
||||
BGMOutputVolumeMenuItem* __weak weakSelf = self;
|
||||
volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [=] {
|
||||
[weakSelf updateVolumeSlider];
|
||||
});
|
||||
}
|
||||
|
||||
// Updates the value of the output volume slider. Should only be called on the main thread because
|
||||
@@ -145,12 +141,67 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
volumeSlider.doubleValue = 0.0;
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
- (void) addOutputDeviceDataSourceListener {
|
||||
// Create the block that updates deviceLabel when the output device's data source changes, e.g.
|
||||
// from Internal Speakers to Headphones.
|
||||
if (!updateLabelListenerBlock) {
|
||||
BGMOutputVolumeMenuItem* __weak weakSelf = self;
|
||||
|
||||
updateLabelListenerBlock =
|
||||
^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
|
||||
// at least one property the block is listening to, so there's no need to check it.
|
||||
#pragma unused (inNumberAddresses, inAddresses)
|
||||
[weakSelf updateLabelAndToolTip];
|
||||
};
|
||||
}
|
||||
|
||||
// Register the listener.
|
||||
//
|
||||
// Instead of swallowing exceptions, we could try again later, but I doubt it would be worth the
|
||||
// effort. And the documentation doesn't actually explain what could cause this to fail.
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::addOutputDeviceDataSourceListener", ([&] {
|
||||
outputDevice.AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateLabelListenerBlock);
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) removeOutputDeviceDataSourceListener {
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::removeOutputDeviceDataSourceListener",
|
||||
([&] {
|
||||
// Technically, there's a race here in that the device could be removed after we check it
|
||||
// exists, but before we try to remove the listener. We could check the error code of the
|
||||
// exception and not log an error message if the code is kAudioHardwareBadObjectError or
|
||||
// kAudioHardwareBadDeviceError, but it probably wouldn't be worth the effort.
|
||||
//
|
||||
// So for now the main reason for checking the device exists here is that it makes debug
|
||||
// builds much less likely to crash here. (They crash/break when an error is logged so it
|
||||
// will be noticed.)
|
||||
if (CAHALAudioObject::ObjectExists(outputDevice)) {
|
||||
outputDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateLabelListenerBlock);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) outputDeviceDidChange {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Remove the data source listener from the previous output device.
|
||||
[self removeOutputDeviceDataSourceListener];
|
||||
|
||||
// Add it to the new output device.
|
||||
outputDevice = audioDevices.outputDevice;
|
||||
[self addOutputDeviceDataSourceListener];
|
||||
|
||||
// Update the label to use the name of the new output device.
|
||||
[self updateLabelAndToolTip];
|
||||
|
||||
// Set the slider to the volume of the new device.
|
||||
[self updateVolumeSlider];
|
||||
});
|
||||
@@ -160,26 +211,26 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
// datasource, the device's name is set as this menu item's tooltip. Falls back to a generic name if
|
||||
// the device returns an error when queried.
|
||||
- (void) updateLabelAndToolTip {
|
||||
BGMAudioDevice device = audioDevices.outputDevice;
|
||||
BOOL didSetLabel = NO;
|
||||
|
||||
try {
|
||||
if (device.HasDataSourceControl(kScope, kMasterChannel)) {
|
||||
if (outputDevice.HasDataSourceControl(kScope, kMasterChannel)) {
|
||||
// The device has datasources, so use the current datasource's name like macOS does.
|
||||
UInt32 dataSourceID = device.GetCurrentDataSourceID(kScope, kMasterChannel);
|
||||
UInt32 dataSourceID = outputDevice.GetCurrentDataSourceID(kScope, kMasterChannel);
|
||||
|
||||
deviceLabel.stringValue =
|
||||
(__bridge_transfer NSString*)device.CopyDataSourceNameForID(kScope,
|
||||
kMasterChannel,
|
||||
dataSourceID);
|
||||
(__bridge_transfer NSString*)outputDevice.CopyDataSourceNameForID(kScope,
|
||||
kMasterChannel,
|
||||
dataSourceID);
|
||||
didSetLabel = YES; // So we know not to change the text if setting the tooltip fails.
|
||||
|
||||
// Set the tooltip of the menu item (the container) rather than the label because menu
|
||||
// items' tooltips will still appear when a different app is focused and, as far as I
|
||||
// know, BGMApp should never be the foreground app.
|
||||
self.toolTip = (__bridge_transfer NSString*)device.CopyName();
|
||||
self.toolTip = (__bridge_transfer NSString*)outputDevice.CopyName();
|
||||
} else {
|
||||
deviceLabel.stringValue = (__bridge_transfer NSString*)device.CopyName();
|
||||
deviceLabel.stringValue = (__bridge_transfer NSString*)outputDevice.CopyName();
|
||||
self.toolTip = nil;
|
||||
}
|
||||
} catch (const CAException& e) {
|
||||
BGMLogException(e);
|
||||
@@ -193,6 +244,10 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
}
|
||||
}
|
||||
|
||||
DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Label: '%s' Tooltip: '%s'",
|
||||
deviceLabel.stringValue.UTF8String,
|
||||
self.toolTip.UTF8String);
|
||||
|
||||
// Take the label out of the accessibility hierarchy, which also moves the slider up a level.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101000 // MAC_OS_X_VERSION_10_10
|
||||
if ([deviceLabel.cell respondsToSelector:@selector(setAccessibilityElement:)]) {
|
||||
@@ -233,4 +288,3 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
|
||||
@@ -638,21 +638,16 @@ OSStatus BGMPlayThrough::Stop()
|
||||
bool outputDeviceAlive = false;
|
||||
|
||||
CATry
|
||||
inputDeviceAlive = mInputDevice.IsAlive();
|
||||
inputDeviceAlive = CAHALAudioObject::ObjectExists(mInputDevice) && mInputDevice.IsAlive();
|
||||
CACatch
|
||||
|
||||
CATry
|
||||
outputDeviceAlive = mOutputDevice.IsAlive();
|
||||
outputDeviceAlive =
|
||||
CAHALAudioObject::ObjectExists(mOutputDevice) && mOutputDevice.IsAlive();
|
||||
CACatch
|
||||
|
||||
if(inputDeviceAlive)
|
||||
{
|
||||
mInputDeviceIOProcState = IOState::Stopping;
|
||||
}
|
||||
if(outputDeviceAlive)
|
||||
{
|
||||
mOutputDeviceIOProcState = IOState::Stopping;
|
||||
}
|
||||
|
||||
mInputDeviceIOProcState = inputDeviceAlive ? IOState::Stopping : IOState::Stopped;
|
||||
mOutputDeviceIOProcState = outputDeviceAlive ? IOState::Stopping : IOState::Stopped;
|
||||
|
||||
// Wait for the IOProcs to stop themselves. This is so the IOProcs don't get called after the BGMPlayThrough instance
|
||||
// (pointed to by the client data they get from the HAL) is deallocated.
|
||||
@@ -662,14 +657,28 @@ OSStatus BGMPlayThrough::Stop()
|
||||
// when you make the call from outside of your IOProc. However, if you call AudioDeviceStop() from inside your IOProc,
|
||||
// you do get the guarantee that your IOProc will not get called again after the IOProc has returned.
|
||||
UInt64 totalWaitNs = 0;
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Stop", [&]() {
|
||||
Float64 expectedInputCycleNs =
|
||||
mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
Float64 expectedOutputCycleNs =
|
||||
mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&]() {
|
||||
Float64 expectedInputCycleNs = 0;
|
||||
|
||||
if(inputDeviceAlive)
|
||||
{
|
||||
expectedInputCycleNs =
|
||||
mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) *
|
||||
NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
Float64 expectedOutputCycleNs = 0;
|
||||
|
||||
if(outputDeviceAlive)
|
||||
{
|
||||
expectedOutputCycleNs =
|
||||
mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) *
|
||||
NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
UInt64 expectedMaxCycleNs =
|
||||
static_cast<UInt64>(std::max(expectedInputCycleNs, expectedOutputCycleNs));
|
||||
|
||||
|
||||
while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping)
|
||||
&& (totalWaitNs < kStopIOProcTimeoutInIOCycles * expectedMaxCycleNs))
|
||||
{
|
||||
|
||||
@@ -169,8 +169,8 @@ private:
|
||||
private:
|
||||
CARingBuffer mBuffer;
|
||||
|
||||
AudioDeviceIOProcID __nullable mInputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mInputDeviceIOProcID { nullptr };
|
||||
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID { nullptr };
|
||||
|
||||
BGMAudioDevice mInputDevice { kAudioObjectUnknown };
|
||||
BGMAudioDevice mOutputDevice { kAudioObjectUnknown };
|
||||
@@ -183,13 +183,13 @@ private:
|
||||
bool mActive = false;
|
||||
bool mPlayingThrough = false;
|
||||
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice;
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice { 0 };
|
||||
|
||||
std::atomic<IOState> mInputDeviceIOProcState { IOState::Stopped };
|
||||
std::atomic<IOState> mOutputDeviceIOProcState { IOState::Stopped };
|
||||
|
||||
// For debug logging.
|
||||
UInt64 mToldOutputDeviceToStartAt;
|
||||
UInt64 mToldOutputDeviceToStartAt { 0 };
|
||||
|
||||
// IOProc vars. (Should only be used inside IOProcs.)
|
||||
|
||||
@@ -199,7 +199,7 @@ private:
|
||||
Float64 mLastOutputSampleTime = -1;
|
||||
|
||||
// Subtract this from the output time to get the input time.
|
||||
Float64 mInToOutSampleOffset;
|
||||
Float64 mInToOutSampleOffset { 0.0 };
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMPreferredOutputDevices.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
// Tries to change BGMApp's output device when the user plugs in or unplugs an audio device, in the
|
||||
// same way macOS would change its default device if Background Music wasn't running.
|
||||
//
|
||||
// For example, if you plug in some USB headphones, make them your default device and then unplug
|
||||
// them, macOS will change its default device to the previous default device. Then, if you plug
|
||||
// them back in, macOS will make them the default device again.
|
||||
//
|
||||
// This class isn't always able to figure out what macOS would do, in which case it leaves BGMApp's
|
||||
// output device as it is.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <CoreAudio/AudioHardwareBase.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMPreferredOutputDevices : NSObject
|
||||
|
||||
// Starts responding to device connections/disconnections immediately. Stops if/when the instance is
|
||||
// deallocated.
|
||||
- (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)userDefaults;
|
||||
|
||||
// Returns the most-preferred device that's currently connected. If no preferred devices are
|
||||
// connected, returns the current output device. If the current output device has been disconnected,
|
||||
// returns an arbitrary device.
|
||||
//
|
||||
// If none of the connected devices can be used as the output device, or if it can't find a device
|
||||
// to use because the HAL returned errors when queried, returns kAudioObjectUnknown.
|
||||
- (AudioObjectID) findPreferredDevice;
|
||||
|
||||
- (void) userChangedOutputDeviceTo:(AudioObjectID)device;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMPreferredOutputDevices.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMPreferredOutputDevices.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// The Plist file CoreAudio stores the preferred devices list in. It isn't part of the public API,
|
||||
// but if this class fails to read/parse it, BGMApp will continue without it.
|
||||
NSString* const kAudioSystemSettingsPlist =
|
||||
@"/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist";
|
||||
|
||||
@implementation BGMPreferredOutputDevices {
|
||||
NSRecursiveLock* _stateLock;
|
||||
|
||||
// Used to change BGMApp's output device.
|
||||
BGMAudioDeviceManager* _devices;
|
||||
|
||||
// User settings and data.
|
||||
BGMUserDefaults* _userDefaults;
|
||||
|
||||
// The UIDs of the preferred devices, in order of preference. The most-preferred device is at
|
||||
// index 0. This list is derived from several sources.
|
||||
NSArray<NSString*>* _preferredDeviceUIDs;
|
||||
|
||||
// Called when a device is connected or disconnected.
|
||||
AudioObjectPropertyListenerBlock _deviceListListener;
|
||||
}
|
||||
|
||||
- (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)userDefaults {
|
||||
if ((self = [super init])) {
|
||||
_stateLock = [NSRecursiveLock new];
|
||||
_devices = devices;
|
||||
_userDefaults = userDefaults;
|
||||
_preferredDeviceUIDs = [self readPreferredDevices];
|
||||
|
||||
DebugMsg("BGMPreferredOutputDevices::initWithDevices: Preferred devices: %s",
|
||||
_preferredDeviceUIDs.debugDescription.UTF8String);
|
||||
|
||||
[self listenForDevicesAddedOrRemoved];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
@try {
|
||||
[_stateLock lock];
|
||||
|
||||
// Tell CoreAudio not to call the listener block anymore.
|
||||
CAHALAudioSystemObject().RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
|
||||
_deviceListListener);
|
||||
} @finally {
|
||||
[_stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the preferred devices list from CoreAudio's Plist file. Uses BGMApp's stored list to fill
|
||||
// in the blanks, if necessary.
|
||||
- (NSArray<NSString*>*) readPreferredDevices {
|
||||
// Read the Plist file into a dictionary.
|
||||
//
|
||||
// TODO: If this file doesn't exist, try paths used by older versions of macOS. (If there are
|
||||
// any, that is. I haven't checked.)
|
||||
NSURL* url = [NSURL fileURLWithPath:kAudioSystemSettingsPlist];
|
||||
NSError* error = nil;
|
||||
NSData* data = [NSData dataWithContentsOfURL:url options:0 error:&error];
|
||||
NSDictionary* settings = nil;
|
||||
|
||||
if (data && !error) {
|
||||
settings = [NSPropertyListSerialization propertyListWithData:data
|
||||
options:NSPropertyListImmutable
|
||||
format:nil
|
||||
error:&error];
|
||||
}
|
||||
|
||||
// Default to a list with just the systemwide default device (or an empty list if that fails) if
|
||||
// we can't read the preferred devices from the Plist because preferredDeviceUIDsFrom will use
|
||||
// BGMApp's stored preferred devices to fill in the rest optimistically. This doesn't help us
|
||||
// tell when to switch to a newly connected device, but it should improve our chances of
|
||||
// switching to the best device if the current output device is disconnected.
|
||||
NSArray<NSDictionary*>* preferredOutputDeviceInfos = @[];
|
||||
|
||||
// If we can't read the Plist, we only know that the current systemwide default device is the
|
||||
// most-preferred device that's currently connected.
|
||||
//
|
||||
// TODO: If we are able to read the Plist, check that the systemwide default device is the
|
||||
// most-preferred device in the list from the Plist that's also connected. If it isn't,
|
||||
// the format of the Plist has probably changed, so we should ignore its data and log a
|
||||
// warning.
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
BGMAudioDevice defaultDevice = CAHALAudioSystemObject().GetDefaultAudioDevice(false, false);
|
||||
NSString* __nullable defaultDeviceUID =
|
||||
(__bridge_transfer NSString* __nullable)defaultDevice.CopyDeviceUID();
|
||||
|
||||
if (defaultDeviceUID) {
|
||||
preferredOutputDeviceInfos = @[ @{ @"uid": BGMNN(defaultDeviceUID) } ];
|
||||
}
|
||||
});
|
||||
|
||||
if (error || !data || !settings) {
|
||||
// The Plist file either doesn't exist or we weren't able to parse it.
|
||||
LogWarning("BGMPreferredOutputDevices::readPreferredDevices: Couldn't read %s. "
|
||||
"(data = %s, settings = %s) Error: %s",
|
||||
kAudioSystemSettingsPlist.UTF8String,
|
||||
data.debugDescription.UTF8String,
|
||||
settings.debugDescription.UTF8String,
|
||||
error.debugDescription.UTF8String);
|
||||
} else if (!settings[@"preferred devices"]) {
|
||||
// The Plist doesn't include the lists of preferred devices (for input, output and system
|
||||
// output).
|
||||
LogWarning("BGMPreferredOutputDevices::readPreferredDevices: No preferred devices in %s",
|
||||
settings.debugDescription.UTF8String);
|
||||
} else if (!settings[@"preferred devices"][@"output"]) {
|
||||
// The Plist doesn't include the list of preferred output devices.
|
||||
LogWarning("BGMPreferredOutputDevices::readPreferredDevices: "
|
||||
"No preferred output devices in %s",
|
||||
settings.debugDescription.UTF8String);
|
||||
} else {
|
||||
// Copy the preferred devices out of the Plist.
|
||||
preferredOutputDeviceInfos = BGMNN(settings[@"preferred devices"][@"output"]);
|
||||
}
|
||||
|
||||
return [self preferredDeviceUIDsFrom:preferredOutputDeviceInfos
|
||||
storedPreferredDeviceUIDs:_userDefaults.preferredDeviceUIDs];
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*) preferredDeviceUIDsFrom:(NSArray<NSDictionary*>*)deviceInfos
|
||||
storedPreferredDeviceUIDs:(NSArray<NSString*>*)storedUIDs {
|
||||
NSArray<NSString*>* deviceUIDs = @[];
|
||||
int storedPreferredDeviceIdx = 0;
|
||||
|
||||
for (NSDictionary* deviceInfo in deviceInfos) {
|
||||
// Check the Plist actually has a UID for this device.
|
||||
if (![deviceInfo[@"uid"] isKindOfClass:NSString.class]) {
|
||||
LogWarning("BGMPreferredOutputDevices::preferredDeviceUIDsFrom: No UID in %s",
|
||||
deviceInfo.debugDescription.UTF8String);
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString* uid = deviceInfo[@"uid"];
|
||||
|
||||
if ([uid isEqualToString:@kBGMDeviceUID] ||
|
||||
[uid isEqualToString:@kBGMDeviceUID_UISounds] ||
|
||||
[uid isEqualToString:@kBGMNullDeviceUID]) {
|
||||
// This is one of the Background Music devices, so look for a preferred device saved
|
||||
// from a previous run of BGMApp and add it instead.
|
||||
//
|
||||
// BGMApp has to set BGMDevice, and often also the Null Device for a short time, as the
|
||||
// systemwide default audio device, which makes CoreAudio put them in its Plist. And
|
||||
// since the Plist is limited to three devices, it only gives us one or two usable ones.
|
||||
// Ideally, CoreAudio just wouldn't add our devices to its list, but I don't think we
|
||||
// can prevent that. And we can't be sure that editing its Plist file ourselves would be
|
||||
// safe.
|
||||
//
|
||||
// TODO: This doesn't work if the user has made BGMDevice the systemwide default device
|
||||
// themselves since the last time they closed BGMApp. We might be able to fix that
|
||||
// by having a background process watch the Plist for changes while BGMApp is
|
||||
// closed or something like that, but I doubt there's a nice or simple solution.
|
||||
deviceUIDs = [self addNextStoredPreferredDevice:&storedPreferredDeviceIdx
|
||||
preferredDeviceUIDs:deviceUIDs
|
||||
storedPreferredDeviceUIDs:storedUIDs];
|
||||
} else if (![deviceUIDs containsObject:uid]) {
|
||||
// Add this preferred device's UID to the list.
|
||||
deviceUIDs = [deviceUIDs arrayByAddingObject:uid];
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in any remaining places in the list with stored devices. Limit the list to three devices
|
||||
// just to match CoreAudio's behaviour.
|
||||
while ((storedPreferredDeviceIdx < storedUIDs.count) && (deviceUIDs.count < 3)) {
|
||||
deviceUIDs = [self addNextStoredPreferredDevice:&storedPreferredDeviceIdx
|
||||
preferredDeviceUIDs:deviceUIDs
|
||||
storedPreferredDeviceUIDs:storedUIDs];
|
||||
}
|
||||
|
||||
return deviceUIDs;
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*) addNextStoredPreferredDevice:(int*)storedPreferredDeviceIdx
|
||||
preferredDeviceUIDs:(NSArray<NSString*>*)deviceUIDs
|
||||
storedPreferredDeviceUIDs:(NSArray<NSString*>*)storedUIDs {
|
||||
NSString* __nullable storedPreferredDevice;
|
||||
|
||||
// Try to find a stored UID that isn't already in the list.
|
||||
do {
|
||||
storedPreferredDevice = (*storedPreferredDeviceIdx < storedUIDs.count) ?
|
||||
storedUIDs[*storedPreferredDeviceIdx] : nil;
|
||||
(*storedPreferredDeviceIdx)++;
|
||||
} while (storedPreferredDevice && [deviceUIDs containsObject:BGMNN(storedPreferredDevice)]);
|
||||
|
||||
// If we found a stored UID, add it to the list.
|
||||
if (storedPreferredDevice) {
|
||||
DebugMsg("BGMPreferredOutputDevices::addNextStoredPreferredDevice: "
|
||||
"Adding stored preferred device: %s",
|
||||
storedPreferredDevice.UTF8String);
|
||||
deviceUIDs = [deviceUIDs arrayByAddingObject:BGMNN(storedPreferredDevice)];
|
||||
}
|
||||
|
||||
return deviceUIDs;
|
||||
}
|
||||
|
||||
- (void) listenForDevicesAddedOrRemoved {
|
||||
// Create the block that will run when a device is added or removed.
|
||||
BGMPreferredOutputDevices* __weak weakSelf = self;
|
||||
|
||||
_deviceListListener = ^(UInt32 inNumberAddresses,
|
||||
const AudioObjectPropertyAddress* inAddresses) {
|
||||
#pragma unused (inNumberAddresses, inAddresses)
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
[weakSelf connectedDeviceListChanged];
|
||||
});
|
||||
};
|
||||
|
||||
// Register the listener block with CoreAudio.
|
||||
CAHALAudioSystemObject().AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
|
||||
_deviceListListener);
|
||||
}
|
||||
|
||||
- (void) connectedDeviceListChanged {
|
||||
@try {
|
||||
[_stateLock lock];
|
||||
|
||||
// Decide which device should be the output device now. If a device has been connected and
|
||||
// it's preferred over the current output device, we'll change to that device. If the
|
||||
// current output device has been removed, we'll change to the next most-preferred device.
|
||||
AudioObjectID preferredDevice = [self findPreferredDevice];
|
||||
|
||||
if (preferredDevice == kAudioObjectUnknown) {
|
||||
LogWarning("BGMPreferredOutputDevices::connectedDeviceListChanged: "
|
||||
"No preferred device found.");
|
||||
} else if (_devices.outputDevice.GetObjectID() == preferredDevice) {
|
||||
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
|
||||
"The preferred device is already set as the output device.");
|
||||
} else {
|
||||
// Change to the preferred device.
|
||||
DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: "
|
||||
"Changing output device to %d.",
|
||||
preferredDevice);
|
||||
NSError* __nullable error = [_devices setOutputDeviceWithID:preferredDevice
|
||||
revertOnFailure:YES];
|
||||
if (error) {
|
||||
// There's not much we can do if this happens.
|
||||
LogError("BGMPreferredOutputDevices::connectedDeviceListChanged: "
|
||||
"Failed to change to preferred device. Error: %s",
|
||||
error.debugDescription.UTF8String);
|
||||
}
|
||||
}
|
||||
} @finally {
|
||||
[_stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (AudioObjectID) findPreferredDevice {
|
||||
AudioObjectID preferredDevice = kAudioObjectUnknown;
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
|
||||
if (!defaultDevice.IsBGMDeviceInstance()) {
|
||||
// BGMDevice isn't the systemwide default device, so we know the default device is the
|
||||
// most-preferred device that's currently connected.
|
||||
preferredDevice = defaultDevice;
|
||||
}
|
||||
});
|
||||
|
||||
if (preferredDevice == kAudioObjectUnknown) {
|
||||
// BGMDevice is the systemwide default device, so this method is probably being called after
|
||||
// launch, since we set BGMDevice as the default device then. It could also be that the user
|
||||
// set it manually or that BGMApp failed to change it back the last time it closed. Either
|
||||
// way, we'll try to find a device to use in the Plist or stored list instead.
|
||||
DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: Checking Plist and stored list.");
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
preferredDevice = [self findPreferredDeviceInDerivedList];
|
||||
});
|
||||
}
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
if (preferredDevice == kAudioObjectUnknown && [self isCurrentOutputDeviceConnected]) {
|
||||
// The output device (for BGMApp) has been set and is still connected. We haven't found
|
||||
// a better device to use, so prefer leaving the output device as it is.
|
||||
DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: "
|
||||
"Choosing the current output device as the preferred device.");
|
||||
preferredDevice = _devices.outputDevice.GetObjectID();
|
||||
}
|
||||
});
|
||||
|
||||
if (preferredDevice == kAudioObjectUnknown) {
|
||||
// The current output device has been disconnected or hasn't been set yet and there are no
|
||||
// preferred devices connected, so pick one arbitrarily.
|
||||
DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: Choosing an arbitrary device.");
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
preferredDevice = [self findPreferredDeviceByLatency];
|
||||
});
|
||||
}
|
||||
|
||||
return preferredDevice;
|
||||
}
|
||||
|
||||
// Looks for a suitable device in _preferredDeviceUIDs, the list of preferred devices derived from
|
||||
// CoreAudio's Plist and BGMApp's stored list.
|
||||
- (AudioObjectID) findPreferredDeviceInDerivedList {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
|
||||
|
||||
// Get the list of currently connected audio devices.
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
// Look through the preferred devices list to see if one's connected. Return the first one we
|
||||
// find because they're stored in order of preference.
|
||||
for (int position = 0; position < _preferredDeviceUIDs.count; position++) {
|
||||
// Compare the current preferred device to each connected device by UID.
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
NSString* __nullable connectedDeviceUID = nil;
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
// Skip devices we can't use, e.g. BGMDevice.
|
||||
if (BGMAudioDevice(devices[i]).CanBeOutputDeviceInBGMApp()) {
|
||||
// Get the connected device's UID.
|
||||
connectedDeviceUID =
|
||||
(__bridge NSString* __nullable)CAHALAudioDevice(devices[i]).CopyDeviceUID();
|
||||
}
|
||||
});
|
||||
|
||||
// If the UIDs match, the current preferred device is connected.
|
||||
//
|
||||
// If you plug a USB device in to different USB port, macOS might assign it a different
|
||||
// UID and make it fail to match its old UID here. CoreAudio/macOS doesn't seem to
|
||||
// handle that case either, though, so it's probably not worth worrying about.
|
||||
if ([connectedDeviceUID isEqualToString:_preferredDeviceUIDs[position]]) {
|
||||
// We're iterating through the preferred devices from most to least-preferred, so
|
||||
// we've found the device to use.
|
||||
DebugMsg("BGMPreferredOutputDevices::findPreferredDeviceInDerivedList: "
|
||||
"Found preferred device '%s' at position %d",
|
||||
_preferredDeviceUIDs[position].UTF8String,
|
||||
position);
|
||||
return devices[i];
|
||||
}
|
||||
}
|
||||
|
||||
DebugMsg("BGMPreferredOutputDevices::findPreferredDeviceInDerivedList: "
|
||||
"Preferred device not connected: %s",
|
||||
_preferredDeviceUIDs[position].UTF8String);
|
||||
}
|
||||
|
||||
return kAudioObjectUnknown;
|
||||
}
|
||||
|
||||
- (bool) isCurrentOutputDeviceConnected {
|
||||
if (_devices.outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: "
|
||||
"The output device hasn't been set yet.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
|
||||
|
||||
// Get the list of currently connected audio devices.
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
// Look for the current output device in the list of connected devices.
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
// TODO: Are AudioObjectIDs reused? If they are, could that cause a collision here?
|
||||
if (_devices.outputDevice.GetObjectID() == devices[i]) {
|
||||
DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: "
|
||||
"The output device is connected.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: "
|
||||
"The output device is not connected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the audio output device with the lowest latency. Used when we have no better way to
|
||||
// choose the output device for BGMApp to use.
|
||||
- (AudioObjectID) findPreferredDeviceByLatency {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
|
||||
AudioObjectID minLatencyDevice = kAudioObjectUnknown;
|
||||
UInt32 minLatency = UINT32_MAX;
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
BGMAudioDevice device(devices[i]);
|
||||
|
||||
if (!device.IsBGMDeviceInstance()) {
|
||||
BOOL hasOutputChannels = NO;
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "GetTotalNumberChannels", [&] {
|
||||
hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
|
||||
});
|
||||
|
||||
if (hasOutputChannels) {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "GetLatency", [&] {
|
||||
UInt32 latency = device.GetLatency(false);
|
||||
|
||||
if (latency < minLatency) {
|
||||
minLatencyDevice = devices[i];
|
||||
minLatency = latency;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minLatencyDevice;
|
||||
}
|
||||
|
||||
- (void) userChangedOutputDeviceTo:(AudioObjectID)device {
|
||||
@try {
|
||||
[_stateLock lock];
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
// Add the new output device to the list.
|
||||
NSString* __nullable outputDeviceUID =
|
||||
(__bridge_transfer NSString* __nullable)CAHALAudioDevice(device).CopyDeviceUID();
|
||||
|
||||
if (outputDeviceUID) {
|
||||
// Limit the list to three devices because that's what macOS does.
|
||||
if (_preferredDeviceUIDs.count >= 2) {
|
||||
_preferredDeviceUIDs = @[BGMNN(outputDeviceUID),
|
||||
_preferredDeviceUIDs[0],
|
||||
_preferredDeviceUIDs[1]];
|
||||
} else if (_preferredDeviceUIDs.count >= 1) {
|
||||
_preferredDeviceUIDs = @[BGMNN(outputDeviceUID), _preferredDeviceUIDs[0]];
|
||||
} else {
|
||||
_preferredDeviceUIDs = @[BGMNN(outputDeviceUID)];
|
||||
}
|
||||
|
||||
DebugMsg("BGMPreferredOutputDevices::userChangedOutputDeviceTo: "
|
||||
"Preferred devices: %s",
|
||||
_preferredDeviceUIDs.debugDescription.UTF8String);
|
||||
|
||||
// Save the list.
|
||||
_userDefaults.preferredDeviceUIDs = _preferredDeviceUIDs;
|
||||
} else {
|
||||
LogWarning("BGMPreferredOutputDevices::userChangedOutputDeviceTo: "
|
||||
"Output device has no UID");
|
||||
}
|
||||
});
|
||||
} @finally {
|
||||
[_stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMStatusBarItem.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
// Forward Declarations
|
||||
@class BGMUserDefaults;
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
typedef NS_ENUM(NSInteger, BGMStatusBarIcon) {
|
||||
BGMFermataStatusBarIcon = 0,
|
||||
BGMVolumeStatusBarIcon
|
||||
};
|
||||
|
||||
static BGMStatusBarIcon const kBGMStatusBarIconMinValue = BGMFermataStatusBarIcon;
|
||||
static BGMStatusBarIcon const kBGMStatusBarIconMaxValue = BGMVolumeStatusBarIcon;
|
||||
static BGMStatusBarIcon const kBGMStatusBarIconDefaultValue = BGMFermataStatusBarIcon;
|
||||
|
||||
@interface BGMStatusBarItem : NSObject
|
||||
|
||||
- (instancetype) initWithMenu:(NSMenu*)bgmMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults;
|
||||
|
||||
// Set this to BGMFermataStatusBarIcon to change the icon to the Background Music logo.
|
||||
//
|
||||
// Set this to BGMFermataStatusBarIcon to change the icon to a volume icon. This icon has the
|
||||
// advantage of indicating the volume level, but we can't make it the default because it looks the
|
||||
// same as the icon for the macOS volume status bar item.
|
||||
@property BGMStatusBarIcon icon;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMStatusBarItem.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMStatusBarItem.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMVolumeChangeListener.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static CGFloat const kStatusBarIconPadding = 0.25;
|
||||
static CGFloat const kVolumeIconAdditionalVerticalPadding = 0.075;
|
||||
|
||||
@implementation BGMStatusBarItem
|
||||
{
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
|
||||
// User settings and data.
|
||||
BGMUserDefaults* userDefaults;
|
||||
|
||||
NSImage* fermataIcon;
|
||||
NSImage* volumeIcon0SoundWaves;
|
||||
NSImage* volumeIcon1SoundWave;
|
||||
NSImage* volumeIcon2SoundWaves;
|
||||
NSImage* volumeIcon3SoundWaves;
|
||||
|
||||
NSStatusItem* statusBarItem;
|
||||
BGMVolumeChangeListener* volumeChangeListener;
|
||||
|
||||
BGMStatusBarIcon _icon;
|
||||
}
|
||||
|
||||
- (instancetype) initWithMenu:(NSMenu*)bgmMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
if ((self = [super init])) {
|
||||
statusBarItem =
|
||||
[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
||||
|
||||
audioDevices = devices;
|
||||
userDefaults = defaults;
|
||||
|
||||
// Initialise the icons.
|
||||
[self initIcons];
|
||||
|
||||
// Set the initial icon.
|
||||
self.icon = userDefaults.statusBarIcon;
|
||||
|
||||
// Set the menu item to open the main menu.
|
||||
statusBarItem.menu = bgmMenu;
|
||||
|
||||
// Set the accessibility label to "Background Music". (We intentionally don't set a title or
|
||||
// a tooltip.)
|
||||
if ([BGMStatusBarItem buttonAvailable]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.accessibilityLabel =
|
||||
[NSRunningApplication currentApplication].localizedName;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
// Update the icon when BGMDevice's volume changes.
|
||||
BGMStatusBarItem* __weak weakSelf = self;
|
||||
volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [=] {
|
||||
[weakSelf bgmDeviceVolumeDidChange];
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
delete volumeChangeListener;
|
||||
}
|
||||
|
||||
- (void) initIcons {
|
||||
// Load the icons.
|
||||
fermataIcon = [NSImage imageNamed:@"FermataIcon"];
|
||||
volumeIcon0SoundWaves = [NSImage imageNamed:@"Volume0"];
|
||||
volumeIcon1SoundWave = [NSImage imageNamed:@"Volume1"];
|
||||
volumeIcon2SoundWaves = [NSImage imageNamed:@"Volume2"];
|
||||
volumeIcon3SoundWaves = [NSImage imageNamed:@"Volume3"];
|
||||
|
||||
// Set the icons' sizes.
|
||||
NSRect statusBarItemFrame;
|
||||
|
||||
if ([BGMStatusBarItem 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 heightMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding);
|
||||
|
||||
// The fermata icon has equal width and height.
|
||||
[fermataIcon setSize:NSMakeSize(heightMinusPadding, heightMinusPadding)];
|
||||
|
||||
// The volume icons are all the same width and height.
|
||||
CGFloat volumeIconWidthToHeightRatio =
|
||||
volumeIcon0SoundWaves.size.width / volumeIcon0SoundWaves.size.height;
|
||||
CGFloat volumeIconWidth = heightMinusPadding * volumeIconWidthToHeightRatio;
|
||||
CGFloat volumeIconHeight = heightMinusPadding * (1 - kVolumeIconAdditionalVerticalPadding);
|
||||
|
||||
[volumeIcon0SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
|
||||
[volumeIcon1SoundWave setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
|
||||
[volumeIcon2SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
|
||||
[volumeIcon3SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)];
|
||||
|
||||
// Make the icons "template images" so they get drawn colour-inverted when they're highlighted
|
||||
// or the system is in dark mode.
|
||||
[fermataIcon setTemplate:YES];
|
||||
[volumeIcon0SoundWaves setTemplate:YES];
|
||||
[volumeIcon1SoundWave setTemplate:YES];
|
||||
[volumeIcon2SoundWaves setTemplate:YES];
|
||||
[volumeIcon3SoundWaves setTemplate:YES];
|
||||
}
|
||||
|
||||
+ (BOOL) buttonAvailable {
|
||||
// NSStatusItem doesn't have the "button" property on OS X 10.9.
|
||||
return (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10);
|
||||
}
|
||||
|
||||
- (void) setImage:(NSImage*)image {
|
||||
if ([BGMStatusBarItem buttonAvailable]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.image = image;
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
statusBarItem.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
- (BGMStatusBarIcon) icon {
|
||||
return _icon;
|
||||
}
|
||||
|
||||
- (void) setIcon:(BGMStatusBarIcon)icon {
|
||||
_icon = icon;
|
||||
|
||||
// Save the setting.
|
||||
userDefaults.statusBarIcon = self.icon;
|
||||
|
||||
// Change the icon (i.e. the image). Dispatch this to the main thread because it changes the UI.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_icon == BGMFermataStatusBarIcon) {
|
||||
[self setImage:fermataIcon];
|
||||
|
||||
// If the icon was greyed out, change it back.
|
||||
if ([BGMStatusBarItem buttonAvailable]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.appearsDisabled = NO;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
} else {
|
||||
BGMAssert((_icon == BGMVolumeStatusBarIcon), "Unknown icon in enum");
|
||||
|
||||
[self updateVolumeStatusBarIcon];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void) bgmDeviceVolumeDidChange {
|
||||
if (self.icon == BGMVolumeStatusBarIcon) {
|
||||
[self updateVolumeStatusBarIcon];
|
||||
}
|
||||
}
|
||||
|
||||
// Should only be called on the main thread because it calls UI functions.
|
||||
- (void) updateVolumeStatusBarIcon {
|
||||
BGMAssert([[NSThread currentThread] isMainThread],
|
||||
"updateVolumeStatusBarIcon called on non-main thread.");
|
||||
BGMAssert((self.icon == BGMVolumeStatusBarIcon), "Volume status bar icon not enabled");
|
||||
|
||||
BGMAudioDevice bgmDevice = [audioDevices bgmDevice];
|
||||
|
||||
// BGMDevice should never return an error for these calls, so we just swallow any exceptions and
|
||||
// give up.
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
|
||||
AudioObjectPropertyScope element = kAudioObjectPropertyElementMaster;
|
||||
|
||||
BOOL hasVolume = bgmDevice.HasVolumeControl(scope, element);
|
||||
|
||||
// Show the button as greyed out if BGMDevice doesn't have a volume control (which means the
|
||||
// output device doesn't have one).
|
||||
if ([BGMStatusBarItem buttonAvailable]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.appearsDisabled = !hasVolume;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
if (hasVolume) {
|
||||
if (bgmDevice.HasMuteControl(scope, element) &&
|
||||
bgmDevice.GetMuteControlValue(scope, element)) {
|
||||
// The device is muted, so use the zero waves icon.
|
||||
[self setImage:volumeIcon0SoundWaves];
|
||||
} else {
|
||||
// Set the icon to reflect the device's volume.
|
||||
double volume = bgmDevice.GetVolumeControlScalarValue(scope, element);
|
||||
|
||||
// These values match the macOS volume status bar item, except for the first one. I
|
||||
// don't know why, but at a very low volume macOS will show the zero waves icon even
|
||||
// though the sound is still audible.
|
||||
if (volume == 0.05) {
|
||||
[self setImage:volumeIcon0SoundWaves];
|
||||
} else if (volume < 0.33) {
|
||||
[self setImage:volumeIcon1SoundWave];
|
||||
} else if (volume < 0.66) {
|
||||
[self setImage:volumeIcon2SoundWaves];
|
||||
} else {
|
||||
[self setImage:volumeIcon3SoundWaves];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Always use the full-volume icon when the device has no volume control.
|
||||
[self setImage:volumeIcon3SoundWaves];
|
||||
}
|
||||
});
|
||||
|
||||
DebugMsg("BGMStatusBarItem::updateVolumeStatusBarIcon: Set icon to %s",
|
||||
statusBarItem.image.name.UTF8String);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// TODO: It's a bit confusing that this slider's default position is all the way right, but the App
|
||||
// Volumes sliders default to 50%. After you move the slider there's no way to tell how to put
|
||||
// it back to its normal position.
|
||||
|
||||
NSString* const kMenuItemToolTip =
|
||||
@"Alerts, notification sounds, etc. Usually short. Can be played by any app.";
|
||||
|
||||
|
||||
@@ -17,13 +17,18 @@
|
||||
// BGMUserDefaults.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2019 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.
|
||||
// apply to BGMApp. The others are stored by BGMDriver.
|
||||
//
|
||||
// Private data will be stored in the user's keychain instead of user defaults.
|
||||
//
|
||||
|
||||
// System includes
|
||||
// Local Includes
|
||||
#import "BGMStatusBarItem.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
@@ -40,6 +45,21 @@
|
||||
|
||||
@property BOOL autoPauseMusicEnabled;
|
||||
|
||||
// The UIDs of the output devices most recently selected by the user. The most-recently selected
|
||||
// device is at index 0. See BGMPreferredOutputDevices.
|
||||
@property NSArray<NSString*>* preferredDeviceUIDs;
|
||||
|
||||
// The (type of) icon to show in the button in the status bar. (The button the user clicks to open
|
||||
// BGMApp's main menu.)
|
||||
@property BGMStatusBarIcon statusBarIcon;
|
||||
|
||||
// The auth code we're required to send when connecting to GPMDP. Stored in the keychain. Reading
|
||||
// this property is thread-safe, but writing it isn't.
|
||||
//
|
||||
// Returns nil if no code is found or if reading fails. If writing fails, an error is logged, but no
|
||||
// exception is thrown.
|
||||
@property NSString* __nullable googlePlayMusicDesktopPlayerPermanentAuthCode;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
+163
-11
@@ -17,18 +17,27 @@
|
||||
// BGMUserDefaults.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
// Self Include
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// Keys
|
||||
static NSString* const BGMDefaults_AutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
|
||||
static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayerID";
|
||||
static NSString* const kDefaultKeyAutoPauseMusicEnabled = @"AutoPauseMusicEnabled";
|
||||
static NSString* const kDefaultKeySelectedMusicPlayerID = @"SelectedMusicPlayerID";
|
||||
static NSString* const kDefaultKeyPreferredDeviceUIDs = @"PreferredDeviceUIDs";
|
||||
static NSString* const kDefaultKeyStatusBarIcon = @"StatusBarIcon";
|
||||
|
||||
// Labels for Keychain Data
|
||||
static NSString* const kKeychainLabelGPMDPAuthCode =
|
||||
@"app.backgroundmusic: Google Play Music Desktop Player permanent auth code";
|
||||
|
||||
@implementation BGMUserDefaults {
|
||||
// The defaults object wrapped by this object.
|
||||
@@ -44,11 +53,11 @@ static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayer
|
||||
|
||||
// Register the settings defaults.
|
||||
//
|
||||
// iTunes is the default music player, but we don't set BGMDefaults_SelectedMusicPlayerID
|
||||
// iTunes is the default music player, but we don't set kDefaultKeySelectedMusicPlayerID
|
||||
// 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 };
|
||||
NSDictionary* defaultsDict = @{ kDefaultKeyAutoPauseMusicEnabled: @YES };
|
||||
|
||||
if (defaults) {
|
||||
[defaults registerDefaults:defaultsDict];
|
||||
@@ -60,22 +69,139 @@ static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayer
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Selected Music Player
|
||||
|
||||
- (NSString* __nullable) selectedMusicPlayerID {
|
||||
return [self get:BGMDefaults_SelectedMusicPlayerID];
|
||||
return [self get:kDefaultKeySelectedMusicPlayerID];
|
||||
}
|
||||
|
||||
- (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID {
|
||||
[self set:BGMDefaults_SelectedMusicPlayerID to:selectedMusicPlayerID];
|
||||
[self set:kDefaultKeySelectedMusicPlayerID to:selectedMusicPlayerID];
|
||||
}
|
||||
|
||||
#pragma mark Auto-pause
|
||||
|
||||
- (BOOL) autoPauseMusicEnabled {
|
||||
return [self getBool:BGMDefaults_AutoPauseMusicEnabled];
|
||||
return [self getBool:kDefaultKeyAutoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled {
|
||||
[self setBool:BGMDefaults_AutoPauseMusicEnabled to:autoPauseMusicEnabled];
|
||||
[self setBool:kDefaultKeyAutoPauseMusicEnabled to:autoPauseMusicEnabled];
|
||||
}
|
||||
|
||||
- (NSArray<NSString*>*) preferredDeviceUIDs {
|
||||
NSArray<NSString*>* __nullable uids = [self get:kDefaultKeyPreferredDeviceUIDs];
|
||||
return uids ? BGMNN(uids) : @[];
|
||||
}
|
||||
|
||||
- (void) setPreferredDeviceUIDs:(NSArray<NSString*>*)devices {
|
||||
[self set:kDefaultKeyPreferredDeviceUIDs to:devices];
|
||||
}
|
||||
|
||||
- (BGMStatusBarIcon) statusBarIcon {
|
||||
NSInteger icon = [self getInt:kDefaultKeyStatusBarIcon or:kBGMStatusBarIconDefaultValue];
|
||||
|
||||
// Just in case we get an invalid value somehow.
|
||||
if ((icon < kBGMStatusBarIconMinValue) || (icon > kBGMStatusBarIconMaxValue)) {
|
||||
NSLog(@"BGMUserDefaults::statusBarIcon: Unknown BGMStatusBarIcon: %ld", (long)icon);
|
||||
icon = kBGMStatusBarIconDefaultValue;
|
||||
}
|
||||
|
||||
return (BGMStatusBarIcon)icon;
|
||||
}
|
||||
|
||||
- (void) setStatusBarIcon:(BGMStatusBarIcon)icon {
|
||||
[self setInt:kDefaultKeyStatusBarIcon to:icon];
|
||||
}
|
||||
|
||||
#pragma mark Google Play Music Desktop Player
|
||||
|
||||
- (NSString* __nullable) googlePlayMusicDesktopPlayerPermanentAuthCode {
|
||||
// Try to read the permanent auth code from the user's keychain.
|
||||
NSDictionary<NSString*, NSObject*>* query = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode,
|
||||
(__bridge NSString*)kSecMatchLimit: (__bridge NSString*)kSecMatchLimitOne,
|
||||
(__bridge NSString*)kSecReturnData: @YES
|
||||
};
|
||||
|
||||
CFTypeRef result = nil;
|
||||
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
|
||||
NSString* __nullable authCode = nil;
|
||||
|
||||
// Check the return status, null check and check the type.
|
||||
if ((err == errSecSuccess) && result && (CFGetTypeID(result) == CFDataGetTypeID())) {
|
||||
// Convert it to a string.
|
||||
CFStringRef __nullable code =
|
||||
CFStringCreateFromExternalRepresentation(kCFAllocatorDefault,
|
||||
result,
|
||||
kCFStringEncodingUTF8);
|
||||
authCode = (__bridge_transfer NSString* __nullable)code;
|
||||
} else if (err != errSecItemNotFound) {
|
||||
NSString* __nullable errMsg =
|
||||
(__bridge_transfer NSString* __nullable)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to read GPMDP auth code from keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
|
||||
// Release the data we read.
|
||||
if (result) {
|
||||
CFRelease(result);
|
||||
}
|
||||
|
||||
return authCode;
|
||||
}
|
||||
|
||||
- (void) setGooglePlayMusicDesktopPlayerPermanentAuthCode:(NSString* __nullable)authCode {
|
||||
if (authCode) {
|
||||
// Convert it to an NSData so we can store it in the user's keychain.
|
||||
NSData* authCodeData = [authCode dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
// Delete the old code if necessary. (There's an update function, but this takes less code.)
|
||||
if (self.googlePlayMusicDesktopPlayerPermanentAuthCode) {
|
||||
[self deleteGPMDPPermanentAuthCode];
|
||||
}
|
||||
|
||||
// Store the code.
|
||||
[self addGPMDPPermanentAuthCode:authCodeData];
|
||||
} else {
|
||||
[self deleteGPMDPPermanentAuthCode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) addGPMDPPermanentAuthCode:(NSData*)authCodeData {
|
||||
NSDictionary<NSString*, NSObject*>* attributes = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode,
|
||||
(__bridge NSString*)kSecValueData: authCodeData
|
||||
};
|
||||
|
||||
OSStatus err = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
|
||||
|
||||
// Just log an error if it failed.
|
||||
if (err != errSecSuccess) {
|
||||
NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to store GPMDP auth code in keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) deleteGPMDPPermanentAuthCode {
|
||||
NSDictionary<NSString*, NSObject*>* query = @{
|
||||
(__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword,
|
||||
(__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode
|
||||
};
|
||||
|
||||
OSStatus err = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
// Just log an error if it failed.
|
||||
if (err != errSecSuccess) {
|
||||
NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil);
|
||||
NSLog(@"Failed to delete GPMDP auth code from keychain: %d, %@", err, errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark General Accessors
|
||||
|
||||
- (id __nullable) get:(NSString*)key {
|
||||
return defaults ? [defaults objectForKey:key] : transientDefaults[key];
|
||||
}
|
||||
@@ -88,6 +214,7 @@ static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayer
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This method should have a default value param.
|
||||
- (BOOL) getBool:(NSString*)key {
|
||||
return defaults ? [defaults boolForKey:key] : [transientDefaults[key] boolValue];
|
||||
}
|
||||
@@ -96,7 +223,32 @@ static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayer
|
||||
if (defaults) {
|
||||
[defaults setBool:value forKey:key];
|
||||
} else {
|
||||
transientDefaults[key] = [NSNumber numberWithBool:value];
|
||||
transientDefaults[key] = @(value);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger) getInt:(NSString*)key or:(NSInteger)valueIfNil
|
||||
{
|
||||
if (defaults) {
|
||||
if ([defaults objectForKey:key]) {
|
||||
return [defaults integerForKey:key];
|
||||
} else {
|
||||
return valueIfNil;
|
||||
}
|
||||
} else {
|
||||
if (transientDefaults[key]) {
|
||||
return [transientDefaults[key] intValue];
|
||||
} else {
|
||||
return valueIfNil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setInt:(NSString*)key to:(NSInteger)value {
|
||||
if (defaults) {
|
||||
[defaults setInteger:value forKey:key];
|
||||
} else {
|
||||
transientDefaults[key] = @(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMVolumeChangeListener.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGMVolumeChangeListener.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAException.h"
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wexit-time-destructors"
|
||||
const static std::vector<CAPropertyAddress> kVolumeChangeProperties = {
|
||||
// Output volume changes
|
||||
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput),
|
||||
// Mute/unmute
|
||||
CAPropertyAddress(kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput),
|
||||
// Received when controls are added to or removed from the device.
|
||||
CAPropertyAddress(kAudioObjectPropertyControlList),
|
||||
// Received when the device has changed and "clients should re-evaluate everything they need
|
||||
// to know about the device, particularly the layout and values of the controls".
|
||||
CAPropertyAddress(kAudioDevicePropertyDeviceHasChanged)
|
||||
};
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
BGMVolumeChangeListener::BGMVolumeChangeListener(BGMAudioDevice device,
|
||||
std::function<void(void)> handler)
|
||||
:
|
||||
mDevice(device)
|
||||
{
|
||||
// Register a listener that will update the slider when the user changes the volume or
|
||||
// mutes/unmutes their audio.
|
||||
mListenerBlock =
|
||||
Block_copy(^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
|
||||
// at least one property the block is listening to, so there's no need to check it.
|
||||
(void)inNumberAddresses;
|
||||
(void)inAddresses;
|
||||
|
||||
// Call the callback.
|
||||
handler();
|
||||
});
|
||||
|
||||
// Register for a number of properties that might indicate that clients need to update. For
|
||||
// example, the mute property changing means UI elements that display the volume will need to be
|
||||
// updated, even though it's not strictly a change in volume.
|
||||
for(CAPropertyAddress property : kVolumeChangeProperties)
|
||||
{
|
||||
// Instead of swallowing exceptions here, we could try again later, but I doubt it would be
|
||||
// worth the effort. And the documentation doesn't actually explain what could cause this
|
||||
// call to fail.
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
mDevice.AddPropertyListenerBlock(property, dispatch_get_main_queue(), mListenerBlock);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BGMVolumeChangeListener::~BGMVolumeChangeListener()
|
||||
{
|
||||
// Deregister and release the listener block.
|
||||
for(CAPropertyAddress property : kVolumeChangeProperties)
|
||||
{
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
mDevice.RemovePropertyListenerBlock(property,
|
||||
dispatch_get_main_queue(),
|
||||
mListenerBlock);
|
||||
});
|
||||
}
|
||||
|
||||
Block_release(mListenerBlock);
|
||||
}
|
||||
|
||||
#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/>.
|
||||
|
||||
//
|
||||
// BGMVolumeChangeListener.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#include "BGMBackgroundMusicDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
// STL Includes
|
||||
#include <functional>
|
||||
|
||||
// System Includes
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGMVolumeChangeListener
|
||||
{
|
||||
|
||||
public:
|
||||
/*!
|
||||
* @param device Listens for notifications about this device.
|
||||
* @param handler The function to call when the device's volume (or mute) changes. Called on the
|
||||
* main queue.
|
||||
*/
|
||||
BGMVolumeChangeListener(BGMAudioDevice device, std::function<void(void)> handler);
|
||||
virtual ~BGMVolumeChangeListener();
|
||||
BGMVolumeChangeListener(const BGMVolumeChangeListener&) = delete;
|
||||
BGMVolumeChangeListener& operator=(const BGMVolumeChangeListener&) = delete;
|
||||
|
||||
private:
|
||||
AudioObjectPropertyListenerBlock mListenerBlock;
|
||||
BGMAudioDevice mDevice;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14835.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<development version="8000" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14835.7"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -40,6 +40,10 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" tag="4" id="rkf-nx-H2J"/>
|
||||
<menuItem title="Output Device" tag="5" enabled="NO" id="chk-9C-pab">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="D3z-zv-JrJ"/>
|
||||
<menuItem title="Preferences" tag="1" id="BHb-uh-9Zm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Preferences" id="Img-Dh-cpU">
|
||||
@@ -48,11 +52,17 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="nb1-jq-97L"/>
|
||||
<menuItem title="Output Device" tag="2" enabled="NO" id="chk-9C-pab">
|
||||
<menuItem title="Status Bar Icon" enabled="NO" id="CmD-ot-1wE">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="D3z-zv-JrJ"/>
|
||||
<menuItem title="About Background Music" tag="3" id="R45-Vo-Eto">
|
||||
<menuItem title="Background Music Logo" state="on" tag="2" id="9VF-qy-6fh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Volume Icon" tag="3" toolTip="todo" id="B47-O2-wd0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="pYP-Fy-nKA"/>
|
||||
<menuItem title="About Background Music" tag="4" id="R45-Vo-Eto">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
@@ -76,7 +86,7 @@
|
||||
<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"/>
|
||||
<font key="font" metaFont="message" size="11"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
@@ -99,7 +109,7 @@
|
||||
<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"/>
|
||||
<rect key="frame" x="243" y="27" width="16" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<contentFilters>
|
||||
<ciFilter name="CIAffineTransform">
|
||||
@@ -135,7 +145,7 @@
|
||||
</subviews>
|
||||
<point key="canvasLocation" x="117" y="-45"/>
|
||||
</customView>
|
||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="Cf4-3V-gl1" customClass="NSPanel">
|
||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" 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="1002" height="335"/>
|
||||
@@ -148,7 +158,7 @@
|
||||
<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"/>
|
||||
<font key="font" metaFont="system" size="18"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
@@ -156,7 +166,7 @@
|
||||
<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.2.0" id="FDH-7l-wFf">
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Version 0.3.1" 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"/>
|
||||
@@ -175,15 +185,6 @@
|
||||
<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" 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"/>
|
||||
@@ -212,14 +213,15 @@
|
||||
<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="415" y="45" width="567" height="245"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
|
||||
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="Cdb-RA-YK0">
|
||||
<rect key="frame" x="1" y="1" width="565" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" id="LSG-PF-cl8">
|
||||
<rect key="frame" x="-6" y="0.0" width="577" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="minSize" width="565" height="243"/>
|
||||
<size key="maxSize" width="594" height="10000000"/>
|
||||
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -228,13 +230,12 @@
|
||||
</allowedInputSourceLocales>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="3jV-aP-AB7">
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="3jV-aP-AB7">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
|
||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
|
||||
<rect key="frame" x="550" y="1" width="16" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
@@ -248,6 +249,15 @@
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<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-2019 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>
|
||||
</subviews>
|
||||
</view>
|
||||
<point key="canvasLocation" x="-200" y="232.5"/>
|
||||
@@ -300,7 +310,7 @@
|
||||
<rect key="frame" x="42" y="1" width="86" height="14"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="System Sounds" id="ATK-L8-s8z">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<font key="font" metaFont="message" size="11"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
@@ -319,53 +329,53 @@
|
||||
<image name="NSComputer" width="32" height="32"/>
|
||||
<image name="buttonCell:IXo-C7-3uE:image" width="1" height="1">
|
||||
<mutableData key="keyedArchiveRepresentation">
|
||||
YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU
|
||||
GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw
|
||||
c1dOU0NvbG9ygAKADRIgwwAAgAOAC1Z7MSwgMX3SFQoWGFpOUy5vYmplY3RzoReABIAK0hUKGh2iGxyA
|
||||
BYAGgAkQANIgCiEiXxAUTlNUSUZGUmVwcmVzZW50YXRpb26AB4AITxEIxE1NACoAAAAKAAAAEAEAAAMA
|
||||
AAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEGAAMAAAABAAEAAAEKAAMA
|
||||
AAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEWAAMAAAABAAEAAAEXAAQA
|
||||
AAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFTAAMAAAACAAEAAYdzAAcA
|
||||
AAf0AAAA0AAAAAAAAAf0YXBwbAIgAABtbnRyR1JBWVhZWiAH0AACAA4ADAAAAABhY3NwQVBQTAAAAABu
|
||||
b25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAG9kc2NtAAABMAAABmZjcHJ0AAAHmAAAADh3
|
||||
dHB0AAAH0AAAABRrVFJDAAAH5AAAAA5kZXNjAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAA
|
||||
AAAAAAAAFUdlbmVyaWMgR3JheSBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNrU0sAAAAqAAABhGVuVVMAAAAoAAABrmNhRVMA
|
||||
AAAsAAAB1nZpVk4AAAAsAAACAnB0QlIAAAAqAAACLnVrVUEAAAAsAAACWGZyRlUAAAAqAAAChGh1SFUA
|
||||
AAAuAAACrnpoVFcAAAAQAAAC3G5iTk8AAAAsAAAC7GtvS1IAAAAYAAADGGNzQ1oAAAAkAAADMGhlSUwA
|
||||
AAAgAAADVHJvUk8AAAAkAAADdGRlREUAAAA6AAADmGl0SVQAAAAuAAAD0nN2U0UAAAAuAAAEAHpoQ04A
|
||||
AAAQAAAELmphSlAAAAAWAAAEPmVsR1IAAAAkAAAEVHB0UE8AAAA4AAAEeG5sTkwAAAAqAAAEsGVzRVMA
|
||||
AAAoAAAE2nRoVEgAAAAkAAAFAnRyVFIAAAAiAAAFJmZpRkkAAAAsAAAFSGhySFIAAAA6AAAFdHBsUEwA
|
||||
AAA2AAAFrnJ1UlUAAAAmAAAF5GFyRUcAAAAoAAAGCmRhREsAAAA0AAAGMgBWAWEAZQBvAGIAZQBjAG4A
|
||||
/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkAbABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAFAAcgBvAGYA
|
||||
aQBsAGUAUABlAHIAZgBpAGwAIABkAGUAIABnAHIAaQBzACAAZwBlAG4A6AByAGkAYwBDHqUAdQAgAGgA
|
||||
7ABuAGgAIABNAOAAdQAgAHgA4QBtACAAQwBoAHUAbgBnAFAAZQByAGYAaQBsACAAQwBpAG4AegBhACAA
|
||||
RwBlAG4A6QByAGkAYwBvBBcEMAQzBDAEOwRMBD0EOAQ5ACAEPwRABD4ERAQwBDkEOwAgAEcAcgBhAHkA
|
||||
UAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAGcAcgBpAHMAwQBsAHQAYQBsAOEAbgBvAHMA
|
||||
IABzAHoA/AByAGsAZQAgAHAAcgBvAGYAaQBskBp1KHBwlo6Ccl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsA
|
||||
IABnAHIA5QB0AG8AbgBlAHAAcgBvAGYAaQBsx3y8GAAgAEcAcgBhAHkAINUEuFzTDMd8AE8AYgBlAGMA
|
||||
bgD9ACABYQBlAGQA/QAgAHAAcgBvAGYAaQBsBeQF6AXVBeQF2QXcACAARwByAGEAeQAgBdsF3AXcBdkA
|
||||
UAByAG8AZgBpAGwAIABnAHIAaQAgAGcAZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAA
|
||||
RwByAGEAdQBzAHQAdQBmAGUAbgAtAFAAcgBvAGYAaQBsAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkA
|
||||
bwAgAGcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QBzAGsAYQBsAGUAcAByAG8A
|
||||
ZgBpAGxmbpAacHBepmPPj/Blh072TgCCLDCwMOwwpDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA8AD
|
||||
wQO/A8YDrwO7ACADswO6A8EDuQBQAGUAcgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMA
|
||||
aQBuAHoAZQBuAHQAbwBzAEEAbABnAGUAbQBlAGUAbgAgAGcAcgBpAGoAcwBwAHIAbwBmAGkAZQBsAFAA
|
||||
ZQByAGYAaQBsACAAZwByAGkAcwAgAGcAZQBuAOkAcgBpAGMAbw5CDhsOIw5EDh8OJQ5MDioONQ5ADhcO
|
||||
Mg4XDjEOSA4nDkQOGwBHAGUAbgBlAGwAIABHAHIAaQAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUA
|
||||
bgAgAGgAYQByAG0AYQBhAHAAcgBvAGYAaQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAHAAcgBvAGYA
|
||||
aQBsACAAcwBpAHYAaQBoACAAdABvAG4AbwB2AGEAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8A
|
||||
ZgBpAGwAIABzAHoAYQByAG8BWwBjAGkEHgQxBEkEOAQ5ACAEQQQ1BEAESwQ5ACAEPwRABD4ERAQ4BDsE
|
||||
TAZFBkQGQQAgBioGOQYxBkoGQQAgAEcAcgBhAHkAIAYnBkQGOQYnBkUARwBlAG4AZQByAGUAbAAgAGcA
|
||||
cgDlAHQAbwBuAGUAYgBlAHMAawByAGkAdgBlAGwAcwBlAAB0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFw
|
||||
cGxlIEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAA
|
||||
AAABAc0AANIlJicoWiRjbGFzc25hbWVYJGNsYXNzZXNfEBBOU0JpdG1hcEltYWdlUmVwoycpKlpOU0lt
|
||||
YWdlUmVwWE5TT2JqZWN00iUmLC1XTlNBcnJheaIsKtIlJi8wXk5TTXV0YWJsZUFycmF5oy8sKtMyMwo0
|
||||
NTZXTlNXaGl0ZVxOU0NvbG9yU3BhY2VEMCAwABADgAzSJSY4OVdOU0NvbG9yojgq0iUmOzxXTlNJbWFn
|
||||
ZaI7Kl8QD05TS2V5ZWRBcmNoaXZlctE/QFRyb290gAEACAARABoAIwAtADIANwBGAEwAVwBeAGUAcgB5
|
||||
AIEAgwCFAIoAjACOAJUAmgClAKcAqQCrALAAswC1ALcAuQC7AMAA1wDZANsJowmoCbMJvAnPCdMJ3gnn
|
||||
CewJ9An3CfwKCwoPChYKHgorCjAKMgo0CjkKQQpECkkKUQpUCmYKaQpuAAAAAAAAAgEAAAAAAAAAQQAA
|
||||
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T
|
||||
S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwXGB0iIycoLzI1Oz5VJG51bGzVDQ4PEBESExQVFlZOU1Np
|
||||
emVWJGNsYXNzXE5TSW1hZ2VGbGFnc1ZOU1JlcHNXTlNDb2xvcoACgA0SIMMAAIADgAtWezEsIDF90hkO
|
||||
GhxaTlMub2JqZWN0c6EbgASACtIZDh4hoh8ggAWABoAJEADSJA4lJl8QFE5TVElGRlJlcHJlc2VudGF0
|
||||
aW9ugAeACE8RCMRNTQAqAAAACgAAABABAAADAAAAAQABAAABAQADAAAAAQABAAABAgADAAAAAgAIAAgB
|
||||
AwADAAAAAQABAAABBgADAAAAAQABAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgBEgADAAAAAQABAAAB
|
||||
FQADAAAAAQACAAABFgADAAAAAQABAAABFwAEAAAAAQAAAAIBHAADAAAAAQABAAABKAADAAAAAQACAAAB
|
||||
UgADAAAAAQABAAABUwADAAAAAgABAAGHcwAHAAAH9AAAANAAAAAAAAAH9GFwcGwCIAAAbW50ckdSQVlY
|
||||
WVogB9AAAgAOAAwAAAAAYWNzcEFQUEwAAAAAbm9uZQAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1h
|
||||
cHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZGVzYwAAAMAA
|
||||
AABvZHNjbQAAATAAAAZmY3BydAAAB5gAAAA4d3RwdAAAB9AAAAAUa1RSQwAAB+QAAAAOZGVzYwAAAAAA
|
||||
AAAVR2VuZXJpYyBHcmF5IFByb2ZpbGUAAAAAAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAHwAAAAxz
|
||||
a1NLAAAAKgAAAYRlblVTAAAAKAAAAa5jYUVTAAAALAAAAdZ2aVZOAAAALAAAAgJwdEJSAAAAKgAAAi51
|
||||
a1VBAAAALAAAAlhmckZVAAAAKgAAAoRodUhVAAAALgAAAq56aFRXAAAAEAAAAtxuYk5PAAAALAAAAuxr
|
||||
b0tSAAAAGAAAAxhjc0NaAAAAJAAAAzBoZUlMAAAAIAAAA1Ryb1JPAAAAJAAAA3RkZURFAAAAOgAAA5hp
|
||||
dElUAAAALgAAA9JzdlNFAAAALgAABAB6aENOAAAAEAAABC5qYUpQAAAAFgAABD5lbEdSAAAAJAAABFRw
|
||||
dFBPAAAAOAAABHhubE5MAAAAKgAABLBlc0VTAAAAKAAABNp0aFRIAAAAJAAABQJ0clRSAAAAIgAABSZm
|
||||
aUZJAAAALAAABUhockhSAAAAOgAABXRwbFBMAAAANgAABa5ydVJVAAAAJgAABeRhckVHAAAAKAAABgpk
|
||||
YURLAAAANAAABjIAVgFhAGUAbwBiAGUAYwBuAP0AIABzAGkAdgD9ACAAcAByAG8AZgBpAGwARwBlAG4A
|
||||
ZQByAGkAYwAgAEcAcgBhAHkAIABQAHIAbwBmAGkAbABlAFAAZQByAGYAaQBsACAAZABlACAAZwByAGkA
|
||||
cwAgAGcAZQBuAOgAcgBpAGMAQx6lAHUAIABoAOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4A
|
||||
ZwBQAGUAcgBmAGkAbAAgAEMAaQBuAHoAYQAgAEcAZQBuAOkAcgBpAGMAbwQXBDAEMwQwBDsETAQ9BDgE
|
||||
OQAgBD8EQAQ+BEQEMAQ5BDsAIABHAHIAYQB5AFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1AGUA
|
||||
IABnAHIAaQBzAMEAbAB0AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABwAHIAbwBmAGkAbJAadShw
|
||||
cJaOgnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAZwByAOUAdABvAG4AZQBwAHIAbwBmAGkAbMd8vBgA
|
||||
IABHAHIAYQB5ACDVBLhc0wzHfABPAGIAZQBjAG4A/QAgAWEAZQBkAP0AIABwAHIAbwBmAGkAbAXkBegF
|
||||
1QXkBdkF3AAgAEcAcgBhAHkAIAXbBdwF3AXZAFAAcgBvAGYAaQBsACAAZwByAGkAIABnAGUAbgBlAHIA
|
||||
aQBjAEEAbABsAGcAZQBtAGUAaQBuAGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBmAGkA
|
||||
bABQAHIAbwBmAGkAbABvACAAZwByAGkAZwBpAG8AIABnAGUAbgBlAHIAaQBjAG8ARwBlAG4AZQByAGkA
|
||||
cwBrACAAZwByAOUAcwBrAGEAbABlAHAAcgBvAGYAaQBsZm6QGnBwXqZjz4/wZYdO9k4AgiwwsDDsMKQw
|
||||
1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68DuwAgA7MDugPBA7kAUABlAHIAZgBpAGwA
|
||||
IABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUAbgB0AG8AcwBBAGwAZwBlAG0AZQBlAG4A
|
||||
IABnAHIAaQBqAHMAcAByAG8AZgBpAGUAbABQAGUAcgBmAGkAbAAgAGcAcgBpAHMAIABnAGUAbgDpAHIA
|
||||
aQBjAG8OQg4bDiMORA4fDiUOTA4qDjUOQA4XDjIOFw4xDkgOJw5EDhsARwBlAG4AZQBsACAARwByAGkA
|
||||
IABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBwAHIAbwBmAGkAaQBsAGkA
|
||||
RwBlAG4AZQByAGkBDQBrAGkAIABwAHIAbwBmAGkAbAAgAHMAaQB2AGkAaAAgAHQAbwBuAG8AdgBhAFUA
|
||||
bgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpBB4EMQRJBDgE
|
||||
OQAgBEEENQRABEsEOQAgBD8EQAQ+BEQEOAQ7BEwGRQZEBkEAIAYqBjkGMQZKBkEAIABHAHIAYQB5ACAG
|
||||
JwZEBjkGJwZFAEcAZQBuAGUAcgBlAGwAIABnAHIA5QB0AG8AbgBlAGIAZQBzAGsAcgBpAHYAZQBsAHMA
|
||||
ZQAAdGV4dAAAAABDb3B5cmlnaHQgMjAwNyBBcHBsZSBJbmMuLCBhbGwgcmlnaHRzIHJlc2VydmVkLgBY
|
||||
WVogAAAAAAAA81EAAQAAAAEWzGN1cnYAAAAAAAAAAQHNAADSKSorLFokY2xhc3NuYW1lWCRjbGFzc2Vz
|
||||
XxAQTlNCaXRtYXBJbWFnZVJlcKMrLS5aTlNJbWFnZVJlcFhOU09iamVjdNIpKjAxV05TQXJyYXmiMC7S
|
||||
KSozNF5OU011dGFibGVBcnJheaMzMC7TNjcOODk6V05TV2hpdGVcTlNDb2xvclNwYWNlRDAgMAAQA4AM
|
||||
0ikqPD1XTlNDb2xvcqI8LtIpKj9AV05TSW1hZ2WiPy4ACAARABoAJAApADIANwBJAEwAUQBTAGIAaABz
|
||||
AHoAgQCOAJUAnQCfAKEApgCoAKoAsQC2AMEAwwDFAMcAzADPANEA0wDVANcA3ADzAPUA9wm/CcQJzwnY
|
||||
CesJ7wn6CgMKCAoQChMKGAonCisKMgo6CkcKTApOClAKVQpdCmAKZQptAAAAAAAAAgEAAAAAAAAAQQAA
|
||||
AAAAAAAAAAAAAAAACnA
|
||||
</mutableData>
|
||||
</image>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"filename" : "Volume0.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"filename" : "Volume1.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"filename" : "Volume2.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"filename" : "Volume3.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -28,12 +28,16 @@
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>Background Music needs to control your music player app if you want it to automatically pause your music.</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2019 Background Music contributors</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The "Background Music" virtual audio device sends system audio to Background Music (the app) through a virtual input device.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSServices</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMDecibel.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -40,11 +40,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) 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];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -54,6 +54,11 @@
|
||||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.decibel.running;
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
// We have a lot more code for GPMDP than most music players largely because GPMDP has a WebSockets
|
||||
// API and because the user has to enter a code from GPMDP to allow BGMApp to control it.
|
||||
// Currently, the other music players all have AppleScript APIs, so for them the OS asks the user
|
||||
// for permission on our behalf automatically and handles the whole process for us.
|
||||
//
|
||||
// This class implements the usual BGMMusicPlayer methods and handles the UI for authenticating
|
||||
// with GPMDP. BGMGooglePlayMusicDesktopPlayerConnection manages the connection to GPMDP and hides
|
||||
// the details of its API.
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
API_AVAILABLE(macos(10.10))
|
||||
@interface BGMGooglePlayMusicDesktopPlayer : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMGooglePlayMusicDesktopPlayer.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
#import "BGMGooglePlayMusicDesktopPlayerConnection.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMGooglePlayMusicDesktopPlayer {
|
||||
BGMUserDefaults* userDefaults;
|
||||
BGMGooglePlayMusicDesktopPlayerConnection* connection;
|
||||
BGMAppWatcher* appWatcher;
|
||||
|
||||
// True while the auth code dialog is open. The user types in the four-digit auth code from
|
||||
// GPMDP when we connect to it for the first time.
|
||||
BOOL showingAuthCodeDialog;
|
||||
// True if the user has cancelled the auth code dialog. We only show the auth code dialog again
|
||||
// after the user has changed the music player and then changed it back to GPMDP (or restarted
|
||||
// BGMApp).
|
||||
BOOL authCancelled;
|
||||
}
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
|
||||
return @[[[self alloc] initWithUserDefaults:userDefaults]];
|
||||
}
|
||||
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen (the
|
||||
// command line tool).
|
||||
NSUUID* playerID = [BGMMusicPlayerBase makeID:@"FCDCC01F-4BF1-4AD2-BE3E-6B7659A90A3F"];
|
||||
if ((self = [super initWithMusicPlayerID:playerID
|
||||
name:@"GPMDP"
|
||||
toolTip:@"Google Play Music Desktop Player"
|
||||
bundleID:@"google-play-music-desktop-player"])) {
|
||||
userDefaults = defaults;
|
||||
showingAuthCodeDialog = NO;
|
||||
authCancelled = NO;
|
||||
|
||||
// We don't strictly need to use a weak ref (at least not yet), but it doesn't hurt.
|
||||
BGMGooglePlayMusicDesktopPlayer* __weak weakSelf = self;
|
||||
|
||||
connection = [[BGMGooglePlayMusicDesktopPlayerConnection alloc]
|
||||
initWithUserDefaults:userDefaults
|
||||
authRequiredHandler:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
return [strongSelf requestAuthCodeFromUser];
|
||||
}
|
||||
connectionErrorHandler:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf showConnectionErrorDialog];
|
||||
}
|
||||
apiVersionMismatchHandler:^(NSString* reportedAPIVersion) {
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf showAPIVersionMismatchDialog:reportedAPIVersion];
|
||||
}];
|
||||
|
||||
// Set up callbacks that run when GPMDP is opened or closed.
|
||||
appWatcher = [[BGMAppWatcher alloc]
|
||||
initWithBundleID:BGMNN(self.bundleID)
|
||||
appLaunched:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf gpmdpWasLaunched];
|
||||
}
|
||||
appTerminated:^{
|
||||
BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
|
||||
[strongSelf gpmdpWasTerminated];
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) gpmdpWasLaunched {
|
||||
if (self.selected) {
|
||||
// Reconnect so we can control GPMDP.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasLaunched: GPMDP launched. Connecting");
|
||||
|
||||
// Try up to 10 times because GPMDP won't start accepting connections until it's finished
|
||||
// starting up.
|
||||
//
|
||||
// TODO: If GPMDP shows an alert before it finishes launching, it doesn't start accepting
|
||||
// connections until the alert is dismissed, which can make this can timeout.
|
||||
// TODO: Is the error dialog still shown if the user closes GPMDP again while we're
|
||||
// retrying? It shouldn't be.
|
||||
[connection connectWithRetries:10];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) gpmdpWasTerminated {
|
||||
if (self.selected) {
|
||||
// Allow the connection to clean up and reset itself.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasTerminated: GPMDP has been closed.");
|
||||
[connection disconnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
|
||||
// Allow the auth code dialog to be shown again if we were hiding it because the user cancelled
|
||||
// it last time.
|
||||
authCancelled = NO;
|
||||
|
||||
if (self.running) {
|
||||
// Only retry once so the error message is shown fairly quickly if we fail to connect.
|
||||
[connection connectWithRetries:1];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) wasDeselected {
|
||||
[super wasDeselected];
|
||||
[connection disconnect];
|
||||
}
|
||||
|
||||
- (NSString* __nullable) requestAuthCodeFromUser {
|
||||
if (showingAuthCodeDialog) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
|
||||
"Already showing the auth code dialog");
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (authCancelled) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
|
||||
"Previously cancelled. Doing nothing.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
showingAuthCodeDialog = YES;
|
||||
|
||||
// Ask the user to read the auth code from GPMDP and type it in to BGMApp.
|
||||
NSString* __nullable authCode = [self showAuthCodeDialog];
|
||||
|
||||
showingAuthCodeDialog = NO;
|
||||
|
||||
return authCode;
|
||||
}
|
||||
|
||||
- (NSString* __nullable) showAuthCodeDialog {
|
||||
// When this isn't being called because the user just changed something in BGMApp (e.g. GPMDP
|
||||
// was closed, they selected it in BGMApp for the first time, then opened GPMDP later), we could
|
||||
// use notifications instead of an NSAlert. But it probably wouldn't happen often enough to be
|
||||
// worth the effort.
|
||||
NSAlert* alert = [NSAlert new];
|
||||
alert.messageText = @"Background Music needs permission to control GPMDP.";
|
||||
alert.informativeText = @"It should be displaying a four-digit code for you to enter.";
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert addButtonWithTitle:@"Cancel"];
|
||||
|
||||
// The text field to type the auth code in.
|
||||
// TODO: Can we derive these dimensions from something instead of hardcoding them?
|
||||
NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 50, 24)];
|
||||
[alert setAccessoryView:input];
|
||||
|
||||
// Focus the text field (so the user doesn't have to do it themselves).
|
||||
[alert.window setInitialFirstResponder:input];
|
||||
|
||||
// Bring GMPDP to the front, underneath our NSAlert, so the user can see the auth code.
|
||||
[self showGPMDPBehindAuthCodeDialog];
|
||||
|
||||
NSModalResponse buttonPressed = [alert runModal];
|
||||
|
||||
if (buttonPressed == NSAlertFirstButtonReturn) {
|
||||
// Set input's value to the text entered by the user so we can access it.
|
||||
[input validateEditing];
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: Got auth code: <private>");
|
||||
return input.stringValue;
|
||||
} else {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: "
|
||||
"The user cancelled the auth code dialog");
|
||||
authCancelled = YES;
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showGPMDPBehindAuthCodeDialog {
|
||||
// Dispatched because if we do this just before showing the auth code dialog, the user's current
|
||||
// active window will be deactivated, the auth code dialog will become the active window and
|
||||
// macOS will act as if the user activated it themselves. To avoid stealing key focus, it won't
|
||||
// activate GPMDP.
|
||||
//
|
||||
// We could pass NSApplicationActivateIgnoringOtherApps to activateWithOptions instead, but then
|
||||
// GPMDP would be activated even if the user really did activate a different application, which
|
||||
// would steal focus from it.
|
||||
//
|
||||
// 250 ms is a reasonable value on my system, but won't always be long enough. When it isn't,
|
||||
// GPMDP won't be activated, but that just means the user will have to do it themselves.
|
||||
const int64_t delay = 250 * NSEC_PER_MSEC;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
// Make GMPDP the frontmost app.
|
||||
NSArray<NSRunningApplication*>* gpmdpApps =
|
||||
[NSRunningApplication
|
||||
runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];
|
||||
|
||||
if (gpmdpApps.count > 0) {
|
||||
[gpmdpApps[0] activateWithOptions:0];
|
||||
}
|
||||
|
||||
// Focus the auth code dialog. It will already be in front of GPMDP because
|
||||
// it's modal. Dispatched for the same reason as above.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void) showConnectionErrorDialog {
|
||||
NSString* errorMsg = @"Could not connect to Google Play Music Desktop Player";
|
||||
NSString* troubleshootingMsg =
|
||||
[NSString stringWithFormat:
|
||||
@"Make sure \"Enable JSON API\" and \"Enable Playback API\" are both checked in GPMDP's "
|
||||
"settings, then restart GPMDP.\n\n"
|
||||
"GPMDP should be listening on its default port, 5672.\n\n"
|
||||
"Consider filing a bug report at %s",
|
||||
kBGMIssueTrackerURL];
|
||||
|
||||
[self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
|
||||
}
|
||||
|
||||
- (void) showAPIVersionMismatchDialog:(NSString*)reportedAPIVersion {
|
||||
NSString* errorMsg = @"Google Play Music Desktop Player Version Not Supported";
|
||||
NSString* troubleshootingMsg =
|
||||
[NSString stringWithFormat:
|
||||
@"GPMDP reported its API version as \"%@\", which Background Music doesn't support "
|
||||
"yet. Background Music might not be able to control GPMDP properly.\n\n"
|
||||
"Feel free to open an issue about this at %s",
|
||||
reportedAPIVersion,
|
||||
kBGMIssueTrackerURL];
|
||||
|
||||
[self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
|
||||
}
|
||||
|
||||
- (void) showErrorDialog:(NSString*)errorMsg troubleshootingMsg:(NSString*)troubleshootingMsg {
|
||||
if (!self.running) {
|
||||
// GPMDP isn't running, so there's no need to inform the user. (The "Auto-pause GPMDP" menu
|
||||
// item will be greyed out, but that's handled elsewhere.)
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::showErrorDialog: Not running");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"%@", errorMsg);
|
||||
|
||||
// Show the error in a UI dialog.
|
||||
NSAlert* alert = [NSAlert new];
|
||||
alert.messageText = errorMsg;
|
||||
alert.informativeText = troubleshootingMsg;
|
||||
// TODO: Show the suppression checkbox and save its value in user defaults.
|
||||
alert.showsSuppressionButton = NO;
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
|
||||
[alert runModal];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// We have to check with NSRunningApplication instead of just setting a flag in appWatcher's
|
||||
// callbacks because BGMAutoPauseMenuItem calls this method when it's notified by its own
|
||||
// instance of BGMAppWatcher. If BGMAutoPauseMenuItem got notified first, the flag wouldn't be
|
||||
// updated in time.
|
||||
//
|
||||
// At some point we might want to try to avoid this by making the BGMMusicPlayers' running
|
||||
// properties observable.
|
||||
NSArray<NSRunningApplication*>* instances =
|
||||
[NSRunningApplication runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];
|
||||
|
||||
return instances.count > 0;
|
||||
}
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && connection.playing;
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && connection.paused;
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here.
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::pause: Pausing Google Play Music Desktop "
|
||||
"Player");
|
||||
// There's a race condition here and in unpause because, if the user paused GPMDP just
|
||||
// before we called playPause, GPMDP would play instead of pausing. I'm not sure there's
|
||||
// much we can/should do about it.
|
||||
[connection playPause];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here.
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayer::unpause: Unpausing Google Play Music Desktop "
|
||||
"Player");
|
||||
[connection playPause];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayerConnection.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
API_AVAILABLE(macos(10.10))
|
||||
@interface BGMGooglePlayMusicDesktopPlayerConnection : NSObject<WKScriptMessageHandler>
|
||||
|
||||
// authRequiredHandler: A UI callback that asks the user for the auth code GPMDP will display.
|
||||
// Returns the auth code they entered, or nil.
|
||||
// connectionErrorHandler: A UI callback that shows a connection error message.
|
||||
// apiVersionMismatchHandler: A UI callback that shows a warning dialog explaining that GPMDP
|
||||
// reported an API version that we don't support yet.
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults
|
||||
authRequiredHandler:(NSString* __nullable (^)(void))authHandler
|
||||
connectionErrorHandler:(void (^)(void))errorHandler
|
||||
apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler;
|
||||
|
||||
// Returns before the connection has been fully established. The playing and paused properties will
|
||||
// remain false until the connection is complete, but playPause can be called at any time after
|
||||
// calling this method.
|
||||
//
|
||||
// If the connection fails, it will be retried after a one second delay, up to the number of times
|
||||
// given.
|
||||
- (void) connectWithRetries:(int)retries;
|
||||
- (void) disconnect;
|
||||
|
||||
// Tell GPMDP to play if it's paused or pause if it's playing.
|
||||
- (void) playPause;
|
||||
|
||||
@property (readonly) BOOL playing;
|
||||
@property (readonly) BOOL paused;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMGooglePlayMusicDesktopPlayerConnection.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMGooglePlayMusicDesktopPlayerConnection.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// When GooglePlayMusicDesktopPlayer.js sends a message to this class, it sets the message handler
|
||||
// name to one of these, which tells us what type of message it is. (This is a macro because you
|
||||
// can't make a static const NSArray.)
|
||||
#define kScriptMessageHandlerNames (@[@"gpmdp", @"log", @"error"])
|
||||
|
||||
@implementation BGMGooglePlayMusicDesktopPlayerConnection {
|
||||
// GPMDP has a WebSocket API, so we use a WKWebView to access it using Javascript. Using a
|
||||
// proper library would make the code a bit cleaner and save a little memory, but I'm not sure
|
||||
// it would be worth adding an external dependency for that.
|
||||
WKWebView* webView;
|
||||
NSString* __nullable permanentAuthCode;
|
||||
BGMUserDefaults* userDefaults;
|
||||
// The number of times to retry if we fail to connect. For example, if GPMDP is still starting
|
||||
// up. Set to 0 when we aren't trying to connect.
|
||||
int connectionRetries;
|
||||
|
||||
// A UI callback that asks the user for the auth code GPMDP will display.
|
||||
NSString* __nullable (^authRequiredHandler)(void);
|
||||
// A UI callback that shows a connection error message.
|
||||
void (^connectionErrorHandler)(void);
|
||||
// A UI callback that shows a warning dialog explaining that GPMDP reported an API version that
|
||||
// we don't support yet.
|
||||
void (^apiVersionMismatchHandler)(NSString* reportedAPIVersion);
|
||||
}
|
||||
|
||||
- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults
|
||||
authRequiredHandler:(NSString* __nullable (^)(void))authHandler
|
||||
connectionErrorHandler:(void (^)(void))errorHandler
|
||||
apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler {
|
||||
if((self = [super init])) {
|
||||
userDefaults = defaults;
|
||||
authRequiredHandler = authHandler;
|
||||
connectionErrorHandler = errorHandler;
|
||||
apiVersionMismatchHandler = apiVersionHandler;
|
||||
connectionRetries = 0;
|
||||
|
||||
// Lazily initialised.
|
||||
permanentAuthCode = nil;
|
||||
|
||||
// Report that GPMDP is stopped until we know otherwise.
|
||||
_playing = NO;
|
||||
_paused = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Creates and initialises webView, a WKWebView we use to communicate with GPMDP over WebSockets.
|
||||
- (void) createWebView {
|
||||
// Read the Javascript we'll need for this.
|
||||
NSString* __nullable jsPath =
|
||||
[[NSBundle mainBundle] pathForResource:@"GooglePlayMusicDesktopPlayer.js"
|
||||
ofType:nil];
|
||||
NSError* err;
|
||||
NSString* __nullable jsStr =
|
||||
(!jsPath ? nil : [NSString stringWithContentsOfFile:BGMNN(jsPath)
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&err]);
|
||||
|
||||
if (err || !jsStr || [jsStr isEqualToString:@""]) {
|
||||
// TODO: Return an error so the caller can show an error dialog or something.
|
||||
NSLog(@"Error loading GPMDP Javascript file: %@", err);
|
||||
} else {
|
||||
webView = [WKWebView new];
|
||||
|
||||
// Register to receive messages from our Javascript. The messages are handled in
|
||||
// userContentController. We register several times using different names as a convenient
|
||||
// way to separate messages from GPMDP, messages to log and errors.
|
||||
for (NSString* name in kScriptMessageHandlerNames) {
|
||||
[webView.configuration.userContentController addScriptMessageHandler:self name:name];
|
||||
}
|
||||
|
||||
// Load our Javascript functions into webView so we can call them later.
|
||||
[self evaluateJavaScript:BGMNN(jsStr)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) connectWithRetries:(int)retries {
|
||||
if (retries < 0) {
|
||||
BGMAssert(false, "retries < 0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!permanentAuthCode) {
|
||||
// Read the API auth code from user defaults (actually the keychain), if there is one. If
|
||||
// the user hasn't authenticated before, it will be nil.
|
||||
//
|
||||
// We do this lazily because it can show a password dialog in debug/unsigned builds.
|
||||
permanentAuthCode = userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode;
|
||||
}
|
||||
|
||||
connectionRetries = retries;
|
||||
|
||||
// Create the WKWebView we'll use to connect to GPMDP with WebSockets. Using a WKWebView means
|
||||
// Background Music uses a bit more memory while connected to GPMDP, around 15 MB for me, but
|
||||
// saves us having to complicate the build process to add a dependency on a proper library.
|
||||
[self createWebView];
|
||||
|
||||
if (permanentAuthCode) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: "
|
||||
"Connecting with auth code");
|
||||
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection
|
||||
toPercentEncoded:BGMNN(permanentAuthCode)];
|
||||
|
||||
[self evaluateJavaScript:[NSString stringWithFormat:@"connect('%@');", percentEncodedCode]];
|
||||
} else {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: "
|
||||
"Connecting without auth code");
|
||||
[self evaluateJavaScript:@"connect();"];
|
||||
}
|
||||
|
||||
// Check whether GPMDP is playing, paused or stopped.
|
||||
[self requestPlaybackState];
|
||||
}
|
||||
|
||||
- (void) disconnect {
|
||||
// Stop retrying if we're in the process of connecting.
|
||||
connectionRetries = 0;
|
||||
|
||||
// evaluateJavaScript is only safe to call on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::disconnect: Disconnecting");
|
||||
|
||||
[webView evaluateJavaScript:@"disconnect();"
|
||||
completionHandler:^(id __nullable result, NSError* __nullable error) {
|
||||
#pragma unused (result)
|
||||
if (error) {
|
||||
NSLog(@"Error closing connection to GPMDP: %@", error);
|
||||
}
|
||||
|
||||
// Allow the WKWebView to be garbage collected.
|
||||
for (NSString* name in kScriptMessageHandlerNames) {
|
||||
[webView.configuration.userContentController
|
||||
removeScriptMessageHandlerForName:name];
|
||||
}
|
||||
webView = nil;
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) evaluateJavaScript:(NSString*)js {
|
||||
// evaluateJavaScript is only safe to call on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[webView evaluateJavaScript:js
|
||||
completionHandler:^(id __nullable result, NSError* __nullable error) {
|
||||
#pragma unused (result)
|
||||
if (error) {
|
||||
// TODO: We should probably show an error dialog in some cases.
|
||||
NSLog(@"JS error: %@", error);
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) playPause {
|
||||
[self evaluateJavaScript:@"playPause();"];
|
||||
}
|
||||
|
||||
- (void) sendAuthCode:(NSString*)authCode {
|
||||
// Don't log the code itself just in case it could be a security problem.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendAuthCode: Sending GPMDP auth code");
|
||||
|
||||
// Percent-encode the user input just in case they entered something that could execute as
|
||||
// Javascript. We could limit the input to four digits instead, but this should be fine.
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:authCode];
|
||||
|
||||
// We send the message to GPMDP even if percentEncodedCode is nil so it will reply with an error
|
||||
// and BGMApp will ask the user for the auth code again.
|
||||
NSString* js = [NSString stringWithFormat:@"window.sendAuthCode('%@');", percentEncodedCode];
|
||||
[self evaluateJavaScript:js];
|
||||
}
|
||||
|
||||
- (void) sendPermanentAuthCode {
|
||||
NSString* __nullable code = permanentAuthCode;
|
||||
|
||||
if (code) {
|
||||
// Don't log the code itself just in case it could be a security problem.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: "
|
||||
"Sending GPMDP permanent auth code");
|
||||
|
||||
// Percent-encode it just in case something it includes could be executed as Javascript.
|
||||
NSString* __nullable percentEncodedCode =
|
||||
[BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:BGMNN(code)];
|
||||
|
||||
// Pass the code to our WKWebView so it can send it to GPMDP.
|
||||
NSString* js =
|
||||
[NSString stringWithFormat:@"sendPermanentAuthCode('%@');", percentEncodedCode];
|
||||
[self evaluateJavaScript:js];
|
||||
} else {
|
||||
NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: No code to send");
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString* __nullable)toPercentEncoded:(NSString*)rawString {
|
||||
// Just percent-encode every character (by passing an empty NSCharacterSet as the allowed
|
||||
// characters).
|
||||
NSString* __nullable percentEncoded = [rawString
|
||||
stringByAddingPercentEncodingWithAllowedCharacters:
|
||||
[NSCharacterSet characterSetWithCharactersInString:@""]];
|
||||
if (percentEncoded) {
|
||||
return percentEncoded;
|
||||
} else {
|
||||
// The docs say that stringByAddingPercentEncodingWithAllowedCharacters returns nil "if the
|
||||
// transformation is not possible", but don't explain when that could happen. According to
|
||||
// https://stackoverflow.com/a/33558934/1091063 it can be caused by the string containing
|
||||
// invalid unicode.
|
||||
NSLog(@"Could not encode");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Ask GPMDP whether it's playing, paused or stopped. The response is handled asynchronously in
|
||||
// handleResultMessage.
|
||||
- (void) requestPlaybackState {
|
||||
[self evaluateJavaScript:@"requestPlaybackState();"];
|
||||
}
|
||||
|
||||
#pragma mark WKScriptMessageHandler Methods
|
||||
|
||||
- (void) userContentController:(WKUserContentController*)userContentController
|
||||
didReceiveScriptMessage:(WKScriptMessage*)message {
|
||||
#pragma unused (userContentController)
|
||||
|
||||
if ([@"log" isEqual:message.name]) {
|
||||
// The message body is always a string in this case.
|
||||
[self handleLogMessage:message.body];
|
||||
} else if ([@"error" isEqual:message.name]) {
|
||||
[self handleConnectionError];
|
||||
} else {
|
||||
BGMAssert([@"gpmdp" isEqual:message.name], "Unexpected message handler name");
|
||||
[self handleGPMDPMessage:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleLogMessage:(NSString*)message {
|
||||
(void)message;
|
||||
#if DEBUG
|
||||
if (permanentAuthCode) {
|
||||
// Avoid logging the auth code, which would be a minor security issue.
|
||||
message = [message stringByReplacingOccurrencesOfString:BGMNN(permanentAuthCode)
|
||||
withString:@"<private>"];
|
||||
}
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::userContentController: %s",
|
||||
message.UTF8String);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) handleConnectionError {
|
||||
if (connectionRetries > 0) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: "
|
||||
"Retrying in 1 second");
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
// Check connectionRetries again because disconnect may have been called.
|
||||
if (connectionRetries > 0) {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::"
|
||||
"handleConnectionError: Retrying");
|
||||
[self connectWithRetries:(connectionRetries - 1)];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: "
|
||||
"No retries left. Giving up.");
|
||||
connectionErrorHandler();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleGPMDPMessage:(WKScriptMessage*)message {
|
||||
// See https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md
|
||||
|
||||
// Type check.
|
||||
if (![message.body isKindOfClass:[NSDictionary class]]) {
|
||||
NSLog(@"Unexpected message body type");
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* body = message.body;
|
||||
NSString* messageType;
|
||||
|
||||
// The key for the message type is "channel", except when the message is a response, in which
|
||||
// case the key can be "namespace".
|
||||
if ([body[@"channel"] isKindOfClass:[NSString class]]) {
|
||||
messageType = body[@"channel"];
|
||||
} else if ([body[@"namespace"] isKindOfClass:[NSString class]]) {
|
||||
messageType = body[@"namespace"];
|
||||
} else {
|
||||
NSLog(@"No channel/namespace");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the message depending on its type (or ignore it).
|
||||
if ([@"API_VERSION" isEqual:messageType]) {
|
||||
[self handleAPIVersionMessage:body];
|
||||
} else if ([@"connect" isEqual:messageType]) {
|
||||
[self handleConnectMessage:body];
|
||||
} else if ([@"playState" isEqual:messageType]) {
|
||||
[self handlePlayStateMessage:body];
|
||||
} else if ([@"result" isEqual:messageType]) {
|
||||
[self handleResultMessage:body];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handleAPIVersionMessage:(NSDictionary*)body {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// Type check.
|
||||
if (![body[@"payload"] isKindOfClass:[NSString class]]) {
|
||||
NSLog(@"Unexpected payload type");
|
||||
[self handleConnectionError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* apiVersion = body[@"payload"];
|
||||
// "1.0.0" -> ["1", "0", "0"]
|
||||
NSArray<NSString*>* versionParts = [apiVersion componentsSeparatedByString:@"."];
|
||||
|
||||
// Check the major version number is 1, which is the only major version we support.
|
||||
if (versionParts.count > 0) {
|
||||
NSInteger majorVersion = versionParts[0].integerValue;
|
||||
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"Major version: %lu", majorVersion);
|
||||
|
||||
if (majorVersion == 1) {
|
||||
// GPMDP uses SemVer, so as long as the major version number matches what we can handle,
|
||||
// it should work.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"This API version is supported");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show a warning dialog box to the user, but try to continue anyway. There's probably a
|
||||
// reasonable chance it'll still work.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: "
|
||||
"Unsupported GPMDP API version");
|
||||
apiVersionMismatchHandler(apiVersion);
|
||||
}
|
||||
|
||||
- (void) handleConnectMessage:(NSDictionary*)body {
|
||||
// Don't log the response as it may contain the auth code.
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectMessage: Received response");
|
||||
|
||||
// Type check.
|
||||
if (![body[@"payload"] isKindOfClass:[NSString class]]) {
|
||||
NSLog(@"Unexpected payload type");
|
||||
[self handleConnectionError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* payload = body[@"payload"];
|
||||
|
||||
if ([@"CODE_REQUIRED" isEqual:payload]) {
|
||||
// Ask the user for the auth code GPMDP is displaying and send it to GPMDP to finish
|
||||
// connecting.
|
||||
NSString* __nullable authCode = authRequiredHandler();
|
||||
|
||||
if (authCode) {
|
||||
[self sendAuthCode:BGMNN(authCode)];
|
||||
}
|
||||
} else {
|
||||
// The payload should be the permanent auth code.
|
||||
permanentAuthCode = payload;
|
||||
[self sendPermanentAuthCode];
|
||||
|
||||
// Save the code to the keychain so we can use it when connecting to GPMDP in future.
|
||||
userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode = permanentAuthCode;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) handlePlayStateMessage:(NSDictionary*)body {
|
||||
(void)body;
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handlePlayStateMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// This message tells us the playstate has changed, but doesn't differentiate between stopped
|
||||
// and paused. The response to this API request will. See handleResultMessage.
|
||||
// TODO: Can it transition from stopped to paused? Would that be a problem?
|
||||
[self requestPlaybackState];
|
||||
}
|
||||
|
||||
- (void) handleResultMessage:(NSDictionary*)body {
|
||||
DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleResultMessage: Response: %s",
|
||||
[NSString stringWithFormat:@"%@", body].UTF8String);
|
||||
|
||||
// Type check.
|
||||
if (![body[@"value"] isKindOfClass:[NSNumber class]]) {
|
||||
NSLog(@"No value");
|
||||
return;
|
||||
}
|
||||
|
||||
// 0 - Playback is stopped
|
||||
// 1 - Track is paused
|
||||
// 2 - Track is playing
|
||||
int state = ((NSNumber*)body[@"value"]).intValue;
|
||||
_playing = (state == 2);
|
||||
_paused = (state == 1);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMHermes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,7 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -39,12 +39,12 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) 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];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -54,6 +54,11 @@
|
||||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.hermes is nil (i.e. Hermes isn't running).
|
||||
return self.hermes.running;
|
||||
|
||||
+6
-11
@@ -14,25 +14,20 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMOutputDevicePrefs.h
|
||||
// BGMMusic.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
// Copyright © 2019 theLMGN
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <AppKit/AppKit.h>
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMOutputDevicePrefs : NSObject
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices;
|
||||
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu;
|
||||
@interface BGMMusic : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMMusic.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2019 Kyle Neideck, theLMGN
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMMusic.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Music.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMMusic {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
+ (NSUUID*) sharedMusicPlayerID {
|
||||
NSUUID* __nullable musicPlayerID =
|
||||
[[NSUUID alloc] initWithUUIDString:@"829B8069-8BD2-481D-BD40-54AB8CDAE228"];
|
||||
NSAssert(musicPlayerID, @"BGMMusic::sharedMusicPlayerID: !musicPlayerID");
|
||||
return (NSUUID*)musicPlayerID;
|
||||
}
|
||||
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusic sharedMusicPlayerID]
|
||||
name:@"Music"
|
||||
bundleID:@"com.apple.Music"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MusicApplication* __nullable) music {
|
||||
return (MusicApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.music.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case Music is closed but self.music
|
||||
// hasn't become nil yet. In that case, reading self.music.playerState could make Scripting Bridge
|
||||
// open Music.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.music.playerState == MusicEPlSPlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.music.playerState == MusicEPlSPaused);
|
||||
}
|
||||
|
||||
- (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("BGMMusic::pause: Pausing Music");
|
||||
[self.music 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("BGMMusic::unpause: Unpausing Music");
|
||||
[self.music playpause];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018, 2019 Kyle Neideck
|
||||
//
|
||||
// The base classes and protocol for objects that represent a music player app.
|
||||
//
|
||||
@@ -41,6 +41,9 @@
|
||||
// BGMDriver will log the bundle ID to system.log when it becomes aware of the music player.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMUserDefaults.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@@ -50,27 +53,26 @@
|
||||
@protocol BGMMusicPlayer <NSObject>
|
||||
|
||||
// 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.
|
||||
// BGMApp. So far that's always been a single instance, but that will probably change eventually.
|
||||
// Most classes don't need to override the default implementation from BGMMusicPlayerBase.
|
||||
//
|
||||
// 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.
|
||||
// But, 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.)
|
||||
//
|
||||
// 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;
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
|
||||
// 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.
|
||||
// you'll probably want to store them in user defaults and load them in createInstancesWithDefaults.
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
|
||||
// The name and icon of the music player, to be used in the UI.
|
||||
// The name, tool-tip and icon of the music player, to be used in the UI.
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable toolTip;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@@ -81,9 +83,12 @@
|
||||
// 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.
|
||||
// BGMMusicPlayers could pass a pointer to itself to createInstancesWithDefaults.
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
// True if this is currently the selected music player.
|
||||
@property (readonly) BOOL selected;
|
||||
|
||||
// The state of the music player.
|
||||
//
|
||||
// True if the music player app is open.
|
||||
@@ -97,10 +102,14 @@
|
||||
// BGMApp paused it.
|
||||
@property (readonly, getter=isPaused) BOOL paused;
|
||||
|
||||
// Called when the user selects this music player.
|
||||
- (void) wasSelected;
|
||||
// Called when this was the selected music player and the user just selected a different one.
|
||||
- (void) wasDeselected;
|
||||
|
||||
// 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.
|
||||
- (BOOL) pause;
|
||||
|
||||
// Unpause the music player. Does nothing if the music player is already playing or isn't running.
|
||||
// Returns YES if the music player is playing now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) unpause;
|
||||
@@ -116,6 +125,12 @@
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString*)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString* __nullable)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid;
|
||||
|
||||
@@ -124,12 +139,16 @@
|
||||
+ (NSUUID*) makeID:(NSString*)musicPlayerIDString;
|
||||
|
||||
// BGMMusicPlayer default implementations
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances;
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults;
|
||||
@property (readonly) NSImage* __nullable icon;
|
||||
@property (readonly) NSUUID* musicPlayerID;
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable toolTip;
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@property NSNumber* __nullable pid;
|
||||
@property (readonly) BOOL selected;
|
||||
- (void) wasSelected;
|
||||
- (void) wasDeselected;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
// BGMMusicPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -33,17 +33,35 @@
|
||||
|
||||
@synthesize musicPlayerID = _musicPlayerID;
|
||||
@synthesize name = _name;
|
||||
@synthesize toolTip = _toolTip;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
@synthesize selected = _selected;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
return [self initWithMusicPlayerID:musicPlayerID name:name bundleID:bundleID pid:nil];
|
||||
return [self initWithMusicPlayerID:musicPlayerID
|
||||
name:name
|
||||
toolTip:nil
|
||||
bundleID:bundleID
|
||||
pid:nil];
|
||||
}
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString*)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
return [self initWithMusicPlayerID:musicPlayerID
|
||||
name:name
|
||||
toolTip:toolTip
|
||||
bundleID:bundleID
|
||||
pid:nil];
|
||||
}
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
toolTip:(NSString* __nullable)toolTip
|
||||
bundleID:(NSString* __nullable)bundleID
|
||||
pid:(NSNumber* __nullable)pid {
|
||||
if ((self = [super init])) {
|
||||
@@ -54,8 +72,10 @@
|
||||
|
||||
_musicPlayerID = musicPlayerID;
|
||||
_name = name;
|
||||
_toolTip = toolTip;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -70,7 +90,8 @@
|
||||
|
||||
#pragma mark BGMMusicPlayer default implementations
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances {
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
|
||||
#pragma unused (userDefaults)
|
||||
return @[ [self new] ];
|
||||
}
|
||||
|
||||
@@ -82,6 +103,14 @@
|
||||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
_selected = YES;
|
||||
}
|
||||
|
||||
- (void) wasDeselected {
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayers.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 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.
|
||||
@@ -43,8 +43,8 @@
|
||||
// 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.
|
||||
// The createInstancesWithDefaults method of each class in musicPlayerClasses will be called and
|
||||
// the results will be stored in the musicPlayers property.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID
|
||||
musicPlayerClasses:(NSArray<Class<BGMMusicPlayer>>*)musicPlayerClasses
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayers.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
@@ -33,6 +33,9 @@
|
||||
#import "BGMVOX.h"
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMHermes.h"
|
||||
#import "BGMSwinsian.h"
|
||||
#import "BGMMusic.h"
|
||||
#import "BGMGooglePlayMusicDesktopPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -46,15 +49,25 @@
|
||||
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
userDefaults:(BGMUserDefaults*)defaults {
|
||||
// The classes handling each music player we support. If you write a new music player class, add
|
||||
// it to this array.
|
||||
NSArray<Class<BGMMusicPlayer>>* mpClasses = @[ [BGMVOX class],
|
||||
[BGMVLC class],
|
||||
[BGMSpotify class],
|
||||
[BGMiTunes class],
|
||||
[BGMDecibel class],
|
||||
[BGMHermes class],
|
||||
[BGMSwinsian class],
|
||||
[BGMMusic class] ];
|
||||
|
||||
// We only support Google Play Music Desktop Player on macOS 10.10 and higher.
|
||||
if (@available(macOS 10.10, *)) {
|
||||
mpClasses = [mpClasses arrayByAddingObject:[BGMGooglePlayMusicDesktopPlayer class]];
|
||||
}
|
||||
|
||||
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] ]
|
||||
musicPlayerClasses:mpClasses
|
||||
userDefaults:defaults];
|
||||
}
|
||||
|
||||
@@ -68,11 +81,14 @@
|
||||
|
||||
// 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.)
|
||||
// Each music player class has a factory method, createInstancesWithDefaults, 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]];
|
||||
NSArray<id<BGMMusicPlayer>>* instances =
|
||||
[musicPlayerClass createInstancesWithDefaults:userDefaults];
|
||||
[musicPlayers addObjectsFromArray:instances];
|
||||
}
|
||||
|
||||
_musicPlayers = [NSArray arrayWithArray:musicPlayers];
|
||||
@@ -204,6 +220,16 @@
|
||||
@"BGMMusicPlayers::setSelectedMusicPlayerImpl: Only the music players in the musicPlayers array can be selected. "
|
||||
"newSelectedMusicPlayer=%@",
|
||||
newSelectedMusicPlayer.name);
|
||||
|
||||
if (_selectedMusicPlayer == newSelectedMusicPlayer) {
|
||||
DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: %s is already the selected music "
|
||||
"player.",
|
||||
_selectedMusicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the current music player (object) a different player has been selected.
|
||||
[_selectedMusicPlayer wasDeselected];
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
@@ -215,6 +241,9 @@
|
||||
|
||||
// Save the new setting in user defaults.
|
||||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
|
||||
// Tell the music player (object) it's been selected.
|
||||
[_selectedMusicPlayer wasSelected];
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMScriptingBridge.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
// A wrapper around Scripting Bridge's SBApplication that tries to avoid ever launching the application.
|
||||
//
|
||||
@@ -29,6 +29,9 @@
|
||||
// unless the music player app is running. That way messages sent while the app is closed are ignored.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
@@ -38,12 +41,19 @@
|
||||
|
||||
@interface BGMScriptingBridge : NSObject <SBApplicationDelegate>
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID;
|
||||
// Only keeps a weak ref to musicPlayer.
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer;
|
||||
|
||||
// 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;
|
||||
|
||||
// macOS 10.14 requires the user's permission to send Apple Events. If the music player that owns
|
||||
// this object (i.e. the one passed to initWithMusicPlayer) is currently the selected music player
|
||||
// and the user hasn't already given us permission to send it Apple Events, this method asks the
|
||||
// user for permission.
|
||||
- (void) ensurePermission;
|
||||
|
||||
// SBApplicationDelegate
|
||||
|
||||
// On 10.11, SBApplicationDelegate.h declares eventDidFail with a non-null return type, but the docs
|
||||
|
||||
@@ -17,29 +17,32 @@
|
||||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "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;
|
||||
id<BGMMusicPlayer> __weak _musicPlayer;
|
||||
BGMAppWatcher* appWatcher;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)inBundleID {
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer {
|
||||
if ((self = [super init])) {
|
||||
bundleID = inBundleID;
|
||||
_musicPlayer = musicPlayer;
|
||||
|
||||
[self initApplication];
|
||||
}
|
||||
@@ -48,13 +51,18 @@
|
||||
}
|
||||
|
||||
- (void) initApplication {
|
||||
NSString* bundleID = _musicPlayer.bundleID;
|
||||
BGMAssert(bundleID, "Music players need a bundle ID to use ScriptingBridge");
|
||||
|
||||
BGMScriptingBridge* __weak weakSelf = self;
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
_application.delegate = self;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
return [[note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier] isEqualToString:bundleID];
|
||||
BGMScriptingBridge* strongSelf = weakSelf;
|
||||
strongSelf->_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
// TODO: The SBApplication will still keep a strong ref to this object, so we would have to
|
||||
// make a separate delegate object to avoid the retain cycle. Not currently a problem
|
||||
// because we only ever create instances that live forever.
|
||||
strongSelf->_application.delegate = strongSelf;
|
||||
};
|
||||
|
||||
// Add observers that create/destroy the SBApplication when the music player is launched/terminated. We
|
||||
@@ -65,27 +73,20 @@
|
||||
// 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;
|
||||
}
|
||||
}];
|
||||
appWatcher =
|
||||
[[BGMAppWatcher alloc] initWithBundleID:bundleID
|
||||
appLaunched:^{
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
[weakSelf ensurePermission];
|
||||
}
|
||||
appTerminated:^{
|
||||
BGMScriptingBridge* strongSelf = weakSelf;
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
strongSelf->_application = nil;
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) {
|
||||
@@ -93,17 +94,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
- (void) ensurePermission {
|
||||
// Skip this check if running on a version of macOS before 10.14. In that case, we don't require
|
||||
// user permission to send Apple Events. Also skip it if compiling on an earlier version.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
|
||||
if (@available(macOS 10.14, *)) {
|
||||
id<BGMMusicPlayer> musicPlayer = _musicPlayer;
|
||||
|
||||
if (!musicPlayer.selected) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not selected. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!musicPlayer.running) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not running. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// AEDeterminePermissionToAutomateTarget will block if it has to show a dialog to the user
|
||||
// to ask for permission, so dispatch this to make sure it doesn't run on the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
NSAppleEventDescriptor* musicPlayerEventDescriptor =
|
||||
[NSAppleEventDescriptor
|
||||
descriptorWithBundleIdentifier:(NSString*)musicPlayer.bundleID];
|
||||
|
||||
OSStatus status =
|
||||
AEDeterminePermissionToAutomateTarget(musicPlayerEventDescriptor.aeDesc,
|
||||
typeWildCard,
|
||||
typeWildCard,
|
||||
true);
|
||||
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: "
|
||||
"Apple Events permission status for %s: %d",
|
||||
musicPlayer.name.UTF8String,
|
||||
status);
|
||||
|
||||
if (status != noErr) {
|
||||
// TODO: If they deny permission, we should grey-out the auto-pause menu item and
|
||||
// add something to the UI that indicates the problem. Maybe a warning icon
|
||||
// that shows an explanation when you hover your mouse over it. (We can't just
|
||||
// ask them again later because the API doesn't support it. They can only fix
|
||||
// it in System Preferences.)
|
||||
NSLog(@"BGMScriptingBridge::ensurePermission: Permission denied for %@. status=%d",
|
||||
musicPlayer.name,
|
||||
status);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: Not macOS 10.14+. Nothing to do.");
|
||||
}
|
||||
#endif /* MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 */
|
||||
}
|
||||
|
||||
#pragma mark SBApplicationDelegate
|
||||
@@ -118,7 +160,7 @@
|
||||
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,
|
||||
_musicPlayer.bundleID.UTF8String,
|
||||
vars.UTF8String);
|
||||
#else
|
||||
#pragma unused (event, error)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMSpotify.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
// Spotify's AppleScript API looks to have been designed to match iTunes', so this file is basically
|
||||
// just s/iTunes/Spotify/ on BGMiTunes.m
|
||||
@@ -33,7 +33,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -42,12 +42,12 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) 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];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -57,6 +57,11 @@
|
||||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.spotify is nil (i.e. Spotify isn't running).
|
||||
return self.spotify.running;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMSwinsian.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMSwinsian : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMSwinsian.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMSwinsian.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Swinsian.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMSwinsian {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (instancetype) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen (the
|
||||
// command line tool).
|
||||
NSUUID* musicPlayerID = [BGMMusicPlayerBase makeID:@"B74D18F6-DFF7-4D88-B719-429CFF98CFFA"];
|
||||
|
||||
if ((self = [super initWithMusicPlayerID:musicPlayerID
|
||||
name:@"Swinsian"
|
||||
bundleID:@"com.swinsian.Swinsian"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (SwinsianApplication* __nullable) swinsian {
|
||||
return (SwinsianApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.swinsian is nil (i.e. Swinsian isn't running).
|
||||
return self.swinsian.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case Swinsian is closed but self.swinsian
|
||||
// hasn't become nil yet. In that case, reading self.swinsian.playerState could make Scripting
|
||||
// Bridge open Swinsian.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.swinsian.playerState == SwinsianPlayerStatePlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.swinsian.playerState == SwinsianPlayerStatePaused);
|
||||
}
|
||||
|
||||
- (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("BGMSwinsian::pause: Pausing Swinsian");
|
||||
[self.swinsian 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("BGMSwinsian::unpause: Unpausing Swinsian");
|
||||
[self.swinsian play];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMVLC.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Portions copyright (C) 2012 Peter Ljunglöf. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -40,11 +40,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) 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];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -54,6 +54,11 @@
|
||||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vlc.running;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMVOX.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,7 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -39,11 +39,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) 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];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -53,6 +53,11 @@
|
||||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vox.running;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMiTunes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,7 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -45,11 +45,11 @@
|
||||
return (NSUUID*)musicPlayerID;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
name:@"iTunes"
|
||||
bundleID:@"com.apple.iTunes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString* __nonnull)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -59,6 +59,11 @@
|
||||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.iTunes.running;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// GooglePlayMusicDesktopPlayer.js
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// The specification for GPMDP's API:
|
||||
// https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md
|
||||
|
||||
try {
|
||||
|
||||
window._log = msg => {
|
||||
window.webkit.messageHandlers.log.postMessage(msg);
|
||||
};
|
||||
|
||||
// Global JS error handler.
|
||||
window.onerror = (msg, url, line, col, error) => {
|
||||
let extra = !col ? '' : '\nColumn: ' + col;
|
||||
extra += !error ? '' : '\nError: ' + error;
|
||||
|
||||
// TODO: I'm not sure this log message is ever actually useful.
|
||||
window._log('Error: ' + msg + '\nURL: ' + url + '\nLine: ' + line + extra);
|
||||
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
};
|
||||
|
||||
// Send a JSON message to GPMDP.
|
||||
//
|
||||
// If we're connecting, this function will return immediately and the message will be sent after we
|
||||
// finish connecting. Logs an error and returns if window.connect() hasn't been called yet.
|
||||
window._sendJSON = json => {
|
||||
if (window._wsPromise) {
|
||||
window._wsPromise.then(() => {
|
||||
window._sendJSONImmediate(json);
|
||||
}).catch(error => {
|
||||
// TODO: Is there anything else we can do? Retries?
|
||||
window._log('Error sending JSON: ' + JSON.stringify(error));
|
||||
});
|
||||
} else {
|
||||
window._log('Error: No WebSocket promise. Discarding JSON message: ' +
|
||||
JSON.stringify(json));
|
||||
}
|
||||
};
|
||||
|
||||
// Send a JSON message to GPMDP, but don't wait if we're in the process of connecting.
|
||||
//
|
||||
// Logs an error and returns if window.connect() hasn't been called yet. The authCode param is
|
||||
// optional and only used to hide the code in log messages.
|
||||
window._sendJSONImmediate = (json, authCode) => {
|
||||
let jsonStr = JSON.stringify(json);
|
||||
let jsonStrSanitized = authCode ? jsonStr.replace(authCode, "<private>") : jsonStr;
|
||||
|
||||
if (window._ws) {
|
||||
window._log('Sending JSON: ' + jsonStrSanitized);
|
||||
window._ws.send(jsonStr);
|
||||
} else {
|
||||
window._log('Error: No WebSocket. Discarding JSON message: ' + jsonStrSanitized);
|
||||
}
|
||||
};
|
||||
|
||||
// permanentAuthCode is optional. If this is the first time they've selected GPMDP, we won't have a
|
||||
// permanent code yet.
|
||||
window.connect = permanentAuthCode => {
|
||||
// Reset the connection state.
|
||||
window._requestID = 1;
|
||||
|
||||
// Close the existing connection if we're already connected.
|
||||
window.disconnect();
|
||||
|
||||
// Create the new connection.
|
||||
window._ws = new WebSocket('ws://localhost:5672');
|
||||
|
||||
window._ws.onmessage = event => {
|
||||
// Pass the message along to BGMGooglePlayMusicDesktopPlayerConnection.
|
||||
let reply = JSON.parse(event.data);
|
||||
window.webkit.messageHandlers.gpmdp.postMessage(reply);
|
||||
};
|
||||
|
||||
window._wsPromise = new Promise((resolve, reject) => {
|
||||
window._ws.onopen = () => {
|
||||
// Send GPMDP the initial connection message.
|
||||
if (permanentAuthCode) {
|
||||
window._log('Connecting with auth code');
|
||||
window.sendPermanentAuthCode(permanentAuthCode);
|
||||
} else {
|
||||
// Since we don't have an auth code, it will display a four-digit code and reply
|
||||
// telling us to ask the user to type it into Background Music.
|
||||
window._log('Connecting without auth code');
|
||||
|
||||
window._sendJSONImmediate({
|
||||
'namespace': 'connect',
|
||||
'method': 'connect',
|
||||
'arguments': ['Background Music']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window._ws.onerror = error => {
|
||||
// Report the error to BGMGooglePlayMusicDesktopPlayerConnection.
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
// Reject the connection promise.
|
||||
reject(error);
|
||||
};
|
||||
|
||||
// Store the function that resolves this promise. We resolve it after we finish
|
||||
// authenticating.
|
||||
window._resolveConnectionPromise = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// Close the connection to GPMDP. Does nothing if we aren't connected.
|
||||
window.disconnect = () => {
|
||||
if (window._ws) {
|
||||
window._log('Closing WebSocket');
|
||||
window._ws.close();
|
||||
window._ws = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Send an authentication code to GPMDP. To send a four-digit code (i.e. one entered by the user),
|
||||
// call this directly. To send a permanent code received from GPMDP, use
|
||||
// window.sendPermanentAuthCode().
|
||||
//
|
||||
// authCode should be percent-encoded.
|
||||
window.sendAuthCode = authCode => {
|
||||
// Percent-decode the auth code string. We pass it percent-encoded just to make sure nothing in
|
||||
// it accidentally gets executed as Javascript.
|
||||
authCode = window.decodeURIComponent(authCode);
|
||||
|
||||
window._sendJSONImmediate({
|
||||
'namespace': 'connect',
|
||||
'method': 'connect',
|
||||
'arguments': ['Background Music', authCode]
|
||||
}, authCode);
|
||||
};
|
||||
|
||||
// Send a permanent authentication code, received from GPMDP previously, to GPMDP.
|
||||
window.sendPermanentAuthCode = permanentAuthCode => {
|
||||
window._log('Sending permanent auth code');
|
||||
window.sendAuthCode(permanentAuthCode);
|
||||
// TODO: If the code is rejected, GPMDP will send us a connect message and we'll show the auth
|
||||
// code dialog, but accepting the promise here means some messages we send might get
|
||||
// ignored.
|
||||
window._resolveConnectionPromise();
|
||||
};
|
||||
|
||||
// Ask GPMDP to send us its current playback state (playing, paused or stopped).
|
||||
window.requestPlaybackState = () => {
|
||||
window._sendJSON({
|
||||
'namespace': 'playback',
|
||||
'method': 'getPlaybackState',
|
||||
// We don't send any other types of request, so the ID we send only needs to be unique.
|
||||
'requestID': window._requestID++
|
||||
});
|
||||
};
|
||||
|
||||
// Tell GPMDP to toggle between playing and paused.
|
||||
window.playPause = () => {
|
||||
window._sendJSON({
|
||||
'namespace': 'playback',
|
||||
'method': 'playPause'
|
||||
});
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
window.webkit.messageHandlers.log.postMessage('Error: ' + JSON.stringify(error));
|
||||
window.webkit.messageHandlers.log.postMessage(JSON.stringify(error.stack));
|
||||
window.webkit.messageHandlers.error.postMessage(error);
|
||||
}
|
||||
|
||||
// Return an empty string as returning some types can cause an error when this Javascript is loaded
|
||||
// into the WKWebView.
|
||||
""
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Music.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /System/Applications/Music.app | sdp -fh --basename Music
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class MusicApplication, MusicItem, MusicAirPlayDevice, MusicArtwork, MusicEncoder, MusicEQPreset, MusicPlaylist, MusicAudioCDPlaylist, MusicLibraryPlaylist, MusicRadioTunerPlaylist, MusicSource, MusicSubscriptionPlaylist, MusicTrack, MusicAudioCDTrack, MusicFileTrack, MusicSharedTrack, MusicURLTrack, MusicUserPlaylist, MusicFolderPlaylist, MusicVisual, MusicWindow, MusicBrowserWindow, MusicEQWindow, MusicMiniplayerWindow, MusicPlaylistWindow, MusicVideoWindow;
|
||||
|
||||
enum MusicEKnd {
|
||||
MusicEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */,
|
||||
MusicEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */,
|
||||
MusicEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */
|
||||
};
|
||||
typedef enum MusicEKnd MusicEKnd;
|
||||
|
||||
enum MusicEnum {
|
||||
MusicEnumStandard = 'lwst' /* Standard PostScript error handling */,
|
||||
MusicEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */
|
||||
};
|
||||
typedef enum MusicEnum MusicEnum;
|
||||
|
||||
enum MusicEPlS {
|
||||
MusicEPlSStopped = 'kPSS',
|
||||
MusicEPlSPlaying = 'kPSP',
|
||||
MusicEPlSPaused = 'kPSp',
|
||||
MusicEPlSFastForwarding = 'kPSF',
|
||||
MusicEPlSRewinding = 'kPSR'
|
||||
};
|
||||
typedef enum MusicEPlS MusicEPlS;
|
||||
|
||||
enum MusicERpt {
|
||||
MusicERptOff = 'kRpO',
|
||||
MusicERptOne = 'kRp1',
|
||||
MusicERptAll = 'kAll'
|
||||
};
|
||||
typedef enum MusicERpt MusicERpt;
|
||||
|
||||
enum MusicEShM {
|
||||
MusicEShMSongs = 'kShS',
|
||||
MusicEShMAlbums = 'kShA',
|
||||
MusicEShMGroupings = 'kShG'
|
||||
};
|
||||
typedef enum MusicEShM MusicEShM;
|
||||
|
||||
enum MusicESrc {
|
||||
MusicESrcLibrary = 'kLib',
|
||||
MusicESrcIPod = 'kPod',
|
||||
MusicESrcAudioCD = 'kACD',
|
||||
MusicESrcMP3CD = 'kMCD',
|
||||
MusicESrcRadioTuner = 'kTun',
|
||||
MusicESrcSharedLibrary = 'kShd',
|
||||
MusicESrcITunesStore = 'kITS',
|
||||
MusicESrcUnknown = 'kUnk'
|
||||
};
|
||||
typedef enum MusicESrc MusicESrc;
|
||||
|
||||
enum MusicESrA {
|
||||
MusicESrAAlbums = 'kSrL' /* albums only */,
|
||||
MusicESrAAll = 'kAll' /* all text fields */,
|
||||
MusicESrAArtists = 'kSrR' /* artists only */,
|
||||
MusicESrAComposers = 'kSrC' /* composers only */,
|
||||
MusicESrADisplayed = 'kSrV' /* visible text fields */,
|
||||
MusicESrASongs = 'kSrS' /* song names only */
|
||||
};
|
||||
typedef enum MusicESrA MusicESrA;
|
||||
|
||||
enum MusicESpK {
|
||||
MusicESpKNone = 'kNon',
|
||||
MusicESpKFolder = 'kSpF',
|
||||
MusicESpKGenius = 'kSpG',
|
||||
MusicESpKLibrary = 'kSpL',
|
||||
MusicESpKMusic = 'kSpZ',
|
||||
MusicESpKPurchasedMusic = 'kSpM'
|
||||
};
|
||||
typedef enum MusicESpK MusicESpK;
|
||||
|
||||
enum MusicEMdK {
|
||||
MusicEMdKSong = 'kMdS' /* music track */,
|
||||
MusicEMdKMusicVideo = 'kVdV' /* music video track */,
|
||||
MusicEMdKUnknown = 'kUnk'
|
||||
};
|
||||
typedef enum MusicEMdK MusicEMdK;
|
||||
|
||||
enum MusicERtK {
|
||||
MusicERtKUser = 'kRtU' /* user-specified rating */,
|
||||
MusicERtKComputed = 'kRtC' /* iTunes-computed rating */
|
||||
};
|
||||
typedef enum MusicERtK MusicERtK;
|
||||
|
||||
enum MusicEAPD {
|
||||
MusicEAPDComputer = 'kAPC',
|
||||
MusicEAPDAirPortExpress = 'kAPX',
|
||||
MusicEAPDAppleTV = 'kAPT',
|
||||
MusicEAPDAirPlayDevice = 'kAPO',
|
||||
MusicEAPDBluetoothDevice = 'kAPB',
|
||||
MusicEAPDHomePod = 'kAPH',
|
||||
MusicEAPDUnknown = 'kAPU'
|
||||
};
|
||||
typedef enum MusicEAPD MusicEAPD;
|
||||
|
||||
enum MusicEClS {
|
||||
MusicEClSUnknown = 'kUnk',
|
||||
MusicEClSPurchased = 'kPur',
|
||||
MusicEClSMatched = 'kMat',
|
||||
MusicEClSUploaded = 'kUpl',
|
||||
MusicEClSIneligible = 'kRej',
|
||||
MusicEClSRemoved = 'kRem',
|
||||
MusicEClSError = 'kErr',
|
||||
MusicEClSDuplicate = 'kDup',
|
||||
MusicEClSSubscription = 'kSub',
|
||||
MusicEClSNoLongerAvailable = 'kRev',
|
||||
MusicEClSNotUploaded = 'kUpP'
|
||||
};
|
||||
typedef enum MusicEClS MusicEClS;
|
||||
|
||||
@protocol MusicGenericMethods
|
||||
|
||||
- (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s)
|
||||
- (void) close; // Close an object
|
||||
- (void) delete; // Delete an element from an object
|
||||
- (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s)
|
||||
- (BOOL) exists; // Verify if an object exists
|
||||
- (void) open; // Open the specified object(s)
|
||||
- (void) save; // Save the specified object(s)
|
||||
- (void) playOnce:(BOOL)once; // play the current track or the specified track or file.
|
||||
- (void) select; // select the specified object(s)
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* iTunes Suite
|
||||
*/
|
||||
|
||||
// The application program
|
||||
@interface MusicApplication : SBApplication
|
||||
|
||||
- (SBElementArray<MusicAirPlayDevice *> *) AirPlayDevices;
|
||||
- (SBElementArray<MusicBrowserWindow *> *) browserWindows;
|
||||
- (SBElementArray<MusicEncoder *> *) encoders;
|
||||
- (SBElementArray<MusicEQPreset *> *) EQPresets;
|
||||
- (SBElementArray<MusicEQWindow *> *) EQWindows;
|
||||
- (SBElementArray<MusicMiniplayerWindow *> *) miniplayerWindows;
|
||||
- (SBElementArray<MusicPlaylist *> *) playlists;
|
||||
- (SBElementArray<MusicPlaylistWindow *> *) playlistWindows;
|
||||
- (SBElementArray<MusicSource *> *) sources;
|
||||
- (SBElementArray<MusicTrack *> *) tracks;
|
||||
- (SBElementArray<MusicVideoWindow *> *) videoWindows;
|
||||
- (SBElementArray<MusicVisual *> *) visuals;
|
||||
- (SBElementArray<MusicWindow *> *) windows;
|
||||
|
||||
@property (readonly) BOOL AirPlayEnabled; // is AirPlay currently enabled?
|
||||
@property (readonly) BOOL converting; // is a track currently being converted?
|
||||
@property (copy) NSArray<MusicAirPlayDevice *> *currentAirPlayDevices; // the currently selected AirPlay device(s)
|
||||
@property (copy) MusicEncoder *currentEncoder; // the currently selected encoder (MP3, AIFF, WAV, etc.)
|
||||
@property (copy) MusicEQPreset *currentEQPreset; // the currently selected equalizer preset
|
||||
@property (copy, readonly) MusicPlaylist *currentPlaylist; // the playlist containing the currently targeted track
|
||||
@property (copy, readonly) NSString *currentStreamTitle; // the name of the current song in the playing stream (provided by streaming server)
|
||||
@property (copy, readonly) NSString *currentStreamURL; // the URL of the playing stream or streaming web site (provided by streaming server)
|
||||
@property (copy, readonly) MusicTrack *currentTrack; // the current targeted track
|
||||
@property (copy) MusicVisual *currentVisual; // the currently selected visual plug-in
|
||||
@property BOOL EQEnabled; // is the equalizer enabled?
|
||||
@property BOOL fixedIndexing; // true if all AppleScript track indices should be independent of the play order of the owning playlist.
|
||||
@property BOOL frontmost; // is iTunes the frontmost application?
|
||||
@property BOOL fullScreen; // are visuals displayed using the entire screen?
|
||||
@property (copy, readonly) NSString *name; // the name of the application
|
||||
@property BOOL mute; // has the sound output been muted?
|
||||
@property double playerPosition; // the player’s position within the currently playing track in seconds.
|
||||
@property (readonly) MusicEPlS playerState; // is iTunes stopped, paused, or playing?
|
||||
@property (copy, readonly) SBObject *selection; // the selection visible to the user
|
||||
@property BOOL shuffleEnabled; // are songs played in random order?
|
||||
@property MusicEShM shuffleMode; // the playback shuffle mode
|
||||
@property MusicERpt songRepeat; // the playback repeat mode
|
||||
@property NSInteger soundVolume; // the sound output volume (0 = minimum, 100 = maximum)
|
||||
@property (copy, readonly) NSString *version; // the version of iTunes
|
||||
@property BOOL visualsEnabled; // are visuals currently being displayed?
|
||||
|
||||
- (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s)
|
||||
- (void) run; // Run iTunes
|
||||
- (void) quit; // Quit iTunes
|
||||
- (MusicTrack *) add:(NSArray<NSURL *> *)x to:(SBObject *)to; // add one or more files to a playlist
|
||||
- (void) backTrack; // reposition to beginning of current track or go to previous track if already at start of current track
|
||||
- (MusicTrack *) convert:(NSArray<SBObject *> *)x; // convert one or more files or tracks
|
||||
- (void) fastForward; // skip forward in a playing track
|
||||
- (void) nextTrack; // advance to the next track in the current playlist
|
||||
- (void) pause; // pause playback
|
||||
- (void) playOnce:(BOOL)once; // play the current track or the specified track or file.
|
||||
- (void) playpause; // toggle the playing/paused state of the current track
|
||||
- (void) previousTrack; // return to the previous track in the current playlist
|
||||
- (void) resume; // disable fast forward/rewind and resume playback, if playing.
|
||||
- (void) rewind; // skip backwards in a playing track
|
||||
- (void) stop; // stop playback
|
||||
- (void) openLocation:(NSString *)x; // Opens a Music Store or audio stream URL
|
||||
|
||||
@end
|
||||
|
||||
// an item
|
||||
@interface MusicItem : SBObject <MusicGenericMethods>
|
||||
|
||||
@property (copy, readonly) SBObject *container; // the container of the item
|
||||
- (NSInteger) id; // the id of the item
|
||||
@property (readonly) NSInteger index; // The index of the item in internal application order.
|
||||
@property (copy) NSString *name; // the name of the item
|
||||
@property (copy, readonly) NSString *persistentID; // the id of the item as a hexadecimal string. This id does not change over time.
|
||||
@property (copy) NSDictionary *properties; // every property of the item
|
||||
|
||||
- (void) download; // download a cloud track or playlist
|
||||
- (void) reveal; // reveal and select a track or playlist
|
||||
|
||||
@end
|
||||
|
||||
// an AirPlay device
|
||||
@interface MusicAirPlayDevice : MusicItem
|
||||
|
||||
@property (readonly) BOOL active; // is the device currently being played to?
|
||||
@property (readonly) BOOL available; // is the device currently available?
|
||||
@property (readonly) MusicEAPD kind; // the kind of the device
|
||||
@property (copy, readonly) NSString *networkAddress; // the network (MAC) address of the device
|
||||
- (BOOL) protected; // is the device password- or passcode-protected?
|
||||
@property BOOL selected; // is the device currently selected?
|
||||
@property (readonly) BOOL supportsAudio; // does the device support audio playback?
|
||||
@property (readonly) BOOL supportsVideo; // does the device support video playback?
|
||||
@property NSInteger soundVolume; // the output volume for the device (0 = minimum, 100 = maximum)
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a piece of art within a track or playlist
|
||||
@interface MusicArtwork : MusicItem
|
||||
|
||||
@property (copy) NSImage *data; // data for this artwork, in the form of a picture
|
||||
@property (copy) NSString *objectDescription; // description of artwork as a string
|
||||
@property (readonly) BOOL downloaded; // was this artwork downloaded by iTunes?
|
||||
@property (copy, readonly) NSNumber *format; // the data format for this piece of artwork
|
||||
@property NSInteger kind; // kind or purpose of this piece of artwork
|
||||
@property (copy) NSData *rawData; // data for this artwork, in original format
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// converts a track to a specific file format
|
||||
@interface MusicEncoder : MusicItem
|
||||
|
||||
@property (copy, readonly) NSString *format; // the data format created by the encoder
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// equalizer preset configuration
|
||||
@interface MusicEQPreset : MusicItem
|
||||
|
||||
@property double band1; // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band2; // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band3; // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band4; // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band5; // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band6; // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band7; // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band8; // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band9; // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB)
|
||||
@property double band10; // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB)
|
||||
@property (readonly) BOOL modifiable; // can this preset be modified?
|
||||
@property double preamp; // the equalizer preamp level (-12.0 dB to +12.0 dB)
|
||||
@property BOOL updateTracks; // should tracks which refer to this preset be updated when the preset is renamed or deleted?
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a list of songs/streams
|
||||
@interface MusicPlaylist : MusicItem
|
||||
|
||||
- (SBElementArray<MusicTrack *> *) tracks;
|
||||
- (SBElementArray<MusicArtwork *> *) artworks;
|
||||
|
||||
@property (copy) NSString *objectDescription; // the description of the playlist
|
||||
@property BOOL disliked; // is this playlist disliked?
|
||||
@property (readonly) NSInteger duration; // the total length of all songs (in seconds)
|
||||
@property (copy) NSString *name; // the name of the playlist
|
||||
@property BOOL loved; // is this playlist loved?
|
||||
@property (copy, readonly) MusicPlaylist *parent; // folder which contains this playlist (if any)
|
||||
@property (readonly) NSInteger size; // the total size of all songs (in bytes)
|
||||
@property (readonly) MusicESpK specialKind; // special playlist kind
|
||||
@property (copy, readonly) NSString *time; // the length of all songs in MM:SS format
|
||||
@property (readonly) BOOL visible; // is this playlist visible in the Source list?
|
||||
|
||||
- (void) moveTo:(SBObject *)to; // Move playlist(s) to a new location
|
||||
- (MusicTrack *) searchFor:(NSString *)for_ only:(MusicESrA)only; // search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes.
|
||||
|
||||
@end
|
||||
|
||||
// a playlist representing an audio CD
|
||||
@interface MusicAudioCDPlaylist : MusicPlaylist
|
||||
|
||||
- (SBElementArray<MusicAudioCDTrack *> *) audioCDTracks;
|
||||
|
||||
@property (copy) NSString *artist; // the artist of the CD
|
||||
@property BOOL compilation; // is this CD a compilation album?
|
||||
@property (copy) NSString *composer; // the composer of the CD
|
||||
@property NSInteger discCount; // the total number of discs in this CD’s album
|
||||
@property NSInteger discNumber; // the index of this CD disc in the source album
|
||||
@property (copy) NSString *genre; // the genre of the CD
|
||||
@property NSInteger year; // the year the album was recorded/released
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the master music library playlist
|
||||
@interface MusicLibraryPlaylist : MusicPlaylist
|
||||
|
||||
- (SBElementArray<MusicFileTrack *> *) fileTracks;
|
||||
- (SBElementArray<MusicURLTrack *> *) URLTracks;
|
||||
- (SBElementArray<MusicSharedTrack *> *) sharedTracks;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the radio tuner playlist
|
||||
@interface MusicRadioTunerPlaylist : MusicPlaylist
|
||||
|
||||
- (SBElementArray<MusicURLTrack *> *) URLTracks;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a music source (music library, CD, device, etc.)
|
||||
@interface MusicSource : MusicItem
|
||||
|
||||
- (SBElementArray<MusicAudioCDPlaylist *> *) audioCDPlaylists;
|
||||
- (SBElementArray<MusicLibraryPlaylist *> *) libraryPlaylists;
|
||||
- (SBElementArray<MusicPlaylist *> *) playlists;
|
||||
- (SBElementArray<MusicRadioTunerPlaylist *> *) radioTunerPlaylists;
|
||||
- (SBElementArray<MusicSubscriptionPlaylist *> *) subscriptionPlaylists;
|
||||
- (SBElementArray<MusicUserPlaylist *> *) userPlaylists;
|
||||
|
||||
@property (readonly) long long capacity; // the total size of the source if it has a fixed size
|
||||
@property (readonly) long long freeSpace; // the free space on the source if it has a fixed size
|
||||
@property (readonly) MusicESrc kind;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a subscription playlist from Apple Music
|
||||
@interface MusicSubscriptionPlaylist : MusicPlaylist
|
||||
|
||||
- (SBElementArray<MusicFileTrack *> *) fileTracks;
|
||||
- (SBElementArray<MusicURLTrack *> *) URLTracks;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// playable audio source
|
||||
@interface MusicTrack : MusicItem
|
||||
|
||||
- (SBElementArray<MusicArtwork *> *) artworks;
|
||||
|
||||
@property (copy) NSString *album; // the album name of the track
|
||||
@property (copy) NSString *albumArtist; // the album artist of the track
|
||||
@property BOOL albumDisliked; // is the album for this track disliked?
|
||||
@property BOOL albumLoved; // is the album for this track loved?
|
||||
@property NSInteger albumRating; // the rating of the album for this track (0 to 100)
|
||||
@property (readonly) MusicERtK albumRatingKind; // the rating kind of the album rating for this track
|
||||
@property (copy) NSString *artist; // the artist/source of the track
|
||||
@property (readonly) NSInteger bitRate; // the bit rate of the track (in kbps)
|
||||
@property double bookmark; // the bookmark time of the track in seconds
|
||||
@property BOOL bookmarkable; // is the playback position for this track remembered?
|
||||
@property NSInteger bpm; // the tempo of this track in beats per minute
|
||||
@property (copy) NSString *category; // the category of the track
|
||||
@property (readonly) MusicEClS cloudStatus; // the iCloud status of the track
|
||||
@property (copy) NSString *comment; // freeform notes about the track
|
||||
@property BOOL compilation; // is this track from a compilation album?
|
||||
@property (copy) NSString *composer; // the composer of the track
|
||||
@property (readonly) NSInteger databaseID; // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data.
|
||||
@property (copy, readonly) NSDate *dateAdded; // the date the track was added to the playlist
|
||||
@property (copy) NSString *objectDescription; // the description of the track
|
||||
@property NSInteger discCount; // the total number of discs in the source album
|
||||
@property NSInteger discNumber; // the index of the disc containing this track on the source album
|
||||
@property BOOL disliked; // is this track disliked?
|
||||
@property (copy, readonly) NSString *downloaderAppleID; // the Apple ID of the person who downloaded this track
|
||||
@property (copy, readonly) NSString *downloaderName; // the name of the person who downloaded this track
|
||||
@property (readonly) double duration; // the length of the track in seconds
|
||||
@property BOOL enabled; // is this track checked for playback?
|
||||
@property (copy) NSString *episodeID; // the episode ID of the track
|
||||
@property NSInteger episodeNumber; // the episode number of the track
|
||||
@property (copy) NSString *EQ; // the name of the EQ preset of the track
|
||||
@property double finish; // the stop time of the track in seconds
|
||||
@property BOOL gapless; // is this track from a gapless album?
|
||||
@property (copy) NSString *genre; // the music/audio genre (category) of the track
|
||||
@property (copy) NSString *grouping; // the grouping (piece) of the track. Generally used to denote movements within a classical work.
|
||||
@property (copy, readonly) NSString *kind; // a text description of the track
|
||||
@property (copy) NSString *longDescription;
|
||||
@property BOOL loved; // is this track loved?
|
||||
@property (copy) NSString *lyrics; // the lyrics of the track
|
||||
@property MusicEMdK mediaKind; // the media kind of the track
|
||||
@property (copy, readonly) NSDate *modificationDate; // the modification date of the content of this track
|
||||
@property (copy) NSString *movement; // the movement name of the track
|
||||
@property NSInteger movementCount; // the total number of movements in the work
|
||||
@property NSInteger movementNumber; // the index of the movement in the work
|
||||
@property NSInteger playedCount; // number of times this track has been played
|
||||
@property (copy) NSDate *playedDate; // the date and time this track was last played
|
||||
@property (copy, readonly) NSString *purchaserAppleID; // the Apple ID of the person who purchased this track
|
||||
@property (copy, readonly) NSString *purchaserName; // the name of the person who purchased this track
|
||||
@property NSInteger rating; // the rating of this track (0 to 100)
|
||||
@property (readonly) MusicERtK ratingKind; // the rating kind of this track
|
||||
@property (copy, readonly) NSDate *releaseDate; // the release date of this track
|
||||
@property (readonly) NSInteger sampleRate; // the sample rate of the track (in Hz)
|
||||
@property NSInteger seasonNumber; // the season number of the track
|
||||
@property BOOL shufflable; // is this track included when shuffling?
|
||||
@property NSInteger skippedCount; // number of times this track has been skipped
|
||||
@property (copy) NSDate *skippedDate; // the date and time this track was last skipped
|
||||
@property (copy) NSString *show; // the show name of the track
|
||||
@property (copy) NSString *sortAlbum; // override string to use for the track when sorting by album
|
||||
@property (copy) NSString *sortArtist; // override string to use for the track when sorting by artist
|
||||
@property (copy) NSString *sortAlbumArtist; // override string to use for the track when sorting by album artist
|
||||
@property (copy) NSString *sortName; // override string to use for the track when sorting by name
|
||||
@property (copy) NSString *sortComposer; // override string to use for the track when sorting by composer
|
||||
@property (copy) NSString *sortShow; // override string to use for the track when sorting by show name
|
||||
@property (readonly) long long size; // the size of the track (in bytes)
|
||||
@property double start; // the start time of the track in seconds
|
||||
@property (copy, readonly) NSString *time; // the length of the track in MM:SS format
|
||||
@property NSInteger trackCount; // the total number of tracks on the source album
|
||||
@property NSInteger trackNumber; // the index of the track on the source album
|
||||
@property BOOL unplayed; // is this track unplayed?
|
||||
@property NSInteger volumeAdjustment; // relative volume adjustment of the track (-100% to 100%)
|
||||
@property (copy) NSString *work; // the work name of the track
|
||||
@property NSInteger year; // the year the track was recorded/released
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a track on an audio CD
|
||||
@interface MusicAudioCDTrack : MusicTrack
|
||||
|
||||
@property (copy, readonly) NSURL *location; // the location of the file represented by this track
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a track representing an audio file (MP3, AIFF, etc.)
|
||||
@interface MusicFileTrack : MusicTrack
|
||||
|
||||
@property (copy) NSURL *location; // the location of the file represented by this track
|
||||
|
||||
- (void) refresh; // update file track information from the current information in the track’s file
|
||||
|
||||
@end
|
||||
|
||||
// a track residing in a shared library
|
||||
@interface MusicSharedTrack : MusicTrack
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a track representing a network stream
|
||||
@interface MusicURLTrack : MusicTrack
|
||||
|
||||
@property (copy) NSString *address; // the URL for this track
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// custom playlists created by the user
|
||||
@interface MusicUserPlaylist : MusicPlaylist
|
||||
|
||||
- (SBElementArray<MusicFileTrack *> *) fileTracks;
|
||||
- (SBElementArray<MusicURLTrack *> *) URLTracks;
|
||||
- (SBElementArray<MusicSharedTrack *> *) sharedTracks;
|
||||
|
||||
@property BOOL shared; // is this playlist shared?
|
||||
@property (readonly) BOOL smart; // is this a Smart Playlist?
|
||||
@property (readonly) BOOL genius; // is this a Genius Playlist?
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a folder that contains other playlists
|
||||
@interface MusicFolderPlaylist : MusicUserPlaylist
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a visual plug-in
|
||||
@interface MusicVisual : MusicItem
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// any window
|
||||
@interface MusicWindow : MusicItem
|
||||
|
||||
@property NSRect bounds; // the boundary rectangle for the window
|
||||
@property (readonly) BOOL closeable; // does the window have a close button?
|
||||
@property (readonly) BOOL collapseable; // does the window have a collapse button?
|
||||
@property BOOL collapsed; // is the window collapsed?
|
||||
@property BOOL fullScreen; // is the window full screen?
|
||||
@property NSPoint position; // the upper left position of the window
|
||||
@property (readonly) BOOL resizable; // is the window resizable?
|
||||
@property BOOL visible; // is the window visible?
|
||||
@property (readonly) BOOL zoomable; // is the window zoomable?
|
||||
@property BOOL zoomed; // is the window zoomed?
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the main iTunes window
|
||||
@interface MusicBrowserWindow : MusicWindow
|
||||
|
||||
@property (copy, readonly) SBObject *selection; // the selected songs
|
||||
@property (copy) MusicPlaylist *view; // the playlist currently displayed in the window
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the iTunes equalizer window
|
||||
@interface MusicEQWindow : MusicWindow
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the miniplayer window
|
||||
@interface MusicMiniplayerWindow : MusicWindow
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a sub-window showing a single playlist
|
||||
@interface MusicPlaylistWindow : MusicWindow
|
||||
|
||||
@property (copy, readonly) SBObject *selection; // the selected songs
|
||||
@property (copy, readonly) MusicPlaylist *view; // the playlist displayed in the window
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// the video window
|
||||
@interface MusicVideoWindow : MusicWindow
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Swinsian.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /Applications/Swinsian.app | sdp -fh --basename Swinsian
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class SwinsianItem, SwinsianColor, SwinsianWindow, SwinsianApplication, SwinsianPlaylist, SwinsianLibrary, SwinsianTrack, SwinsianLibraryTrack, SwinsianIPodTrack, SwinsianQueue, SwinsianSmartPlaylist, SwinsianNormalPlaylist, SwinsianPlaylistFolder, SwinsianAudioDevice;
|
||||
|
||||
enum SwinsianSaveOptions {
|
||||
SwinsianSaveOptionsYes = 'yes ' /* Save the file. */,
|
||||
SwinsianSaveOptionsNo = 'no ' /* Do not save the file. */,
|
||||
SwinsianSaveOptionsAsk = 'ask ' /* Ask the user whether or not to save the file. */
|
||||
};
|
||||
typedef enum SwinsianSaveOptions SwinsianSaveOptions;
|
||||
|
||||
enum SwinsianPlayerState {
|
||||
SwinsianPlayerStateStopped = 'kPSS',
|
||||
SwinsianPlayerStatePlaying = 'kPSP',
|
||||
SwinsianPlayerStatePaused = 'kPSp'
|
||||
};
|
||||
typedef enum SwinsianPlayerState SwinsianPlayerState;
|
||||
|
||||
@protocol SwinsianGenericMethods
|
||||
|
||||
- (void) closeSaving:(SwinsianSaveOptions)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) saveIn:(NSURL *)in_ as:(NSString *)as; // Save an object.
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Standard Suite
|
||||
*/
|
||||
|
||||
// A scriptable object.
|
||||
@interface SwinsianItem : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy) NSDictionary *properties; // All of the object's properties.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A color.
|
||||
@interface SwinsianColor : SBObject <SwinsianGenericMethods>
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A window.
|
||||
@interface SwinsianWindow : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy) NSString *name; // The full title of the window.
|
||||
- (NSNumber *) id; // The unique identifier of the window.
|
||||
@property NSRect bounds; // The bounding rectangle of the window.
|
||||
@property (readonly) BOOL closeable; // Whether the window has a close box.
|
||||
@property (readonly) BOOL titled; // Whether the window has a title bar.
|
||||
@property (copy) NSNumber *index; // The index of the window in the back-to-front window ordering.
|
||||
@property (readonly) BOOL floating; // Whether the window floats.
|
||||
@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 (readonly) BOOL resizable; // Whether the window can be resized.
|
||||
@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.
|
||||
@property (copy, readonly) NSArray<SwinsianTrack *> *selection; // Currently seleted tracks
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Swinsian Suite
|
||||
*/
|
||||
|
||||
// The application
|
||||
@interface SwinsianApplication : SBApplication
|
||||
|
||||
- (SBElementArray<SwinsianWindow *> *) windows;
|
||||
- (SBElementArray<SwinsianPlaylist *> *) playlists;
|
||||
- (SBElementArray<SwinsianSmartPlaylist *> *) smartPlaylists;
|
||||
- (SBElementArray<SwinsianNormalPlaylist *> *) normalPlaylists;
|
||||
- (SBElementArray<SwinsianLibrary *> *) libraries;
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
- (SBElementArray<SwinsianAudioDevice *> *) audioDevices;
|
||||
|
||||
@property (copy, readonly) NSString *name; // The name of the application.
|
||||
@property (readonly) BOOL frontmost; // Is this the frontmost (active) application?
|
||||
@property (copy, readonly) NSString *version; // The version of the application.
|
||||
@property NSInteger playerPosition; // the player’s position within the currently playing track in seconds.
|
||||
@property (copy, readonly) SwinsianTrack *currentTrack; // the currently playing track
|
||||
@property (copy) NSNumber *soundVolume; // the volume. (0 minimum, 100 maximum)
|
||||
@property (readonly) SwinsianPlayerState playerState; // are we stopped, paused or still playing?
|
||||
@property (copy, readonly) SwinsianQueue *playbackQueue; // the currently queued tracks
|
||||
@property (copy) SwinsianAudioDevice *outputDevice; // current audio output device
|
||||
|
||||
- (void) open:(NSURL *)x; // Open an object.
|
||||
- (void) print:(NSURL *)x; // Print an object.
|
||||
- (void) quitSaving:(SwinsianSaveOptions)saving; // Quit an application.
|
||||
- (void) play; // begin playing the current playlist
|
||||
- (void) pause; // pause playback
|
||||
- (void) nextTrack; // skip to the next track in the current playlist
|
||||
- (void) stop; // stop playback
|
||||
- (NSArray<SwinsianTrack *> *) searchPlaylist:(SwinsianPlaylist *)playlist for:(NSString *)for_; // search a playlist for tracks matching a string
|
||||
- (void) previousTrack; // skip back to the previous track
|
||||
- (void) playpause; // toggle play/pause
|
||||
- (void) addTracks:(NSArray<SwinsianTrack *> *)tracks to:(SwinsianNormalPlaylist *)to; // add a track to a playlist
|
||||
- (void) notify; // show currently playing track notification
|
||||
- (void) rescanTags:(NSArray<SwinsianTrack *> *)x; // rescan tags on tracks
|
||||
- (NSArray<SwinsianTrack *> *) findTrack:(NSString *)x; // Finds tracks for the given path
|
||||
- (void) removeTracks:(NSArray<SwinsianTrack *> *)tracks from:(SwinsianNormalPlaylist *)from; // remove tracks from a playlist
|
||||
|
||||
@end
|
||||
|
||||
// generic playlist type, subcasses include smart playlist and normal playlist
|
||||
@interface SwinsianPlaylist : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
@property (copy) NSString *name; // the name of the playlist
|
||||
@property (readonly) BOOL smart; // is this a smart playlist
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianLibrary : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a music track
|
||||
@interface SwinsianTrack : SwinsianItem
|
||||
|
||||
@property (copy) NSString *album; // the album of the track
|
||||
@property (copy) NSString *artist; // the artist
|
||||
@property (copy) NSString *composer; // the composer
|
||||
@property (copy) NSString *genre; // the genre
|
||||
@property (copy, readonly) NSString *time; // the length of the track in text format as MM:SS
|
||||
@property NSInteger year; // the year the track was recorded
|
||||
@property (copy, readonly) NSDate *dateAdded; // the date the track was added to the library
|
||||
@property (readonly) double duration; // the length of the track in seconds
|
||||
@property (copy, readonly) NSString *location; // location on disk
|
||||
@property (readonly) BOOL iPodTrack; // TRUE if the track is on an iPod
|
||||
@property (copy) NSString *name; // the title of the track (same as title)
|
||||
@property (readonly) NSInteger bitRate; // the bitrate of the track
|
||||
@property (copy, readonly) NSString *kind; // a text description of the type of file the track is
|
||||
@property (copy) NSNumber *rating; // Track rating. 0-5
|
||||
@property NSInteger trackNumber; // the Track number
|
||||
@property (readonly) NSInteger fileSize; // file size in bytes
|
||||
@property (copy, readonly) NSImage *albumArt; // the album artwork
|
||||
@property (copy, readonly) NSString *artFormat; // the data format for this piece of artwork. text that will be "PNG" or "JPEG". getting the album art property first will mean this information has been retrieved already, otherwise the tags for the file will have to be re-read
|
||||
@property (copy) NSNumber *discNumber; // the disc number
|
||||
@property (copy) NSNumber *discCount; // the total number of discs in the album
|
||||
- (NSString *) id; // uuid
|
||||
@property (copy) NSString *albumArtist; // the album artist
|
||||
@property (copy, readonly) NSString *albumArtistOrArtist; // the album artist of the track, or is none is set, the artist
|
||||
@property BOOL compilation; // compilation flag
|
||||
@property (copy) NSString *title; // track title (the same as name)
|
||||
@property (copy) NSString *comment; // the comment
|
||||
@property (copy, readonly) NSDate *dateCreated; // the date created
|
||||
@property (readonly) NSInteger channels; // audio channel count
|
||||
@property (readonly) NSInteger sampleRate; // audio sample rate
|
||||
@property (readonly) NSInteger bitDepth; // the audio bit depth
|
||||
@property (copy) NSDate *lastPlayed; // date track was last played
|
||||
@property (copy) NSString *lyrics; // track lyrics
|
||||
@property (copy, readonly) NSString *path; // POSIX style path
|
||||
@property (copy) NSString *grouping; // grouping
|
||||
@property (copy) NSString *publisher; // the publisher
|
||||
@property (copy) NSString *conductor; // the conductor
|
||||
@property (copy) NSString *objectDescription; // the description
|
||||
@property (copy, readonly) NSString *encoder; // the encoder
|
||||
@property (copy, readonly) NSString *copyright; // the copyright
|
||||
@property (copy) NSString *catalogNumber; // the catalog number
|
||||
@property (copy, readonly) NSDate *dateModified; // the date modified
|
||||
@property NSInteger playCount; // the play count
|
||||
@property (copy) NSNumber *trackCount; // the total number of tracks in the album
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianLibraryTrack : SwinsianTrack
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianIPodTrack : SwinsianTrack
|
||||
|
||||
@property (copy, readonly) NSString *iPodName; // the name of the iPod this track is on
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// The playback queue
|
||||
@interface SwinsianQueue : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a smart playlist
|
||||
@interface SwinsianSmartPlaylist : SwinsianPlaylist
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a normal, non-smart, playlist
|
||||
@interface SwinsianNormalPlaylist : SwinsianPlaylist
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
- (NSString *) id; // uuid
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// folder of playlists
|
||||
@interface SwinsianPlaylistFolder : SwinsianPlaylist
|
||||
|
||||
- (SBElementArray<SwinsianPlaylist *> *) playlists;
|
||||
|
||||
- (NSString *) id; // uuid
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// an audio output device
|
||||
@interface SwinsianAudioDevice : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy, readonly) NSString *name; // device name
|
||||
- (NSString *) id; // uuid
|
||||
- (void) setId: (NSString *) id;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAutoPauseMusicPrefs.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
@@ -69,6 +69,7 @@ static NSInteger const kPrefsMenuAutoPauseHeaderTag = 1;
|
||||
action:@selector(handleMusicPlayerChange:)
|
||||
keyEquivalent:@""
|
||||
atIndex:musicPlayerItemsIndex];
|
||||
menuItem.toolTip = musicPlayer.toolTip;
|
||||
|
||||
musicPlayerMenuItems = [musicPlayerMenuItems arrayByAddingObject:menuItem];
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMPreferencesMenu.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018, 2019 Kyle Neideck
|
||||
//
|
||||
// Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather
|
||||
// than to other BGMApp classes.
|
||||
@@ -26,6 +26,7 @@
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMStatusBarItem.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@@ -33,11 +34,12 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BGMPreferencesMenu : NSObject <NSMenuDelegate>
|
||||
@interface BGMPreferencesMenu : NSObject
|
||||
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMPreferencesMenu.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018, 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAutoPauseMusicPrefs.h"
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMAboutPanel.h"
|
||||
|
||||
|
||||
@@ -33,13 +32,19 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Interface Builder tags
|
||||
static NSInteger const kPreferencesMenuItemTag = 1;
|
||||
static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||
static NSInteger const kBGMIconMenuItemTag = 2;
|
||||
static NSInteger const kVolumeIconMenuItemTag = 3;
|
||||
static NSInteger const kAboutPanelMenuItemTag = 4;
|
||||
|
||||
@implementation BGMPreferencesMenu {
|
||||
// Menu sections
|
||||
// Menu sections/items
|
||||
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
|
||||
BGMOutputDevicePrefs* outputDevicePrefs;
|
||||
|
||||
NSMenuItem* bgmIconMenuItem;
|
||||
NSMenuItem* volumeIconMenuItem;
|
||||
|
||||
// The menu item you press to open BGMApp's main menu.
|
||||
BGMStatusBarItem* statusBarItem;
|
||||
|
||||
// The About Background Music window
|
||||
BGMAboutPanel* aboutPanel;
|
||||
}
|
||||
@@ -47,20 +52,33 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
statusBarItem:(BGMStatusBarItem*)inStatusBarItem
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
|
||||
if ((self = [super init])) {
|
||||
NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu];
|
||||
[prefsMenu setDelegate:self];
|
||||
|
||||
autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu
|
||||
audioDevices:inAudioDevices
|
||||
musicPlayers:inMusicPlayers];
|
||||
|
||||
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices];
|
||||
|
||||
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
|
||||
|
||||
|
||||
statusBarItem = inStatusBarItem;
|
||||
|
||||
// Set up the menu items under the "Status Bar Icon" heading.
|
||||
bgmIconMenuItem = [prefsMenu itemWithTag:kBGMIconMenuItemTag];
|
||||
bgmIconMenuItem.state =
|
||||
(statusBarItem.icon == BGMFermataStatusBarIcon) ? NSOnState : NSOffState;
|
||||
[bgmIconMenuItem setTarget:self];
|
||||
[bgmIconMenuItem setAction:@selector(useBGMStatusBarIcon)];
|
||||
|
||||
volumeIconMenuItem = [prefsMenu itemWithTag:kVolumeIconMenuItemTag];
|
||||
volumeIconMenuItem.state =
|
||||
(statusBarItem.icon == BGMVolumeStatusBarIcon) ? NSOnState : NSOffState;
|
||||
[volumeIconMenuItem setTarget:self];
|
||||
[volumeIconMenuItem setAction:@selector(useVolumeStatusBarIcon)];
|
||||
|
||||
// Set up the "About Background Music" menu item
|
||||
NSMenuItem* aboutMenuItem = [prefsMenu itemWithTag:kAboutPanelMenuItemTag];
|
||||
[aboutMenuItem setTarget:aboutPanel];
|
||||
@@ -70,10 +88,27 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark NSMenuDelegate
|
||||
- (void) useBGMStatusBarIcon {
|
||||
// Change the icon.
|
||||
statusBarItem.icon = BGMFermataStatusBarIcon;
|
||||
|
||||
- (void) menuNeedsUpdate:(NSMenu*)menu {
|
||||
[outputDevicePrefs populatePreferencesMenu:menu];
|
||||
// Select/deselect the menu items.
|
||||
bgmIconMenuItem.state = NSOnState;
|
||||
volumeIconMenuItem.state = NSOffState;
|
||||
}
|
||||
|
||||
- (void) useVolumeStatusBarIcon {
|
||||
// TODO: Maybe we should show a message that tells the user how to hide the built-in volume
|
||||
// icon. They probably won't want two status bar items that look the same. Or we might be
|
||||
// able to automatically hide the built-in icon while BGMApp is running and show it again
|
||||
// when BGMApp is closed.
|
||||
|
||||
// Change the icon.
|
||||
statusBarItem.icon = BGMVolumeStatusBarIcon;
|
||||
|
||||
// Select/deselect the menu items.
|
||||
bgmIconMenuItem.state = NSOffState;
|
||||
volumeIconMenuItem.state = NSOnState;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/*
|
||||
* BGMApp.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef "/Applications/Background Music.app" | sdp -fh --basename BGMApp
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
@@ -14,11 +17,11 @@
|
||||
* Background Music
|
||||
*/
|
||||
|
||||
// an output audio device
|
||||
// A hardware device that can play audio
|
||||
@interface BGMAppOutputDevice : SBObject
|
||||
|
||||
@property (copy, readonly) NSString *name;
|
||||
@property BOOL selected; // is this the device to be used for audio output?
|
||||
@property (copy, readonly) NSString *name; // The name of the output device.
|
||||
@property BOOL selected; // Is this the device to be used for audio output?
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,5 +30,7 @@
|
||||
|
||||
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
|
||||
|
||||
@property (copy) BGMAppOutputDevice *selectedOutputDevice; // The device to be used for audio output
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAppUITests.mm
|
||||
// BGMAppUITests
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
// Copyright © 2017, 2018 Kyle Neideck
|
||||
//
|
||||
// You might want to use Xcode's UI test recording feature if you add new tests.
|
||||
//
|
||||
@@ -34,6 +34,8 @@
|
||||
// TODO: Skip these tests if macOS SDK 10.11 or higher isn't available.
|
||||
// TODO: Mock BGMDevice and music players.
|
||||
|
||||
#if __clang_major__ >= 9
|
||||
|
||||
@interface BGMAppUITests : XCTestCase
|
||||
@end
|
||||
|
||||
@@ -60,8 +62,8 @@
|
||||
// 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"];
|
||||
icon = [app.menuBars childrenMatchingType:XCUIElementTypeStatusItem].element;
|
||||
|
||||
// TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp.
|
||||
|
||||
@@ -72,6 +74,20 @@
|
||||
|
||||
// Launch BGMApp.
|
||||
[app launch];
|
||||
|
||||
if (![icon waitForExistenceWithTimeout:1.0]) {
|
||||
// The status bar icon/button has this type when using older versions of XCTest, so try
|
||||
// both. (Actually, it might depend on the macOS or Xcode version. I'm not sure.)
|
||||
XCUIElement* iconOldType =
|
||||
[app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
|
||||
if (![iconOldType waitForExistenceWithTimeout:5.0]) {
|
||||
icon = iconOldType;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the initial elements.
|
||||
XCTAssert([app waitForExistenceWithTimeout:10.0]);
|
||||
XCTAssert([icon waitForExistenceWithTimeout:10.0]);
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
@@ -91,10 +107,22 @@
|
||||
- (void) testCycleOutputDevices {
|
||||
const int NUM_CYCLES = 2;
|
||||
|
||||
// Get the list of output devices from the preferences menu.
|
||||
// sbApp lets us use AppleScript to query BGMApp and check the test has made the changes to its
|
||||
// settings we expect.
|
||||
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
|
||||
|
||||
// Get macOS to show the "'Xcode' wants to control 'Background Music'" dialog before we start
|
||||
// the test so it doesn't interrupt it.
|
||||
[[sbApp selectedOutputDevice] name];
|
||||
|
||||
// Click the icon to open the main menu.
|
||||
[icon click];
|
||||
[prefs hover];
|
||||
NSArray<XCUIElement*>* outputDeviceMenuItems = [self outputDeviceMenuItems];
|
||||
|
||||
// Get the list of output devices from the main menu.
|
||||
// BGMOutputDeviceMenuSection::createMenuItemForDevice gives every output device menu item the
|
||||
// accessibility identifier "output-device" so we can find all of them here.
|
||||
NSArray<XCUIElement*>* outputDeviceMenuItems =
|
||||
[menuItems matchingIdentifier:@"output-device"].allElementsBoundByIndex;
|
||||
|
||||
// For debugging certain issues, it can be useful to repeatedly switch between two
|
||||
// devices:
|
||||
@@ -105,14 +133,10 @@
|
||||
// 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.
|
||||
@@ -128,29 +152,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
@@ -234,3 +235,5 @@
|
||||
|
||||
@end
|
||||
|
||||
#endif /* __clang_major__ >= 9 */
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayersUnitTests.mm
|
||||
// BGMAppUnitTests
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Unit include
|
||||
@@ -72,6 +72,47 @@
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
class BGMMockBackgroundMusicDevice
|
||||
:
|
||||
public BGMBackgroundMusicDevice
|
||||
{
|
||||
|
||||
public:
|
||||
CFStringRef GetMusicPlayerBundleID() const;
|
||||
void SetMusicPlayerBundleID(CFStringRef inBundleID);
|
||||
|
||||
private:
|
||||
CFStringRef mMusicPlayerBundleID = CFSTR("");
|
||||
|
||||
};
|
||||
|
||||
CFStringRef BGMMockBackgroundMusicDevice::GetMusicPlayerBundleID() const
|
||||
{
|
||||
return mMusicPlayerBundleID;
|
||||
}
|
||||
|
||||
void BGMMockBackgroundMusicDevice::SetMusicPlayerBundleID(CFStringRef inBundleID)
|
||||
{
|
||||
mMusicPlayerBundleID = inBundleID;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
@interface BGMMockAudioDeviceManager : BGMAudioDeviceManager
|
||||
@end
|
||||
|
||||
@implementation BGMMockAudioDeviceManager {
|
||||
BGMBackgroundMusicDevice bgmDevice;
|
||||
}
|
||||
|
||||
- (BGMBackgroundMusicDevice) bgmDevice {
|
||||
return bgmDevice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
@interface BGMMusicPlayersUnitTests : XCTestCase
|
||||
@end
|
||||
|
||||
@@ -85,8 +126,8 @@
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
|
||||
devices = [BGMAudioDeviceManager new];
|
||||
|
||||
devices = [BGMMockAudioDeviceManager new];
|
||||
defaults = [BGMMockUserDefaults new];
|
||||
|
||||
// These are the IDs hardcoded in BGMSpotify and BGMVLC.
|
||||
@@ -95,8 +136,6 @@
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
[self resetDevice];
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2019 Background Music contributors</string>
|
||||
<key>XPCService</key>
|
||||
<dict>
|
||||
<key>ServiceType</key>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# post_install.sh
|
||||
# BGMXPCHelper
|
||||
#
|
||||
# Copyright © 2016, 2017 Kyle Neideck
|
||||
# Copyright © 2016-2018 Kyle Neideck
|
||||
#
|
||||
# Installs BGMXPCHelper's launchd plist file and "bootstraps" (registers/enables) it with launchd.
|
||||
#
|
||||
@@ -65,6 +65,14 @@ else
|
||||
RESOURCES_PATH="$3"
|
||||
fi
|
||||
|
||||
# If DEPLOYMENT_POSTPROCESSING is true, xcodebuild calls this script even if you're just building
|
||||
# (and not also installing). I'm not sure why, as we have the "run script only when installing"
|
||||
# option enabled.
|
||||
if ! [[ -z ${ACTION} ]] && [[ "${ACTION}" != "install" ]]; then
|
||||
echo "$0 should only be called during an install. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Safe mode.
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -236,7 +236,10 @@ bool CAHALAudioObject::ObjectExists(AudioObjectID inObjectID)
|
||||
{
|
||||
Boolean isSettable;
|
||||
CAPropertyAddress theAddress(kAudioObjectPropertyClass);
|
||||
return (inObjectID == 0) || (AudioObjectIsPropertySettable(inObjectID, &theAddress, &isSettable) != 0);
|
||||
// BGM edit: Negated the expression returned. Seems to have been a bug.
|
||||
//return (inObjectID == 0) || (AudioObjectIsPropertySettable(inObjectID, &theAddress, &isSettable) != 0);
|
||||
return (inObjectID != kAudioObjectUnknown) && (AudioObjectIsPropertySettable(inObjectID, &theAddress, &isSettable) == kAudioHardwareNoError);
|
||||
// BGM edit end
|
||||
}
|
||||
|
||||
UInt32 CAHALAudioObject::GetNumberOwnedObjects(AudioClassID inClass) const
|
||||
|
||||
@@ -156,15 +156,21 @@ public:
|
||||
void GetItemByIndex(UInt32 inIndex, AudioObjectPropertyAddress& outAddress) const { if(inIndex < mAddressList.size()) { outAddress = mAddressList.at(inIndex); } }
|
||||
const AudioObjectPropertyAddress* GetItems() const { return &(*mAddressList.begin()); }
|
||||
AudioObjectPropertyAddress* GetItems() { return &(*mAddressList.begin()); }
|
||||
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
bool HasItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::CongruentEqualTo(), inAddress)); return theIterator != mAddressList.end(); }
|
||||
bool HasExactItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); return theIterator != mAddressList.end(); }
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
void AppendItem(const AudioObjectPropertyAddress& inAddress) { mAddressList.push_back(inAddress); }
|
||||
void AppendUniqueItem(const AudioObjectPropertyAddress& inAddress) { if(!HasItem(inAddress)) { mAddressList.push_back(inAddress); } }
|
||||
void AppendUniqueExactItem(const AudioObjectPropertyAddress& inAddress) { if(!HasExactItem(inAddress)) { mAddressList.push_back(inAddress); } }
|
||||
void InsertItemAtIndex(UInt32 inIndex, const AudioObjectPropertyAddress& inAddress) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.insert(theIterator, inAddress); } else { mAddressList.push_back(inAddress); } }
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
void EraseExactItem(const AudioObjectPropertyAddress& inAddress) { AddressList::iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); if(theIterator != mAddressList.end()) { mAddressList.erase(theIterator); } }
|
||||
#pragma clang diagnostic pop
|
||||
void EraseItemAtIndex(UInt32 inIndex) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.erase(theIterator); } }
|
||||
void EraseAllItems() { mAddressList.clear(); }
|
||||
|
||||
|
||||
@@ -9,29 +9,29 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
19FE742AEBE30B21C4CF9285 /* BGM_Control.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */; };
|
||||
19FE761291BF07AEA278F25C /* BGM_MuteControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */; };
|
||||
19FE766482B57D852CCF6F0A /* BGM_MuteControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */; };
|
||||
19FE77D40F15EA060B462D83 /* BGM_Control.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */; };
|
||||
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 */; };
|
||||
19FE766482B57D852CCF6F0A /* BGM_MuteControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_MuteControl.cpp"; }; };
|
||||
19FE77D40F15EA060B462D83 /* BGM_Control.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Control.cpp"; }; };
|
||||
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B01C642C600084C15A /* BGM_Client.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Client.cpp"; }; };
|
||||
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B21C642C600084C15A /* BGM_ClientMap.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_ClientMap.cpp"; }; };
|
||||
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C0CB6B41C642C600084C15A /* BGM_Clients.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Clients.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 */; };
|
||||
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_TaskQueue.cpp"; }; };
|
||||
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */; };
|
||||
1C7010751F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */; };
|
||||
1C7010751F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_AudibleState.cpp"; }; };
|
||||
1C7010761F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */; };
|
||||
1C7010791F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */; };
|
||||
1C7010791F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_VolumeControl.cpp"; }; };
|
||||
1C70107A1F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */; };
|
||||
1C780FEF1FEE78E800497FAD /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C780FEE1FEE78E800497FAD /* Accelerate.framework */; };
|
||||
1C780FF41FF275F300497FAD /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C780FEE1FEE78E800497FAD /* Accelerate.framework */; };
|
||||
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 */; };
|
||||
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Stream.cpp"; }; };
|
||||
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_PlugInInterface.cpp"; }; };
|
||||
1CB8B3761BBBD924000E2DD1 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */; };
|
||||
1CB8B3771BBBD924000E2DD1 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */; };
|
||||
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 */; };
|
||||
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3901BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp */; };
|
||||
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B37B1BBCCF62000E2DD1 /* BGM_PlugIn.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_PlugIn.cpp"; }; };
|
||||
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Device.cpp"; }; };
|
||||
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3811BBCE7B5000E2DD1 /* BGM_Object.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_Object.cpp"; }; };
|
||||
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3901BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_WrappedAudioEngine.cpp"; }; };
|
||||
1CBB322C1BDD3A3000C9BD55 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */; };
|
||||
1CC1DF8D1BE5705700FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */; };
|
||||
1CC1DF8E1BE5706C00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68F1BE2683900167F5D /* CACFArray.cpp */; };
|
||||
@@ -41,25 +41,25 @@
|
||||
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 */; };
|
||||
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_NullDevice.cpp"; }; };
|
||||
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-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 */; };
|
||||
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 27381A141C8EF50F00DF167C /* BGM_XPCHelper.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-BGM_XPCHelper.m"; }; };
|
||||
2743C9CD1D7EF8760089613B /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68F1BE2683900167F5D /* CACFArray.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CACFArray.cpp"; }; };
|
||||
2743C9CF1D7EF8760089613B /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CACFDictionary.cpp"; }; };
|
||||
2743C9D11D7EF8760089613B /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CACFNumber.cpp"; }; };
|
||||
2743C9D31D7EF8760089613B /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38A1BBCF4A9000E2DD1 /* CACFString.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CACFString.cpp"; }; };
|
||||
2743C9D51D7EF8760089613B /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF871BE558B000FB8FE4 /* CADebugger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CADebugger.cpp"; }; };
|
||||
2743C9D71D7EF8760089613B /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3701BBBD8A4000E2DD1 /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CADebugMacros.cpp"; }; };
|
||||
2743C9D91D7EF8760089613B /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3781BBBDFA2000E2DD1 /* CADebugPrintf.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CADebugPrintf.cpp"; }; };
|
||||
2743C9DB1D7EF8760089613B /* CADispatchQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3931BBD2418000E2DD1 /* CADispatchQueue.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CADispatchQueue.cpp"; }; };
|
||||
2743C9DE1D7EF8760089613B /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3871BBCF08A000E2DD1 /* CAHostTimeBase.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CAHostTimeBase.cpp"; }; };
|
||||
2743C9E01D7EF8760089613B /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CAMutex.cpp"; }; };
|
||||
2743C9E21D7EF8760089613B /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-CAPThread.cpp"; }; };
|
||||
2743C9E41D7EF8760089613B /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38C1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=libPublicUtility-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 */; };
|
||||
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 275343BC1DE9B44900DF3858 /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMDriver-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 */; };
|
||||
@@ -440,6 +440,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
);
|
||||
mainGroup = 1CB8B3591BBBB69C000E2DD1;
|
||||
@@ -634,6 +635,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
@@ -700,6 +702,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
@@ -784,6 +787,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEPLOYMENT_POSTPROCESSING = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = "compiler-default";
|
||||
GCC_ENABLE_CPP_RTTI = NO;
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
#include "CACFArray.h"
|
||||
#include "CACFString.h"
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAHostTimeBase.h"
|
||||
|
||||
// STL Includes
|
||||
#include <stdexcept>
|
||||
|
||||
// System Includes
|
||||
#include <mach/mach_time.h>
|
||||
#include <CoreAudio/AudioHardwareBase.h>
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ BGM_Device::BGM_Device(AudioObjectID inObjectID,
|
||||
mMuteControl(inOutputMuteControlID, GetObjectID())
|
||||
{
|
||||
// Initialises the loopback clock with the default sample rate and, if there is one, sets the wrapped device to the same sample rate
|
||||
SetSampleRate(kSampleRateDefault);
|
||||
SetSampleRate(kSampleRateDefault, true);
|
||||
}
|
||||
|
||||
BGM_Device::~BGM_Device()
|
||||
@@ -196,12 +196,8 @@ void BGM_Device::Deactivate()
|
||||
|
||||
void BGM_Device::InitLoopback()
|
||||
{
|
||||
// Calculate the host ticks per frame for the loopback timer
|
||||
struct mach_timebase_info theTimeBaseInfo;
|
||||
mach_timebase_info(&theTimeBaseInfo);
|
||||
Float64 theHostClockFrequency = theTimeBaseInfo.denom / theTimeBaseInfo.numer;
|
||||
theHostClockFrequency *= 1000000000.0;
|
||||
mLoopbackTime.hostTicksPerFrame = theHostClockFrequency / mLoopbackSampleRate;
|
||||
// Calculate the number of host clock ticks per frame for our loopback clock.
|
||||
mLoopbackTime.hostTicksPerFrame = CAHostTimeBase::GetFrequency() / mLoopbackSampleRate;
|
||||
|
||||
// Zero-out the loopback buffer
|
||||
// 2 channels * 32-bit float = bytes in each frame
|
||||
@@ -1268,7 +1264,7 @@ void BGM_Device::GetZeroTimeStamp(Float64& outSampleTime, UInt64& outHostTime, U
|
||||
UInt64 theNextHostTime;
|
||||
|
||||
// get the current host time
|
||||
theCurrentHostTime = mach_absolute_time();
|
||||
theCurrentHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
|
||||
// calculate the next host time
|
||||
theHostTicksPerRingBuffer = mLoopbackTime.hostTicksPerFrame * kLoopbackRingBufferFrameSize;
|
||||
@@ -1707,7 +1703,7 @@ void BGM_Device::SetEnabledControls(bool inVolumeEnabled, bool inMuteEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
void BGM_Device::SetSampleRate(Float64 inSampleRate)
|
||||
void BGM_Device::SetSampleRate(Float64 inSampleRate, bool force)
|
||||
{
|
||||
// We try to support any sample rate a real output device might.
|
||||
ThrowIf(inSampleRate < 1.0,
|
||||
@@ -1715,24 +1711,37 @@ void BGM_Device::SetSampleRate(Float64 inSampleRate)
|
||||
"BGM_Device::SetSampleRate: unsupported sample rate");
|
||||
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
// Update the sample rate on the wrapped device if we have one.
|
||||
if(mWrappedAudioEngine != nullptr)
|
||||
|
||||
Float64 theCurrentSampleRate = GetSampleRate();
|
||||
|
||||
if((inSampleRate != theCurrentSampleRate) || force) // Check whether we need to change it.
|
||||
{
|
||||
kern_return_t theError = _HW_SetSampleRate(inSampleRate);
|
||||
ThrowIfKernelError(theError,
|
||||
CAException(kAudioHardwareUnspecifiedError),
|
||||
"BGM_Device::SetSampleRate: Error setting the sample rate on the "
|
||||
"wrapped audio device.");
|
||||
DebugMsg("BGM_Device::SetSampleRate: Changing the sample rate from %f to %f",
|
||||
theCurrentSampleRate,
|
||||
inSampleRate);
|
||||
|
||||
// Update the sample rate on the wrapped device if we have one.
|
||||
if(mWrappedAudioEngine != nullptr)
|
||||
{
|
||||
kern_return_t theError = _HW_SetSampleRate(inSampleRate);
|
||||
ThrowIfKernelError(theError,
|
||||
CAException(kAudioHardwareUnspecifiedError),
|
||||
"BGM_Device::SetSampleRate: Error setting the sample rate on the "
|
||||
"wrapped audio device.");
|
||||
}
|
||||
|
||||
// Update the sample rate for loopback.
|
||||
mLoopbackSampleRate = inSampleRate;
|
||||
InitLoopback();
|
||||
|
||||
// Update the streams.
|
||||
mInputStream.SetSampleRate(inSampleRate);
|
||||
mOutputStream.SetSampleRate(inSampleRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugMsg("BGM_Device::SetSampleRate: The sample rate is already set to %f", inSampleRate);
|
||||
}
|
||||
|
||||
// Update the sample rate for loopback.
|
||||
mLoopbackSampleRate = inSampleRate;
|
||||
InitLoopback();
|
||||
|
||||
// Update the streams.
|
||||
mInputStream.SetSampleRate(inSampleRate);
|
||||
mOutputStream.SetSampleRate(inSampleRate);
|
||||
}
|
||||
|
||||
bool BGM_Device::IsStreamID(AudioObjectID inObjectID) const noexcept
|
||||
@@ -1763,7 +1772,7 @@ kern_return_t BGM_Device::_HW_StartIO()
|
||||
|
||||
// Reset the loopback timing values
|
||||
mLoopbackTime.numberTimeStamps = 0;
|
||||
mLoopbackTime.anchorHostTime = mach_absolute_time();
|
||||
mLoopbackTime.anchorHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
// ...and the most-recent audible/silent sample times. mAudibleState is usually guarded by the
|
||||
// IO mutex, but we haven't started IO yet (and this function can only be called by one thread
|
||||
// at a time).
|
||||
|
||||
@@ -162,10 +162,13 @@ private:
|
||||
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
|
||||
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
|
||||
@param inNewSampleRate The sample rate.
|
||||
@param force If true, set the sample rate on the device even if it's currently set to
|
||||
inNewSampleRate.
|
||||
@throws CAException if inNewSampleRate < 1 or if applying the sample rate to one of the streams
|
||||
fails.
|
||||
*/
|
||||
void SetSampleRate(Float64 inNewSampleRate);
|
||||
void SetSampleRate(Float64 inNewSampleRate, bool force = false);
|
||||
|
||||
/*! @return True if inObjectID is the ID of one of this device's streams. */
|
||||
inline bool IsStreamID(AudioObjectID inObjectID) const noexcept;
|
||||
@@ -224,7 +227,7 @@ private:
|
||||
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.
|
||||
Float64 mPendingSampleRate;
|
||||
Float64 mPendingSampleRate = kSampleRateDefault;
|
||||
|
||||
BGM_WrappedAudioEngine* __nullable mWrappedAudioEngine;
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@
|
||||
#include "CAException.h"
|
||||
#include "CAPropertyAddress.h"
|
||||
#include "CADispatchQueue.h"
|
||||
|
||||
// System Includes
|
||||
#include <mach/mach_time.h> // For mach_absolute_time
|
||||
#include "CAHostTimeBase.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -89,12 +87,8 @@ void BGM_NullDevice::Activate()
|
||||
// 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;
|
||||
// Calculate the number of host clock ticks per frame for this device's clock.
|
||||
mHostTicksPerFrame = CAHostTimeBase::GetFrequency() / kSampleRate;
|
||||
|
||||
SendDeviceIsAlivePropertyNotifications();
|
||||
}
|
||||
@@ -410,7 +404,7 @@ void BGM_NullDevice::StartIO(UInt32 inClientID)
|
||||
{
|
||||
// Reset the clock.
|
||||
mNumberTimeStamps = 0;
|
||||
mAnchorHostTime = mach_absolute_time();
|
||||
mAnchorHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
|
||||
// Send notifications.
|
||||
DebugMsg("BGM_NullDevice::StartIO: Sending kAudioDevicePropertyDeviceIsRunning");
|
||||
@@ -460,7 +454,7 @@ void BGM_NullDevice::GetZeroTimeStamp(Float64& outSampleTime,
|
||||
// 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();
|
||||
UInt64 theCurrentHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
|
||||
// Calculate the next host time.
|
||||
Float64 theHostTicksPerPeriod = mHostTicksPerFrame * static_cast<Float64>(kZeroTimeStampPeriod);
|
||||
|
||||
@@ -98,14 +98,16 @@ BGM_TaskQueue::BGM_TaskQueue()
|
||||
BGM_TaskQueue::~BGM_TaskQueue()
|
||||
{
|
||||
// Join the worker threads
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ true);
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ false);
|
||||
|
||||
BGMLogAndSwallowExceptionsMsg("BGM_TaskQueue::~BGM_TaskQueue", "QueueSync", ([&] {
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ true);
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ false);
|
||||
}));
|
||||
|
||||
// Destroy the semaphores
|
||||
auto destroySemaphore = [] (semaphore_t inSemaphore) {
|
||||
kern_return_t theError = semaphore_destroy(mach_task_self(), inSemaphore);
|
||||
|
||||
BGM_Utils::ThrowIfMachError("BGM_TaskQueue::~BGM_TaskQueue", "semaphore_destroy", theError);
|
||||
BGM_Utils::LogIfMachError("BGM_TaskQueue::~BGM_TaskQueue", "semaphore_destroy", theError);
|
||||
};
|
||||
|
||||
destroySemaphore(mRealTimeThreadWorkQueuedSemaphore);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -37,7 +37,7 @@
|
||||
</array>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2019 Background Music contributors</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
|
||||
@@ -36,7 +36,36 @@
|
||||
#include <stdexcept>
|
||||
|
||||
|
||||
@interface BGM_DeviceTests : XCTestCase
|
||||
// Subclass BGM_Device to add some test-only functions.
|
||||
class TestBGM_Device
|
||||
:
|
||||
public BGM_Device
|
||||
{
|
||||
|
||||
public:
|
||||
TestBGM_Device();
|
||||
~TestBGM_Device() = default;
|
||||
|
||||
};
|
||||
|
||||
TestBGM_Device::TestBGM_Device()
|
||||
:
|
||||
BGM_Device(kObjectID_Device,
|
||||
CFSTR(kDeviceName),
|
||||
CFSTR(kBGMDeviceUID),
|
||||
CFSTR(kBGMDeviceModelUID),
|
||||
kObjectID_Stream_Input,
|
||||
kObjectID_Stream_Output,
|
||||
kObjectID_Volume_Output_Master,
|
||||
kObjectID_Mute_Output_Master)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
|
||||
|
||||
@interface BGM_DeviceTests : XCTestCase {
|
||||
TestBGM_Device* testDevice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -45,29 +74,79 @@
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
testDevice = new TestBGM_Device();
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
// Reminder: add code here, above the super call
|
||||
delete testDevice;
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void) testDoIOOperation_writeMix_readInput {
|
||||
// The number of audio frames to send/receive in the IO operations.
|
||||
const int kFrameSize = 512;
|
||||
|
||||
// Choose a sample time that will make the data wrap around to the start of the device's
|
||||
// internal ring buffer.
|
||||
AudioServerPlugInIOCycleInfo cycleInfo {};
|
||||
cycleInfo.mOutputTime.mSampleTime = kLoopbackRingBufferFrameSize - 25.0;
|
||||
|
||||
// Generate the test input data.
|
||||
Float32 inputBuffer[kFrameSize * 2];
|
||||
|
||||
for(int i = 0; i < kFrameSize * 2; i++)
|
||||
{
|
||||
inputBuffer[i] = static_cast<Float32>(i);
|
||||
}
|
||||
|
||||
// Send a copy of the input buffer just in case DoIOOperation modifies the data for some reason.
|
||||
Float32 inputBufferCopy[kFrameSize * 2];
|
||||
memcpy(inputBufferCopy, inputBuffer, sizeof(inputBuffer));
|
||||
|
||||
// Send the input data to the device.
|
||||
testDevice->DoIOOperation(/* inStreamObjectID = */ kObjectID_Stream_Output,
|
||||
/* inClientID = */ 0,
|
||||
/* inOperationID = */ kAudioServerPlugInIOOperationWriteMix,
|
||||
/* inIOBufferFrameSize = */ kFrameSize,
|
||||
/* inIOCycleInfo = */ cycleInfo,
|
||||
/* ioMainBuffer = */ inputBuffer,
|
||||
/* ioSecondaryBuffer = */ nullptr);
|
||||
|
||||
// Request data from the same point in time so we get the same data back.
|
||||
cycleInfo.mInputTime.mSampleTime = kLoopbackRingBufferFrameSize - 25.0;
|
||||
|
||||
// Read the data back from the device.
|
||||
Float32 outputBuffer[kFrameSize * 2];
|
||||
|
||||
testDevice->DoIOOperation(/* inStreamObjectID = */ kObjectID_Stream_Output,
|
||||
/* inClientID = */ 0,
|
||||
/* inOperationID = */ kAudioServerPlugInIOOperationReadInput,
|
||||
/* inIOBufferFrameSize = */ kFrameSize,
|
||||
/* inIOCycleInfo = */ cycleInfo,
|
||||
/* ioMainBuffer = */ outputBuffer,
|
||||
/* ioSecondaryBuffer = */ nullptr);
|
||||
|
||||
// Check the output matches the input.
|
||||
for(int i = 0; i < kFrameSize * 2; i++)
|
||||
{
|
||||
XCTAssertEqual(inputBuffer[i], outputBuffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) testCustomPropertyMusicPlayerBundleID {
|
||||
BGM_Device& device = BGM_Device::GetInstance();
|
||||
|
||||
// Convenience wrappers
|
||||
auto getBundleID = [&](UInt32 inDataSize = sizeof(CFStringRef)){
|
||||
CFStringRef bundleID = NULL;
|
||||
CFStringRef bundleID = nullptr;
|
||||
UInt32 outDataSize;
|
||||
|
||||
device.GetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 3,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ NULL,
|
||||
/* inDataSize = */ inDataSize,
|
||||
/* outDataSize = */ outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(&bundleID));
|
||||
testDevice->GetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 3,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ nullptr,
|
||||
/* inDataSize = */ inDataSize,
|
||||
/* outDataSize = */ outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(&bundleID));
|
||||
|
||||
// This isn't technically required, but we're unlikely to ever want to return any more/less data from GetPropertyData.
|
||||
XCTAssertEqual(outDataSize, sizeof(CFStringRef));
|
||||
@@ -76,13 +155,13 @@
|
||||
};
|
||||
|
||||
auto setBundleID = [&](const CFStringRef* __nullable bundleID, UInt32 dataSize = sizeof(CFStringRef)){
|
||||
device.SetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 1234,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ NULL,
|
||||
/* inDataSize = */ dataSize,
|
||||
/* inData = */ reinterpret_cast<const void* __nonnull>(bundleID));
|
||||
testDevice->SetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 1234,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ nullptr,
|
||||
/* inDataSize = */ dataSize,
|
||||
/* inData = */ reinterpret_cast<const void* __nonnull>(bundleID));
|
||||
};
|
||||
|
||||
// Should be set to the empty string by default.
|
||||
@@ -103,11 +182,12 @@
|
||||
// Arguments should be null-checked.
|
||||
BGMShouldThrow<std::runtime_error>(self, [&](){
|
||||
UInt32 outDataSize;
|
||||
device.GetPropertyData(kObjectID_Device, 0, kBGMMusicPlayerBundleIDAddress, 0, NULL, sizeof(CFStringRef),
|
||||
outDataSize, /* outData = */ reinterpret_cast<void* __nonnull>(NULL));
|
||||
testDevice->GetPropertyData(kObjectID_Device, 0, kBGMMusicPlayerBundleIDAddress, 0, nullptr,
|
||||
sizeof(CFStringRef), outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(NULL));
|
||||
});
|
||||
BGMShouldThrow<std::runtime_error>(self, [&](){
|
||||
setBundleID(NULL);
|
||||
setBundleID(nullptr);
|
||||
});
|
||||
|
||||
// Invalid data should be rejected.
|
||||
@@ -115,7 +195,7 @@
|
||||
setBundleID((CFStringRef*)&kCFNull);
|
||||
});
|
||||
BGMShouldThrow<CAException>(self, [&](){
|
||||
CFStringRef nullRef = NULL;
|
||||
CFStringRef nullRef = nullptr;
|
||||
setBundleID(&nullRef);
|
||||
});
|
||||
BGMShouldThrow<CAException>(self, [&](){
|
||||
|
||||
@@ -156,15 +156,21 @@ public:
|
||||
void GetItemByIndex(UInt32 inIndex, AudioObjectPropertyAddress& outAddress) const { if(inIndex < mAddressList.size()) { outAddress = mAddressList.at(inIndex); } }
|
||||
const AudioObjectPropertyAddress* GetItems() const { return &(*mAddressList.begin()); }
|
||||
AudioObjectPropertyAddress* GetItems() { return &(*mAddressList.begin()); }
|
||||
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
bool HasItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::CongruentEqualTo(), inAddress)); return theIterator != mAddressList.end(); }
|
||||
bool HasExactItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); return theIterator != mAddressList.end(); }
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
void AppendItem(const AudioObjectPropertyAddress& inAddress) { mAddressList.push_back(inAddress); }
|
||||
void AppendUniqueItem(const AudioObjectPropertyAddress& inAddress) { if(!HasItem(inAddress)) { mAddressList.push_back(inAddress); } }
|
||||
void AppendUniqueExactItem(const AudioObjectPropertyAddress& inAddress) { if(!HasExactItem(inAddress)) { mAddressList.push_back(inAddress); } }
|
||||
void InsertItemAtIndex(UInt32 inIndex, const AudioObjectPropertyAddress& inAddress) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.insert(theIterator, inAddress); } else { mAddressList.push_back(inAddress); } }
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
void EraseExactItem(const AudioObjectPropertyAddress& inAddress) { AddressList::iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); if(theIterator != mAddressList.end()) { mAddressList.erase(theIterator); } }
|
||||
#pragma clang diagnostic pop
|
||||
void EraseItemAtIndex(UInt32 inIndex) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.erase(theIterator); } }
|
||||
void EraseAllItems() { mAddressList.clear(); }
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 192 KiB |
@@ -0,0 +1,69 @@
|
||||
% 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/>.
|
||||
|
||||
%
|
||||
% VolumeIcons.tex
|
||||
%
|
||||
% Build with XeTeX:
|
||||
% xelatex -jobname=Volume0 '\def\UseOption{}\input{VolumeIcons.tex}'
|
||||
% xelatex -jobname=Volume1 '\def\UseOption{w1}\input{VolumeIcons.tex}'
|
||||
% xelatex -jobname=Volume2 '\def\UseOption{w1,w2}\input{VolumeIcons.tex}'
|
||||
% xelatex -jobname=Volume3 '\def\UseOption{w1,w2,w3}\input{VolumeIcons.tex}'
|
||||
% for n in 0 1 2 3; do mv Volume$n.pdf ../BGMApp/BGMApp/Images.xcassets/Volume$n.imageset/; done
|
||||
%
|
||||
% Might build correctly with regular LaTeX. I haven't tried it.
|
||||
%
|
||||
|
||||
\documentclass[tikz]{standalone}
|
||||
\usepackage{tikz}
|
||||
% "dummyOption" prevents "Package optional Warning: No options were selected,
|
||||
% so all optional text will be printed" when building Volume0.pdf.
|
||||
\usepackage[dummyOption]{optional}
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
|
||||
% Speaker (Rounded box and triangle)
|
||||
\fill[rounded corners=5mm]
|
||||
(0mm, 62.5mm) rectangle (25mm, 37.5mm) {};
|
||||
\draw[rounded corners=2.5mm,fill=black]
|
||||
(3mm, 50mm)--(34mm, 76.5mm)--(34mm, 23.5mm)--cycle;
|
||||
|
||||
% First sound wave (Curved line)
|
||||
\opt{w1}{
|
||||
\draw[line width=4.3mm,line cap=round]
|
||||
(44mm, 36.5mm) to[out=46,in=-46] (44mm, 63.5mm);
|
||||
}
|
||||
|
||||
% Second sound wave (Curved line)
|
||||
\opt{w2}{
|
||||
\draw[line width=4.3mm,line cap=round]
|
||||
(57.5mm, 27.5mm) to[out=46,in=-46] (57.5mm, 72.5mm);
|
||||
}
|
||||
|
||||
% Third sound wave (Curved line)
|
||||
\opt{w3}{
|
||||
\draw[line width=4.3mm,line cap=round]
|
||||
(72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm);
|
||||
}
|
||||
|
||||
% Always draw a transparent copy of the third wave so the images will all have
|
||||
% the same width.
|
||||
\draw[line width=4.3mm,line cap=round,opacity=0]
|
||||
(72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm);
|
||||
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Background Music
|
||||
##### macOS audio utility
|
||||
|
||||

|
||||
<img src="Images/README/Screenshot.png" width="340" height="342" />
|
||||
|
||||
- Automatically pauses your music player when other audio starts playing and unpauses it afterwards
|
||||
- Per-application volume, boost quiet apps
|
||||
@@ -15,22 +15,37 @@
|
||||
|
||||
## Download
|
||||
|
||||
### Version 0.1.1
|
||||
### Version 0.2.0
|
||||
|
||||
<a href="https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.1.1/BackgroundMusic-0.1.1.pkg"><img
|
||||
<a href="https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.2.0/BackgroundMusic-0.2.0.pkg"><img
|
||||
src="Images/README/pkg-icon.png" width="32" height="32" align="absmiddle" />
|
||||
BackgroundMusic-0.1.1.pkg</a> (473 KB)
|
||||
BackgroundMusic-0.2.0.pkg</a> (581 KB)
|
||||
|
||||
Still very much in alpha. Not code signed, so you'll have to **right-click it and choose "Open"**.
|
||||
|
||||
**Requires OS X 10.10+**. Should work on 10.9, but I haven't tried it.
|
||||
**Requires macOS 10.10+**. Should work on 10.9, but I haven't tried it.
|
||||
|
||||
> <sub>MD5: e02988e6b32eafa88b99c4da33e7fe56</sub><br/>
|
||||
> <sub>SHA256: 7ce875bb00fdeb2b5b363aa92367b3fa096d18cb02a02c461d5df66307ab1088</sub><br/>
|
||||
> <sub>MD5: 23653c889f9ba7efc9cbb4e550c54060</sub><br/>
|
||||
> <sub>SHA256: 78184ba743ba79e8cbd81bc8e2ea28b59100cc626ea77be58d6612bcfe2c7b78</sub><br/>
|
||||
> <sub>PGP:
|
||||
> [sig](https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.1.1/BackgroundMusic-0.1.1.pkg.asc),
|
||||
> [sig](https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.2.0/BackgroundMusic-0.2.0.pkg.asc),
|
||||
> [key (0595DF814E41A6F69334C5E2CAA8D9B8E39EC18C)](https://bearisdriving.com/kyle-neideck.gpg)</sub>
|
||||
|
||||
We also have [snapshot builds](https://github.com/kyleneideck/BackgroundMusic/releases).
|
||||
|
||||
### Or install using [Homebrew](https://brew.sh/)
|
||||
|
||||
```bash
|
||||
brew cask install background-music
|
||||
```
|
||||
|
||||
If you want the snapshot version:
|
||||
|
||||
```bash
|
||||
brew tap homebrew/cask-versions
|
||||
brew cask install background-music-pre
|
||||
```
|
||||
|
||||
## Auto-pause music
|
||||
|
||||
Background Music can pause your music player app when other audio starts playing and unpause it afterwards. The idea is
|
||||
@@ -38,10 +53,12 @@ that when I'm listening to music and pause it to watch a video or something I al
|
||||
this keeps me from wearing headphones for hours listening to nothing.
|
||||
|
||||
So far iTunes, [Spotify](https://www.spotify.com), [VLC](https://www.videolan.org/vlc/),
|
||||
[VOX](https://coppertino.com/vox/mac), [Decibel](https://sbooth.org/Decibel/) and [Hermes](http://hermesapp.org/) are
|
||||
supported. Adding support for a new music player should only take a few minutes<sup id="a1">[1](#f1)</sup> -- see
|
||||
[VOX](https://vox.rocks/mac-music-player), [Decibel](https://sbooth.org/Decibel/), [Hermes](http://hermesapp.org/),
|
||||
[Swinsian](https://swinsian.com/) and [GPMDP](https://www.googleplaymusicdesktopplayer.com/) are supported. Adding
|
||||
support for a new music player should only take a few minutes<sup id="a1">[1](#f1)</sup> -- see
|
||||
[BGMMusicPlayer.h](BGMApp/BGMApp/Music%20Players/BGMMusicPlayer.h). If you don't know how to program, or just don't feel
|
||||
like it, create an issue and I'll try to add it for you.
|
||||
like it, feel free to [create an issue](https://github.com/kyleneideck/BackgroundMusic/issues/new).
|
||||
|
||||
|
||||
## App volumes
|
||||
|
||||
@@ -85,7 +102,7 @@ Otherwise, to build and install from source:
|
||||
- If the project is in a zip, unzip it.
|
||||
- Open `Terminal.app` and [change directory](https://github.com/0nn0/terminal-mac-cheatsheet#core-commands) to the
|
||||
directory containing the project.
|
||||
- Run the following the command: `/bin/bash build_and_install.sh`.
|
||||
- Run the following command: `/bin/bash build_and_install.sh`.
|
||||
|
||||
The script restarts the system audio process (coreaudiod) at the end of the installation, so you might want to pause any
|
||||
apps playing audio.
|
||||
@@ -117,6 +134,8 @@ Failing that, you might have to uninstall. Consider filing a bug report if you d
|
||||
|
||||
## Known issues
|
||||
|
||||
- Setting an app's volume above 50% can cause [clipping](https://en.wikipedia.org/wiki/Clipping_(audio)). Currently, the
|
||||
best solution is to instead set your overall volume to max and lower the volumes of other apps.
|
||||
- VLC automatically pauses iTunes/Spotify when it starts playing something, but that stops Background Music from
|
||||
unpausing your music afterwards. To workaround it, open VLC's preferences, click `Show All`, go `Interface` > `Main
|
||||
interfaces` > `macosx` and change `Control external music players` to either `Do nothing` or `Pause and resume
|
||||
@@ -169,7 +188,7 @@ Failing that, you might have to uninstall. Consider filing a bug report if you d
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2016, 2017 [Background Music contributors](https://github.com/kyleneideck/BackgroundMusic/graphs/contributors).
|
||||
Copyright © 2016-2019 [Background Music contributors](https://github.com/kyleneideck/BackgroundMusic/graphs/contributors).
|
||||
Licensed under [GPLv2](https://www.gnu.org/licenses/gpl-2.0.html), or any later version.
|
||||
|
||||
Background Music includes code from:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGM_Types.h
|
||||
// SharedSource
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016, 2017, 2019 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef SharedSource__BGM_Types
|
||||
@@ -77,7 +77,7 @@ enum
|
||||
|
||||
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
|
||||
// master element."
|
||||
static const AudioObjectPropertyElement kMasterChannel = 0;
|
||||
static const AudioObjectPropertyElement kMasterChannel = kAudioObjectPropertyElementMaster;
|
||||
|
||||
#pragma BGM Plug-in Custom Properties
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGM_Utils.h
|
||||
// SharedSource
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef SharedSource__BGM_Utils
|
||||
@@ -60,6 +60,11 @@
|
||||
__FUNCTION__, \
|
||||
expressionStr);
|
||||
|
||||
// Used to give the first 3 arguments of BGM_Utils::LogAndSwallowExceptions and
|
||||
// BGM_Utils::LogUnexpectedExceptions (and probably others in future). Mainly so we can call those
|
||||
// functions directly instead of using the macro wrappers.
|
||||
#define BGMDbgArgs __FILE__, __LINE__, __FUNCTION__
|
||||
|
||||
#pragma mark Objective-C Macros
|
||||
|
||||
#if defined(__OBJC__)
|
||||
@@ -81,7 +86,7 @@
|
||||
__typeof((expression)) value = (expression); \
|
||||
BGMAssertNonNull2(value, #expression); \
|
||||
BGMNonNullCastHelper<__typeof((expression))>* helper; \
|
||||
(__typeof(helper.asNonNull))value; \
|
||||
(__typeof(helper.asNonNull) __nonnull)value; \
|
||||
})
|
||||
|
||||
#else /* __has_feature(objc_generics) */
|
||||
@@ -144,7 +149,7 @@ namespace BGM_Utils
|
||||
template <typename T>
|
||||
inline T __nonnull NN(T __nullable v) {
|
||||
BGMAssertNonNull(v);
|
||||
return v;
|
||||
return static_cast<T __nonnull>(v);
|
||||
}
|
||||
|
||||
// Log (and swallow) errors returned by Mach functions. Returns false if there was an error.
|
||||
|
||||
+55
-27
@@ -19,7 +19,7 @@
|
||||
#
|
||||
# build_and_install.sh
|
||||
#
|
||||
# Copyright © 2016, 2017 Kyle Neideck
|
||||
# Copyright © 2016-2018 Kyle Neideck
|
||||
# Copyright © 2016 Nick Jacques
|
||||
#
|
||||
# Builds and installs BGMApp, BGMDriver and BGMXPCHelper. Requires xcodebuild and Xcode.
|
||||
@@ -40,7 +40,7 @@ set -o errtrace
|
||||
cd "$( dirname "${BASH_SOURCE[0]}" )"
|
||||
|
||||
error_handler() {
|
||||
LAST_COMMAND="${BASH_COMMAND}" LAST_COMMAND_EXIT_STATUS=$?
|
||||
LAST_COMMAND="$3" LAST_COMMAND_EXIT_STATUS="$2"
|
||||
|
||||
# Log the error.
|
||||
echo "Failure in $0 at line $1. The last command was (probably)" >> ${LOG_FILE}
|
||||
@@ -86,7 +86,7 @@ enable_error_handling() {
|
||||
# TODO: The version of Bash that ships with OSX only gives you the line number of the
|
||||
# function the error occurred in -- not the line the error occurred on. There are a
|
||||
# few solutions suggested on various websites, but none of them work.
|
||||
trap 'error_handler ${LINENO}' ERR
|
||||
trap 'error_handler ${LINENO} $? "${BASH_COMMAND}"' ERR
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ if ! [[ -x "${XCODEBUILD}" ]]; then
|
||||
fi
|
||||
# This check is last because it takes 10 seconds or so if it fails.
|
||||
if ! [[ -x "${XCODEBUILD}" ]]; then
|
||||
XCODEBUILD=$(/usr/bin/xcrun --find xcodebuild &2>>${LOG_FILE} || true)
|
||||
XCODEBUILD="$(/usr/bin/xcrun --find xcodebuild 2>>${LOG_FILE} || true)"
|
||||
fi
|
||||
|
||||
RECOMMENDED_MIN_XCODE_VERSION=8
|
||||
@@ -219,12 +219,11 @@ show_spinner() {
|
||||
# (wait returns 127 if the process has already exited.)
|
||||
if [[ ${EXIT_STATUS} -ne 0 ]] && [[ ${EXIT_STATUS} -ne 127 ]]; then
|
||||
ERROR_MSG="$1"
|
||||
if [[ ${DID_TIMEOUT} -eq 0 ]]; then
|
||||
ERROR_MSG+="\n\nFailed command:
|
||||
${PREV_COMMAND_STRING}"
|
||||
if [[ ${DID_TIMEOUT} -ne 0 ]]; then
|
||||
ERROR_MSG+="\n\nCommand timed out after ${TIMEOUT} seconds."
|
||||
fi
|
||||
|
||||
error_handler ${LINENO}
|
||||
error_handler ${LINENO} ${EXIT_STATUS} "${PREV_COMMAND_STRING}"
|
||||
|
||||
if [[ ${CONTINUE_ON_ERROR} -eq 0 ]]; then
|
||||
exit ${EXIT_STATUS}
|
||||
@@ -246,8 +245,11 @@ parse_options() {
|
||||
CONFIGURATION="Debug"
|
||||
;;
|
||||
b)
|
||||
# Just build; don't install.
|
||||
XCODEBUILD_ACTION="build"
|
||||
# The dirs xcodebuild will build in.
|
||||
# TODO: If these dirs were created by running this script without -b, they'll be
|
||||
# owned by root and xcodebuild will fail.
|
||||
APP_PATH="./BGMApp/build"
|
||||
DRIVER_PATH="./BGMDriver/build"
|
||||
;;
|
||||
@@ -524,6 +526,19 @@ log_debug_info() {
|
||||
LOG_DEBUG_INFO_TASK_PID=$!
|
||||
}
|
||||
|
||||
# Cleans the build products and intermediate files for a build scheme.
|
||||
#
|
||||
# Params:
|
||||
# - The Xcode build scheme to clean, e.g. "Background Music Device".
|
||||
clean() {
|
||||
if [[ "${CLEAN}" != "" ]]; then
|
||||
${SUDO} "${XCODEBUILD}" -scheme "$1" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
BUILD_DIR=./build \
|
||||
${CLEAN} >> ${LOG_FILE} 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Register our handler so we can print a message and clean up if there's an error.
|
||||
enable_error_handling
|
||||
|
||||
@@ -607,35 +622,48 @@ if [[ "${XCODEBUILD_ACTION}" == "install" ]]; then
|
||||
SUDO="sudo"
|
||||
ACTIONING="Installing"
|
||||
else
|
||||
# No need to sudo if we're only building.
|
||||
SUDO=""
|
||||
ACTIONING="Building"
|
||||
fi
|
||||
|
||||
# Enable AddressSanitizer in debug builds to catch memory bugs. Allow ENABLE_ASAN to be set as an
|
||||
# environment variable by only setting it here if it isn't already set. (Used by package.sh.)
|
||||
if [[ "${CONFIGURATION}" == "Debug" ]]; then
|
||||
ENABLE_ASAN=YES
|
||||
ENABLE_ASAN="${ENABLE_ASAN:-YES}"
|
||||
else
|
||||
ENABLE_ASAN=NO
|
||||
ENABLE_ASAN="${ENABLE_ASAN:-NO}"
|
||||
fi
|
||||
|
||||
# Clean all projects. Done separately to workaround what I think is a bug in Xcode 10.0. If you just
|
||||
# add "clean" to the other xcodebuild commands, they seem to fail because of the DSTROOT="/" arg.
|
||||
if [[ "${CLEAN}" != "" ]]; then
|
||||
# Disable the -e shell option and error trap for build commands so we can handle errors
|
||||
# differently.
|
||||
(disable_error_handling
|
||||
clean "Background Music Device"
|
||||
clean "PublicUtility"
|
||||
clean "BGMXPCHelper"
|
||||
clean "Background Music"
|
||||
# Also delete the build dirs as files/dirs left in them can make the install step fail and,
|
||||
# if you're using Xcode 10, the commands above will have cleaned the DerivedData dir but not
|
||||
# the build dirs. I think this is a separate Xcode bug. See
|
||||
# <http://www.openradar.me/40906897>.
|
||||
${SUDO} /bin/rm -rf BGMDriver/build BGMApp/build >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
echo "Cleaning"
|
||||
show_spinner "Clean command failed. Try deleting the directories BGMDriver/build and \
|
||||
BGMApp/build manually and running '$0 -n' to skip the cleaning step."
|
||||
fi
|
||||
|
||||
# BGMDriver
|
||||
|
||||
echo "[1/3] ${ACTIONING} the virtual audio device $(bold_face ${DRIVER_DIR}) to" \
|
||||
"$(bold_face ${DRIVER_PATH})" \
|
||||
"$(bold_face ${DRIVER_PATH})" \
|
||||
| tee -a ${LOG_FILE}
|
||||
|
||||
# Disable the -e shell option and error trap for build commands so we can handle errors differently.
|
||||
(disable_error_handling
|
||||
# Build Apple's PublicUtility classes as a static library.
|
||||
${SUDO} "${XCODEBUILD}" -scheme "PublicUtility" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
-enableAddressSanitizer ${ENABLE_ASAN} \
|
||||
BUILD_DIR=./build \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} build >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
(disable_error_handling
|
||||
# Build and install BGMDriver
|
||||
# Build and install BGMDriver.
|
||||
${SUDO} "${XCODEBUILD}" -scheme "Background Music Device" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
-enableAddressSanitizer ${ENABLE_ASAN} \
|
||||
@@ -643,14 +671,14 @@ echo "[1/3] ${ACTIONING} the virtual audio device $(bold_face ${DRIVER_DIR}) to"
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
DSTROOT="/" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
# BGMXPCHelper
|
||||
|
||||
echo "[2/3] ${ACTIONING} $(bold_face ${XPC_HELPER_DIR}) to $(bold_face ${XPC_HELPER_PATH})" \
|
||||
| tee -a ${LOG_FILE}
|
||||
| tee -a ${LOG_FILE}
|
||||
|
||||
(disable_error_handling
|
||||
${SUDO} "${XCODEBUILD}" -scheme BGMXPCHelper \
|
||||
@@ -661,7 +689,7 @@ echo "[2/3] ${ACTIONING} $(bold_face ${XPC_HELPER_DIR}) to $(bold_face ${XPC_HEL
|
||||
DSTROOT="/" \
|
||||
INSTALL_PATH="${XPC_HELPER_PATH}" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
@@ -678,7 +706,7 @@ echo "[3/3] ${ACTIONING} $(bold_face ${APP_DIR}) to $(bold_face ${APP_PATH})" \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
DSTROOT="/" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
|
||||
+71
-20
@@ -19,15 +19,16 @@
|
||||
#
|
||||
# package.sh
|
||||
#
|
||||
# Copyright © 2017 Kyle Neideck
|
||||
# Copyright © 2017, 2018 Kyle Neideck
|
||||
# Copyright © 2016, 2017 Takayama Fumihiko
|
||||
#
|
||||
# Build Background Music and package it into a .pkg file and a .zip of the debug symbols (dSYM).
|
||||
# Call this script with -d to use the debug build configuration.
|
||||
#
|
||||
# Based on https://github.com/tekezo/Karabiner-Elements/blob/master/make-package.sh
|
||||
#
|
||||
|
||||
# TODO: Code signing. See `man pkgbuild`.
|
||||
# TODO: Code signing. See `man productbuild`.
|
||||
|
||||
PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin"; export PATH
|
||||
|
||||
@@ -46,26 +47,63 @@ set_permissions() {
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
# Build
|
||||
bash build_and_install.sh -b
|
||||
# Use the release configuration by default.
|
||||
debug_build=NO
|
||||
build_output_path="build/Release"
|
||||
|
||||
# Handle the options passed to this script.
|
||||
while getopts ":d" opt; do
|
||||
case $opt in
|
||||
d)
|
||||
debug_build=YES
|
||||
build_output_path="build/Debug"
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build
|
||||
if [[ $debug_build == YES ]]; then
|
||||
# Disable AddressSanitizer so we can distribute debug packages to users reporting bugs without
|
||||
# worrying about loading the AddressSanitizer dylib in coreaudiod.
|
||||
#
|
||||
# TODO: Would debug packages be more useful if they were built with optimization (i.e. using the
|
||||
# DebugOpt configuration instead of Debug)?
|
||||
ENABLE_ASAN=NO bash build_and_install.sh -b -d
|
||||
build_status=$?
|
||||
else
|
||||
bash build_and_install.sh -b
|
||||
build_status=$?
|
||||
fi
|
||||
|
||||
# Exit if the build failed.
|
||||
if [[ $build_status -ne 0 ]]; then
|
||||
exit $build_status
|
||||
fi
|
||||
|
||||
# Read the version string from the build.
|
||||
version="$(/usr/libexec/PlistBuddy \
|
||||
-c "Print CFBundleShortVersionString" \
|
||||
"BGMApp/build/Release/Background Music.app/Contents/Info.plist")"
|
||||
"BGMApp/${build_output_path}/Background Music.app/Contents/Info.plist")"
|
||||
|
||||
# Everything in out_dir at the end of this script will be released in the Travis CI builds.
|
||||
out_dir="Background-Music-$version"
|
||||
rm -rf "$out_dir"
|
||||
mkdir "$out_dir"
|
||||
|
||||
# Separate the debug symbols and the .app
|
||||
echo "Archiving debug symbols"
|
||||
if [[ $debug_build == NO ]]; then
|
||||
# Separate the debug symbols and the .app bundle.
|
||||
echo "Archiving debug symbols"
|
||||
|
||||
dsym_archive="$out_dir/Background Music.dSYM-$version.zip"
|
||||
mv "BGMApp/build/Release/Background Music.app/Contents/MacOS/Background Music.dSYM" \
|
||||
"Background Music.dSYM"
|
||||
zip -r "$dsym_archive" "Background Music.dSYM"
|
||||
rm -r "Background Music.dSYM"
|
||||
dsym_archive="$out_dir/Background Music.dSYM-$version.zip"
|
||||
mv "BGMApp/${build_output_path}/Background Music.app/Contents/MacOS/Background Music.dSYM" \
|
||||
"Background Music.dSYM"
|
||||
zip -r "$dsym_archive" "Background Music.dSYM"
|
||||
rm -r "Background Music.dSYM"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -75,10 +113,11 @@ rm -rf "pkgroot"
|
||||
mkdir -p "pkgroot"
|
||||
|
||||
mkdir -p "pkgroot/Library/Audio/Plug-Ins/HAL"
|
||||
cp -R "BGMDriver/build/Release/Background Music Device.driver" "pkgroot/Library/Audio/Plug-Ins/HAL/"
|
||||
cp -R "BGMDriver/${build_output_path}/Background Music Device.driver" \
|
||||
"pkgroot/Library/Audio/Plug-Ins/HAL/"
|
||||
|
||||
mkdir -p "pkgroot/Applications"
|
||||
cp -R "BGMApp/build/Release/Background Music.app" "pkgroot/Applications"
|
||||
cp -R "BGMApp/${build_output_path}/Background Music.app" "pkgroot/Applications"
|
||||
|
||||
scripts_dir="$(mktemp -d)"
|
||||
cp "pkg/preinstall" "$scripts_dir"
|
||||
@@ -86,7 +125,7 @@ cp "pkg/postinstall" "$scripts_dir"
|
||||
cp "BGMApp/BGMXPCHelper/com.bearisdriving.BGM.XPCHelper.plist.template" "$scripts_dir"
|
||||
cp "BGMApp/BGMXPCHelper/safe_install_dir.sh" "$scripts_dir"
|
||||
cp "BGMApp/BGMXPCHelper/post_install.sh" "$scripts_dir"
|
||||
cp -R "BGMApp/build/Release/BGMXPCHelper.xpc" "$scripts_dir"
|
||||
cp -R "BGMApp/${build_output_path}/BGMXPCHelper.xpc" "$scripts_dir"
|
||||
|
||||
set_permissions "pkgroot"
|
||||
chmod 755 "pkgroot/Applications/Background Music.app/Contents/MacOS/Background Music"
|
||||
@@ -107,7 +146,11 @@ sed "s/{{VERSION}}/$version/g" "pkg/Distribution.xml.template" > "pkg/Distributi
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
pkg="$out_dir/BackgroundMusic-$version.pkg"
|
||||
# As a security check for releases, we manually build the same package locally, compare it to the
|
||||
# release built by Travis and then code sign it. (And then remove the code signature on a different
|
||||
# computer and check that the signed package still matches the one from Travis.) So we include
|
||||
# "unsigned" in the name to differentiate the two versions.
|
||||
pkg="$out_dir/BackgroundMusic-$version.unsigned.pkg"
|
||||
pkg_identifier="com.bearisdriving.BGM"
|
||||
|
||||
echo "Creating $pkg"
|
||||
@@ -123,6 +166,7 @@ pkgbuild \
|
||||
|
||||
productbuild \
|
||||
--distribution "pkg/Distribution.xml" \
|
||||
--identifier "$pkg_identifier" \
|
||||
--resources "pkgres" \
|
||||
--package-path "$out_dir" \
|
||||
"$pkg"
|
||||
@@ -133,9 +177,16 @@ rm -rf "pkgres"
|
||||
rm -f "pkg/Distribution.xml"
|
||||
|
||||
# Print checksums
|
||||
echo "MD5 checksums:"
|
||||
md5 {"$pkg","$dsym_archive"}
|
||||
echo "SHA256 checksums:"
|
||||
shasum -a 256 {"$pkg","$dsym_archive"}
|
||||
if [[ $debug_build == YES ]]; then
|
||||
echo "MD5 checksum:"
|
||||
md5 "$pkg"
|
||||
echo "SHA256 checksum:"
|
||||
shasum -a 256 "$pkg"
|
||||
else
|
||||
echo "MD5 checksums:"
|
||||
md5 {"$pkg","$dsym_archive"}
|
||||
echo "SHA256 checksums:"
|
||||
shasum -a 256 {"$pkg","$dsym_archive"}
|
||||
fi
|
||||
|
||||
|
||||
|
||||
+15
-3
@@ -17,13 +17,14 @@
|
||||
# along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# preinstall
|
||||
# postinstall
|
||||
#
|
||||
# Copyright © 2017 Kyle Neideck
|
||||
# Copyright © 2017, 2018 Kyle Neideck
|
||||
#
|
||||
|
||||
PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH
|
||||
|
||||
coreaudiod_plist="/System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist"
|
||||
dest_volume="$3"
|
||||
xpc_helper_path="$(bash safe_install_dir.sh -y)"
|
||||
|
||||
@@ -34,6 +35,8 @@ cp -Rf "BGMXPCHelper.xpc" "$xpc_helper_path"
|
||||
bash "post_install.sh" "$xpc_helper_path" "BGMXPCHelper.xpc/Contents/MacOS/BGMXPCHelper" "."
|
||||
|
||||
# TODO: Verify the installed files, their permissions, the _BGMXPCHelper user/group, etc.
|
||||
# TODO: Instead of just sleeping for 5 seconds, wait until coreaudiod is restarted and BGMDevice is
|
||||
# ready to use.
|
||||
|
||||
# The extra or-clauses are fallback versions of the command that restarts coreaudiod. Apparently
|
||||
# some of these commands don't work with older versions of launchctl, so I figure there's no
|
||||
@@ -47,9 +50,18 @@ bash "post_install.sh" "$xpc_helper_path" "BGMXPCHelper.xpc/Contents/MacOS/BGMXP
|
||||
killall coreaudiod &>/dev/null) && \
|
||||
sleep 5
|
||||
|
||||
open "${dest_volume}/Applications/Background Music.app"
|
||||
# Try opening BGMApp using its bundle ID first so the installer can declare it as "relocatable".
|
||||
# That way, if the user moves BGMApp and then installs a newer version of Background Music, the
|
||||
# installer will try to find the old version of BGMApp and put the new one in the same place.
|
||||
#
|
||||
# If we can't open BGMApp, it very likely didn't install properly, so we fail the install.
|
||||
open -b com.bearisdriving.BGM.App || \
|
||||
open "${dest_volume}/Applications/Background Music.app" || \
|
||||
exit 1
|
||||
|
||||
# The installer plays a sound when it finishes, so give BGMApp a second to launch.
|
||||
sleep 1
|
||||
|
||||
exit 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user