Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -7,6 +7,8 @@ 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
|
||||
|
||||
|
||||
+20
-5
@@ -2,10 +2,18 @@ language: objective-c
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode10
|
||||
osx_image: xcode11
|
||||
xcode_sdk: macosx10.14
|
||||
sudo: required
|
||||
env: DEPLOY=true
|
||||
- os: osx
|
||||
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
|
||||
@@ -18,10 +26,11 @@ matrix:
|
||||
osx_image: xcode9.2
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
xcode_sdk: macosx10.12
|
||||
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
|
||||
@@ -41,7 +50,10 @@ script:
|
||||
- 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
|
||||
@@ -60,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.
|
||||
|
||||
@@ -7,65 +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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; };
|
||||
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 */; };
|
||||
@@ -73,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 */; };
|
||||
@@ -106,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 */; };
|
||||
@@ -115,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 */; };
|
||||
@@ -172,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 */
|
||||
@@ -204,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>"; };
|
||||
@@ -253,21 +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; };
|
||||
@@ -296,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>"; };
|
||||
@@ -362,6 +403,7 @@
|
||||
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 */,
|
||||
@@ -415,8 +457,6 @@
|
||||
27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */,
|
||||
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */,
|
||||
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */,
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
||||
);
|
||||
name = "Preferences Menu";
|
||||
sourceTree = "<group>";
|
||||
@@ -498,8 +538,14 @@
|
||||
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 */,
|
||||
@@ -518,6 +564,7 @@
|
||||
1CB8B32D1BBA75EF000E2DD1 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C43DABE22F582780004AF35 /* Background Music.entitlements */,
|
||||
1CB8B3381BBA75EF000E2DD1 /* BGMApp */,
|
||||
1CB8B34C1BBA75F0000E2DD1 /* BGMApp Tests */,
|
||||
27379B901C7F57DB0084A24C /* BGMXPCHelper */,
|
||||
@@ -547,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 */,
|
||||
@@ -557,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 */,
|
||||
@@ -567,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 */,
|
||||
@@ -672,6 +729,8 @@
|
||||
27F7D4911D2484A300821C4B /* Decibel.h */,
|
||||
279F48781DD6D94000768A85 /* Hermes.h */,
|
||||
27379B851C7C54870084A24C /* iTunes.h */,
|
||||
1CDE224022CBB95B0008E3AC /* Music.h */,
|
||||
1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */,
|
||||
27379B861C7C54870084A24C /* Spotify.h */,
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */,
|
||||
27379B871C7C552A0084A24C /* VLC.h */,
|
||||
@@ -697,6 +756,7 @@
|
||||
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C8034D420B0347A004BC50C /* Security.framework */,
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */,
|
||||
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
|
||||
@@ -851,6 +911,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@@ -875,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 */,
|
||||
@@ -902,6 +964,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -939,7 +1002,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
shellPath = /bin/bash;
|
||||
shellScript = "/bin/bash $SRCROOT/BGMXPCHelper/post_install.sh\n";
|
||||
shellScript = "/bin/bash \"$SRCROOT/BGMXPCHelper/post_install.sh\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -953,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 */,
|
||||
@@ -982,9 +1047,11 @@
|
||||
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 */,
|
||||
@@ -997,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;
|
||||
};
|
||||
@@ -1004,10 +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 */,
|
||||
@@ -1015,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 */,
|
||||
@@ -1023,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 */,
|
||||
@@ -1031,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 */,
|
||||
@@ -1053,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;
|
||||
};
|
||||
@@ -1088,10 +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 */,
|
||||
@@ -1126,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;
|
||||
};
|
||||
@@ -1204,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;
|
||||
@@ -1252,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;
|
||||
@@ -1306,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;
|
||||
@@ -1389,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;
|
||||
@@ -1433,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;
|
||||
@@ -1452,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",
|
||||
@@ -1519,6 +1615,7 @@
|
||||
27379B991C7F57DB0084A24C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"CoreAudio_Debug=1",
|
||||
@@ -1537,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;
|
||||
};
|
||||
@@ -1549,6 +1648,7 @@
|
||||
27379B9B1C7F57DB0084A24C /* DebugOpt */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEPLOYMENT_POSTPROCESSING = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"CoreAudio_Debug=1",
|
||||
|
||||
+128
-108
@@ -17,59 +17,64 @@
|
||||
// BGMAppDelegate.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 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 {
|
||||
[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) {
|
||||
@@ -78,72 +83,19 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
|
||||
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];
|
||||
|
||||
// NSStatusItem doesn't have the "button" property on OS X 10.9.
|
||||
BOOL buttonAvailable = (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10);
|
||||
|
||||
// Set the title/tooltip to "Background Music".
|
||||
statusBarItem.title = [NSRunningApplication currentApplication].localizedName;
|
||||
statusBarItem.toolTip = statusBarItem.title;
|
||||
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.accessibilityLabel = statusBarItem.title;
|
||||
#pragma clang diagnostic pop
|
||||
// Set up audioDevices, which coordinates BGMDevice and the output device. It manages
|
||||
// playthrough, volume/mute controls, etc.
|
||||
if (![self initAudioDeviceManager]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the icon.
|
||||
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
|
||||
// Stored user settings
|
||||
userDefaults = [self createUserDefaults];
|
||||
|
||||
if (icon != nil) {
|
||||
NSRect statusBarItemFrame;
|
||||
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItemFrame = statusBarItem.button.frame;
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
// OS X 10.9 fallback. I haven't tested this (or anything else on 10.9).
|
||||
statusBarItemFrame = statusBarItem.view.frame;
|
||||
}
|
||||
|
||||
CGFloat lengthMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding);
|
||||
[icon setSize:NSMakeSize(lengthMinusPadding, lengthMinusPadding)];
|
||||
|
||||
// Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or the status
|
||||
// bar's in dark mode
|
||||
[icon setTemplate:YES];
|
||||
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.image = icon;
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
statusBarItem.image = icon;
|
||||
}
|
||||
} else {
|
||||
// If our icon is missing for some reason, fallback to a fermata character (1D110)
|
||||
if (buttonAvailable) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
statusBarItem.button.title = @"𝄐";
|
||||
#pragma clang diagnostic pop
|
||||
} else {
|
||||
statusBarItem.title = @"𝄐";
|
||||
}
|
||||
}
|
||||
|
||||
// Set the main menu
|
||||
statusBarItem.menu = self.bgmMenu;
|
||||
// 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 {
|
||||
@@ -158,9 +110,13 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -171,15 +127,13 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
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];
|
||||
|
||||
[self setUpMainMenu:userDefaults];
|
||||
[self setUpMainMenu];
|
||||
|
||||
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
|
||||
helperConnectionErrorHandler:^(NSError* error) {
|
||||
@@ -192,11 +146,31 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
|
||||
// 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];
|
||||
audioDevices = [BGMAudioDeviceManager new];
|
||||
|
||||
if (!audioDevices) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:error.code];
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -253,7 +227,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults {
|
||||
- (void) setUpMainMenu {
|
||||
autoPauseMenuItem =
|
||||
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
autoPauseMusic:autoPauseMusic
|
||||
@@ -262,9 +236,18 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
|
||||
[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];
|
||||
|
||||
@@ -326,30 +309,48 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
|
||||
#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;
|
||||
@@ -373,6 +374,25 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||
}
|
||||
}
|
||||
|
||||
- (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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,16 +26,17 @@
|
||||
// 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 "CAAutoDisposer.h"
|
||||
#import "CAAtomic.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -60,41 +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 = 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());
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
LogError("BGMAudioDeviceManager::init: BGMDevice not found. (%d)", e.GetError());
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
@@ -116,90 +101,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item {
|
||||
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
|
||||
@@ -319,29 +228,9 @@
|
||||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
|
||||
|
||||
try {
|
||||
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();
|
||||
}
|
||||
[self setOutputDeviceWithIDImpl:newDeviceID
|
||||
dataSourceID:dataSourceID
|
||||
currentDeviceID:currentDeviceID];
|
||||
} catch (const CAException& e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
@@ -355,6 +244,7 @@
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
|
||||
// Tell other classes and BGMXPCHelper that we changed the output device.
|
||||
[self propagateOutputDeviceChange];
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
@@ -363,6 +253,41 @@
|
||||
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 {
|
||||
@@ -406,6 +331,7 @@
|
||||
|
||||
// Update the menu item for the volume of the output device.
|
||||
[outputVolumeMenuItem outputDeviceDidChange];
|
||||
[outputDeviceMenuSection outputDeviceDidChange];
|
||||
}
|
||||
|
||||
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)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 {
|
||||
|
||||
@@ -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, 2018 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"
|
||||
@@ -46,7 +47,7 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
NSTextField* deviceLabel;
|
||||
NSSlider* volumeSlider;
|
||||
BGMAudioDevice outputDevice;
|
||||
AudioObjectPropertyListenerBlock updateSliderListenerBlock;
|
||||
BGMVolumeChangeListener* volumeChangeListener;
|
||||
AudioObjectPropertyListenerBlock updateLabelListenerBlock;
|
||||
}
|
||||
|
||||
@@ -66,9 +67,8 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
volumeSlider = slider;
|
||||
outputDevice = audioDevices.outputDevice;
|
||||
|
||||
// These are initialised in the methods called below.
|
||||
updateSliderListenerBlock = nil;
|
||||
updateLabelListenerBlock = nil;
|
||||
// volumeChangeListener and updateLabelListenerBlock are initialised in the methods called
|
||||
// below.
|
||||
|
||||
// Apply our custom view from MainMenu.xib.
|
||||
self.view = view;
|
||||
@@ -84,27 +84,11 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
return self;
|
||||
}
|
||||
|
||||
// We currently only use one instance of this class and it's never deallocated, but it's probably
|
||||
// good practice to define dealloc anyway.
|
||||
- (void) dealloc {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
// Remove the audio property listeners.
|
||||
[self removeOutputDeviceDataSourceListener];
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
|
||||
audioDevices.bgmDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateSliderListenerBlock);
|
||||
}));
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] {
|
||||
audioDevices.bgmDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateSliderListenerBlock);
|
||||
}));
|
||||
});
|
||||
// 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 {
|
||||
@@ -120,32 +104,9 @@ 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.
|
||||
BGMOutputVolumeMenuItem* __weak weakSelf = self;
|
||||
|
||||
updateSliderListenerBlock =
|
||||
^(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 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(),
|
||||
updateSliderListenerBlock);
|
||||
}));
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] {
|
||||
// Register the same listener for mute/unmute notifications.
|
||||
audioDevices.bgmDevice.AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyMute, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateSliderListenerBlock);
|
||||
}));
|
||||
volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [=] {
|
||||
[weakSelf updateVolumeSlider];
|
||||
});
|
||||
}
|
||||
|
||||
// Updates the value of the output volume slider. Should only be called on the main thread because
|
||||
@@ -180,7 +141,7 @@ 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.
|
||||
@@ -212,10 +173,20 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
- (void) removeOutputDeviceDataSourceListener {
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::removeOutputDeviceDataSourceListener",
|
||||
([&] {
|
||||
outputDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateLabelListenerBlock);
|
||||
// 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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="14313.18" 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="14313.18"/>
|
||||
<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>
|
||||
@@ -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.0" id="FDH-7l-wFf">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -205,7 +215,7 @@
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
@@ -242,7 +252,7 @@
|
||||
<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-2018 Background Music contributors" placeholderString="" id="ctF-95-uVu">
|
||||
<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"/>
|
||||
@@ -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>
|
||||
@@ -316,56 +326,56 @@
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="FermataIcon" width="284" height="284"/>
|
||||
<image name="NSComputer" width="128" height="128"/>
|
||||
<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.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -33,11 +33,11 @@
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016-2018 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>
|
||||
<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>
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
+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, 2018 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,7 +83,7 @@
|
||||
// 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.
|
||||
@@ -101,9 +103,9 @@
|
||||
@property (readonly, getter=isPaused) BOOL paused;
|
||||
|
||||
// Called when the user selects this music player.
|
||||
- (void) onSelect;
|
||||
// Called when this is the selected music player and the user selects a different one.
|
||||
- (void) onDeselect;
|
||||
- (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.
|
||||
@@ -123,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;
|
||||
|
||||
@@ -131,15 +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) onSelect;
|
||||
- (void) onDeselect;
|
||||
- (void) wasSelected;
|
||||
- (void) wasDeselected;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
@synthesize musicPlayerID = _musicPlayerID;
|
||||
@synthesize name = _name;
|
||||
@synthesize toolTip = _toolTip;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
@synthesize selected = _selected;
|
||||
@@ -40,11 +41,27 @@
|
||||
- (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])) {
|
||||
@@ -55,6 +72,7 @@
|
||||
|
||||
_musicPlayerID = musicPlayerID;
|
||||
_name = name;
|
||||
_toolTip = toolTip;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
_selected = NO;
|
||||
@@ -72,7 +90,8 @@
|
||||
|
||||
#pragma mark BGMMusicPlayer default implementations
|
||||
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstances {
|
||||
+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
|
||||
#pragma unused (userDefaults)
|
||||
return @[ [self new] ];
|
||||
}
|
||||
|
||||
@@ -84,11 +103,11 @@
|
||||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
- (void) wasSelected {
|
||||
_selected = YES;
|
||||
}
|
||||
|
||||
- (void) onDeselect {
|
||||
- (void) wasDeselected {
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
@@ -34,6 +34,8 @@
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMHermes.h"
|
||||
#import "BGMSwinsian.h"
|
||||
#import "BGMMusic.h"
|
||||
#import "BGMGooglePlayMusicDesktopPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -47,16 +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],
|
||||
[BGMSwinsian class] ]
|
||||
musicPlayerClasses:mpClasses
|
||||
userDefaults:defaults];
|
||||
}
|
||||
|
||||
@@ -70,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];
|
||||
@@ -215,7 +229,7 @@
|
||||
}
|
||||
|
||||
// Tell the current music player (object) a different player has been selected.
|
||||
[_selectedMusicPlayer onDeselect];
|
||||
[_selectedMusicPlayer wasDeselected];
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
@@ -229,7 +243,7 @@
|
||||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
|
||||
// Tell the music player (object) it's been selected.
|
||||
[_selectedMusicPlayer onSelect];
|
||||
[_selectedMusicPlayer wasSelected];
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016-2019 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppWatcher.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
@@ -34,8 +35,7 @@
|
||||
|
||||
@implementation BGMScriptingBridge {
|
||||
id<BGMMusicPlayer> __weak _musicPlayer;
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id _didLaunchToken, _didTerminateToken;
|
||||
BGMAppWatcher* appWatcher;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
@@ -57,17 +57,14 @@
|
||||
BGMScriptingBridge* __weak weakSelf = self;
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
BGMScriptingBridge* strongSelf = weakSelf;
|
||||
strongSelf->_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
// TODO: I think the SBApplication will still keep a strong ref to this object, so we might
|
||||
// have to make a separate delegate object.
|
||||
// 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;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
return [[note.userInfo[NSWorkspaceApplicationKey] bundleIdentifier] isEqualToString:bundleID];
|
||||
};
|
||||
|
||||
// Add observers that create/destroy the SBApplication when the music player is launched/terminated. We
|
||||
// only create the SBApplication when the music player is open. If it isn't open, creating the
|
||||
// SBApplication or sending it events could launch the music player. Whether or not it does depends on
|
||||
@@ -76,31 +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();
|
||||
[weakSelf ensurePermission];
|
||||
}
|
||||
}];
|
||||
_didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note)
|
||||
{
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
strongSelf->_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) {
|
||||
@@ -108,19 +94,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (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.
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
return (SwinsianApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
- (void) wasSelected {
|
||||
[super wasSelected];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,13 +17,13 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.0</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;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -37,7 +37,7 @@
|
||||
</array>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016-2018 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2019 Background Music contributors</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
|
||||
@@ -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,24 +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
|
||||
@@ -40,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
|
||||
|
||||
@@ -87,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.
|
||||
@@ -119,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
|
||||
@@ -171,7 +188,7 @@ Failing that, you might have to uninstall. Consider filing a bug report if you d
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2016-2018 [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) */
|
||||
|
||||
@@ -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.
|
||||
|
||||
+5
-1
@@ -146,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"
|
||||
|
||||
Reference in New Issue
Block a user