Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0282706df | |||
| 94a5f37c2b | |||
| 797d2f14f5 | |||
| b3b4482bda | |||
| 9b33fffd23 | |||
| ac33909b51 | |||
| 1a49802675 | |||
| 75e8d5ceac | |||
| 08fdef6084 | |||
| 1e5d625d64 | |||
| ffa86bbcd9 | |||
| 5f31f54a85 | |||
| 2a41204fc0 | |||
| cb9cdb00b6 | |||
| ed06a257a8 | |||
| 7171cfcb78 | |||
| 3ba53a50ac | |||
| 944fc11212 | |||
| 287bae0923 | |||
| 6117bc285c | |||
| 18aa97f055 | |||
| ac2130c3c3 | |||
| e83d07f00b | |||
| b693a8af1e | |||
| b3b60559f2 | |||
| f64cf41f8a | |||
| bd90399ce3 | |||
| 425cb4af9d | |||
| 4c6de2f77f | |||
| ec81520379 | |||
| 1171bee102 | |||
| fb0740c4c1 | |||
| db63ae0cf1 | |||
| 59e70fb9d1 | |||
| 47ff99303a | |||
| 173914e343 | |||
| cce8111251 | |||
| df67e4fa2b | |||
| e4b98e4099 | |||
| 9fd5c89b27 | |||
| 3c001066c4 | |||
| 289f6b3d27 | |||
| 07a419fb34 | |||
| 283db29fb4 | |||
| 02558cd275 | |||
| e05acde351 | |||
| 09aacabefa | |||
| b715212cab | |||
| a6e9179f2d | |||
| ccac7d7001 | |||
| 243c798ccd | |||
| a4a9530513 | |||
| 9aec1aed34 | |||
| 7f784b5d94 | |||
| c907f13554 | |||
| 5a657a01a6 | |||
| aa8d9ae518 | |||
| 36b4b7e0c4 | |||
| 462bdee9cd | |||
| bf9faaef55 |
@@ -0,0 +1,15 @@
|
||||
# This file was added to get GitHub to display our source code correctly when
|
||||
# it has mixed tabs and spaces. (I didn't realise the sample code BGMDriver is
|
||||
# based on used tabs and it's too late to fix it now.)
|
||||
#
|
||||
# See http://editorconfig.org.
|
||||
|
||||
# This is the top-most .editorconfig file.
|
||||
root = true
|
||||
|
||||
# Set tabs to the width of 4 spaces in C, C++, Objective-C and Objective-C++
|
||||
# source files.
|
||||
[*.{h,c,cpp,m,mm}]
|
||||
tab_width = 4
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
tags
|
||||
cmake-build-debug/
|
||||
/Background-Music-*/
|
||||
BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
|
||||
# Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore
|
||||
|
||||
|
||||
+29
-12
@@ -2,26 +2,31 @@ language: objective-c
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
xcode_sdk: macosx10.12
|
||||
osx_image: xcode10
|
||||
xcode_sdk: macosx10.14
|
||||
sudo: required
|
||||
env: DEPLOY=true
|
||||
- os: osx
|
||||
osx_image: xcode8.2
|
||||
xcode_sdk: macosx10.12
|
||||
osx_image: xcode9.4
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8.1
|
||||
xcode_sdk: macosx10.12
|
||||
osx_image: xcode9.3
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8
|
||||
xcode_sdk: macosx10.11
|
||||
osx_image: xcode9.2
|
||||
xcode_sdk: macosx10.13
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
xcode_sdk: macosx10.12
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode7.3
|
||||
xcode_sdk: macosx10.11
|
||||
sudo: required
|
||||
env: PACKAGE=false
|
||||
# branches:
|
||||
# only:
|
||||
# - master
|
||||
@@ -31,7 +36,7 @@ install:
|
||||
- sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod || sudo killall coreaudiod
|
||||
script:
|
||||
# Build in a case-sensitive disk image to catch failures that only happen on case-sensitive filesystems.
|
||||
- hdiutil create -type SPARSEBUNDLE -fs 'Case-sensitive Journaled HFS+' -volname bgmbuild -nospotlight -verbose -attach -size 50m bgmbuild.dmg
|
||||
- hdiutil create -type SPARSEBUNDLE -fs 'Case-sensitive Journaled HFS+' -volname bgmbuild -nospotlight -verbose -attach -size 100m bgmbuild.dmg
|
||||
- sudo cp -r . /Volumes/bgmbuild
|
||||
- cd /Volumes/bgmbuild
|
||||
# Install Background Music.
|
||||
@@ -48,7 +53,10 @@ script:
|
||||
- ls -la "/usr/local/libexec/BGMXPCHelper.xpc" || ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"
|
||||
- ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"
|
||||
# Close BGMApp (which the install script opened).
|
||||
- osascript -e 'tell application "Background Music" to quit'
|
||||
#
|
||||
# The killall fallback command is necessary because the AppleScript gets "user canceled" on Travis'
|
||||
# Xcode 9 images for some reason.
|
||||
- osascript -e 'tell application "Background Music" to quit' || killall "Background Music"
|
||||
# Skip the UI tests until Travis has support for them.
|
||||
- BGMApp/BGMAppTests/UITests/travis-skip.py
|
||||
# Run the tests.
|
||||
@@ -63,8 +71,15 @@ script:
|
||||
- if ls -la "/usr/local/libexec/BGMXPCHelper.xpc"; then false; fi
|
||||
- if ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"; then false; fi
|
||||
- if ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"; then false; fi
|
||||
# Build the .pkg installer.
|
||||
- ./package.sh
|
||||
# Return early if we're not testing packaging on this OS X version.
|
||||
- if [[ "$PACKAGE" == "false" ]]; then exit 0; fi
|
||||
# Build the .pkg installer. Print the build logs if it fails. If this build is for a tag with
|
||||
# "DEBUG" in its name, build a debug package. (More detailed logging, no optimization, etc.)
|
||||
- if [[ "$TRAVIS_TAG" =~ .*DEBUG.* ]]; then
|
||||
./package.sh -d || (cat build_and_install.log && travis_terminate 1);
|
||||
else
|
||||
./package.sh || (cat build_and_install.log && travis_terminate 1);
|
||||
fi
|
||||
# Install the .pkg.
|
||||
- sudo installer -pkg Background-Music-*/BackgroundMusic-*.pkg -target / -verbose -dumplog
|
||||
# Check the BGM dirs and files were installed again.
|
||||
@@ -83,6 +98,8 @@ deploy:
|
||||
file_glob: true
|
||||
file: Background-Music-*/*
|
||||
skip_cleanup: true
|
||||
name: $TRAVIS_TAG
|
||||
prerelease: true
|
||||
on:
|
||||
repo: kyleneideck/BackgroundMusic
|
||||
tags: true
|
||||
|
||||
Generated
+6
@@ -37,6 +37,12 @@
|
||||
<FileRef
|
||||
location = "group:uninstall.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:package.sh">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:pkg">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Images">
|
||||
</FileRef>
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */; };
|
||||
@@ -32,7 +37,7 @@
|
||||
1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* 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.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */; };
|
||||
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 */; };
|
||||
1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
@@ -40,17 +45,37 @@
|
||||
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 */; };
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; };
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* 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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
1CC1DF961BE8607700FB8FE4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1CC1DF951BE8607700FB8FE4 /* Images.xcassets */; };
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* 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 */; };
|
||||
1CCC4F4D1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F4B1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm */; };
|
||||
1CCC4F4E1E581C40008053E4 /* Mock_CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F4C1E581C40008053E4 /* Mock_CAHALAudioObject.cpp */; };
|
||||
1CCC4F621E584100008053E4 /* BGMAppUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F611E584100008053E4 /* BGMAppUITests.m */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
1CD989351ECFFC9E0014BBBF /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; };
|
||||
1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; };
|
||||
@@ -65,7 +90,7 @@
|
||||
1CD9893F1ECFFC9E0014BBBF /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; };
|
||||
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
|
||||
1CD989411ECFFCD10014BBBF /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; };
|
||||
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */; };
|
||||
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; };
|
||||
1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; };
|
||||
1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */; };
|
||||
1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; };
|
||||
@@ -91,8 +116,26 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */; };
|
||||
@@ -120,13 +163,11 @@
|
||||
2743CA111D86D7FA0089613B /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; };
|
||||
2743CA121D86D7FA0089613B /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; };
|
||||
2743CA141D86D7FA0089613B /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; };
|
||||
2743CA151D86D7FA0089613B /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; };
|
||||
2743CA161D86D7FA0089613B /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; };
|
||||
2743CA171D86D7FA0089613B /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; };
|
||||
2743CA181D86D7FA0089613B /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
|
||||
2743CA191D86D7FA0089613B /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; };
|
||||
2743CA1D1D86DA9B0089613B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2743CA1C1D86DA9B0089613B /* Foundation.framework */; };
|
||||
2743CA1F1D86DE1C0089613B /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; };
|
||||
2743CA211D86DE780089613B /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; };
|
||||
2743CA221D86DE960089613B /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; };
|
||||
2743CA231D86DEA70089613B /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
|
||||
@@ -136,7 +177,7 @@
|
||||
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.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.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 */; };
|
||||
27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; };
|
||||
@@ -210,16 +251,25 @@
|
||||
1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMASOutputDevice.h; path = Scripting/BGMASOutputDevice.h; sourceTree = "<group>"; };
|
||||
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlsList.cpp; sourceTree = "<group>"; };
|
||||
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlsList.h; sourceTree = "<group>"; };
|
||||
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumes.mm; 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>"; };
|
||||
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>"; };
|
||||
1C50FF641EC9F4500031A6EA /* libPublicUtility.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libPublicUtility.a; path = "../../../Library/Developer/Xcode/DerivedData/BGM-cgeucfvbrkmtbnhewbqjwrqspirp/Build/Products/Debug/libPublicUtility.a"; 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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
1CB8B33A1BBA75EF000E2DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppDelegate.h; sourceTree = "<group>"; };
|
||||
@@ -233,6 +283,8 @@
|
||||
1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CADebugger.cpp; path = PublicUtility/CADebugger.cpp; sourceTree = "<group>"; };
|
||||
1CC1DF901BE5891300FB8FE4 /* CADebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugger.h; path = PublicUtility/CADebugger.h; sourceTree = "<group>"; };
|
||||
1CC1DF951BE8607700FB8FE4 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMTermination.mm; sourceTree = "<group>"; };
|
||||
1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMTermination.h; sourceTree = "<group>"; };
|
||||
1CCC4F3B1E58196C008053E4 /* BGMXPCHelperTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "BGMXPCHelperTests-Info.plist"; path = "BGMXPCHelperTests/BGMXPCHelperTests-Info.plist"; sourceTree = SOURCE_ROOT; };
|
||||
1CCC4F3C1E58196C008053E4 /* BGMXPCHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMXPCHelperTests.m; path = BGMXPCHelperTests/BGMXPCHelperTests.m; sourceTree = SOURCE_ROOT; };
|
||||
1CCC4F491E581C0D008053E4 /* BGMAppUnitTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "BGMAppUnitTests-Info.plist"; path = "UnitTests/BGMAppUnitTests-Info.plist"; sourceTree = "<group>"; };
|
||||
@@ -240,10 +292,13 @@
|
||||
1CCC4F4C1E581C40008053E4 /* Mock_CAHALAudioObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioObject.cpp; path = UnitTests/Mock_CAHALAudioObject.cpp; sourceTree = "<group>"; };
|
||||
1CCC4F541E584081008053E4 /* BGMAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1CCC4F5F1E5840EF008053E4 /* BGMAppUITests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "BGMAppUITests-Info.plist"; path = "UITests/BGMAppUITests-Info.plist"; sourceTree = "<group>"; };
|
||||
1CCC4F611E584100008053E4 /* BGMAppUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMAppUITests.m; path = BGMAppTests/UITests/BGMAppUITests.m; sourceTree = SOURCE_ROOT; };
|
||||
1CCC4F611E584100008053E4 /* BGMAppUITests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMAppUITests.mm; path = BGMAppTests/UITests/BGMAppUITests.mm; sourceTree = SOURCE_ROOT; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAudioDeviceManager.mm; sourceTree = "<group>"; };
|
||||
@@ -291,7 +346,7 @@
|
||||
27D643B41C9FABBD00737F6E /* BGM_Types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_Types.h; path = ../SharedSource/BGM_Types.h; sourceTree = "<group>"; };
|
||||
27D643B51C9FABBD00737F6E /* BGMXPCProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCProtocols.h; path = ../SharedSource/BGMXPCProtocols.h; sourceTree = "<group>"; };
|
||||
27D643B91C9FB84C00737F6E /* BGMXPCHelperService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCHelperService.h; path = BGMXPCHelper/BGMXPCHelperService.h; sourceTree = SOURCE_ROOT; };
|
||||
27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMXPCHelperService.m; path = BGMXPCHelper/BGMXPCHelperService.m; sourceTree = SOURCE_ROOT; };
|
||||
27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMXPCHelperService.mm; path = BGMXPCHelper/BGMXPCHelperService.mm; sourceTree = SOURCE_ROOT; };
|
||||
27D643BB1C9FB84C00737F6E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = BGMXPCHelper/Info.plist; sourceTree = SOURCE_ROOT; };
|
||||
27D643BC1C9FB84C00737F6E /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = BGMXPCHelper/main.m; sourceTree = SOURCE_ROOT; };
|
||||
27D643C41C9FBE5600737F6E /* BGM_TestUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_TestUtils.h; path = ../SharedSource/BGM_TestUtils.h; sourceTree = "<group>"; };
|
||||
@@ -306,6 +361,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */,
|
||||
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */,
|
||||
1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */,
|
||||
@@ -316,6 +372,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -324,6 +381,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C1AA4B31F9DE40000BCFB22 /* AudioToolbox.framework in Frameworks */,
|
||||
1CF2D58F1F944773008B6E35 /* CoreAudio.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -445,6 +504,8 @@
|
||||
1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */,
|
||||
279F48751DD6D73900768A85 /* BGMHermes.h */,
|
||||
279F48761DD6D73900768A85 /* BGMHermes.m */,
|
||||
1C8D8303204238DB00A838F2 /* BGMSwinsian.h */,
|
||||
1C8D8302204238DB00A838F2 /* BGMSwinsian.m */,
|
||||
27379B881C7C562D0084A24C /* BGMVLC.h */,
|
||||
27379B891C7C562D0084A24C /* BGMVLC.m */,
|
||||
273F10DD1CC3D0B900C1C6DA /* BGMVOX.h */,
|
||||
@@ -486,12 +547,20 @@
|
||||
children = (
|
||||
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */,
|
||||
1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */,
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */,
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */,
|
||||
1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */,
|
||||
1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */,
|
||||
1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */,
|
||||
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */,
|
||||
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */,
|
||||
1CD410D21F9EDDAD0070A094 /* BGMAppVolumesController.h */,
|
||||
1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */,
|
||||
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */,
|
||||
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */,
|
||||
1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */,
|
||||
1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */,
|
||||
1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */,
|
||||
1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */,
|
||||
27C457E41CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.h */,
|
||||
27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */,
|
||||
1C1465B91BCC49D1003AEFE6 /* BGMAutoPauseMusic.h */,
|
||||
@@ -504,6 +573,8 @@
|
||||
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */,
|
||||
1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */,
|
||||
1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */,
|
||||
1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */,
|
||||
1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */,
|
||||
2743C9ED1D8538700089613B /* BGMUserDefaults.h */,
|
||||
2743C9F01D853FBB0089613B /* BGMUserDefaults.m */,
|
||||
2795973C1C982E8C00A002FB /* BGMXPCListener.h */,
|
||||
@@ -572,6 +643,7 @@
|
||||
children = (
|
||||
1CCC4F4B1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm */,
|
||||
1CCC4F4C1E581C40008053E4 /* Mock_CAHALAudioObject.cpp */,
|
||||
1CEACF4E1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp */,
|
||||
);
|
||||
name = "Unit Tests";
|
||||
sourceTree = "<group>";
|
||||
@@ -580,7 +652,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C2FC30D1EBC97DA00A76592 /* BGMApp.h */,
|
||||
1CCC4F611E584100008053E4 /* BGMAppUITests.m */,
|
||||
1CCC4F611E584100008053E4 /* BGMAppUITests.mm */,
|
||||
);
|
||||
name = "UI Tests";
|
||||
path = ../BGMAppUITests;
|
||||
@@ -598,9 +670,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27F7D4911D2484A300821C4B /* Decibel.h */,
|
||||
279F48781DD6D94000768A85 /* Hermes.h */,
|
||||
27379B851C7C54870084A24C /* iTunes.h */,
|
||||
27379B861C7C54870084A24C /* Spotify.h */,
|
||||
279F48781DD6D94000768A85 /* Hermes.h */,
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */,
|
||||
27379B871C7C552A0084A24C /* VLC.h */,
|
||||
273F10DC1CC3CF9C00C1C6DA /* VOX.h */,
|
||||
);
|
||||
@@ -611,7 +684,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27D643B91C9FB84C00737F6E /* BGMXPCHelperService.h */,
|
||||
27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.m */,
|
||||
27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */,
|
||||
277170141CA24D7C00AB34B4 /* BGMXPCListenerDelegate.h */,
|
||||
277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */,
|
||||
27D643BC1C9FB84C00737F6E /* main.m */,
|
||||
@@ -624,7 +697,7 @@
|
||||
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C50FF641EC9F4500031A6EA /* libPublicUtility.a */,
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */,
|
||||
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
|
||||
1C1963021BCAC160008A4DF7 /* CoreAudio.framework */,
|
||||
@@ -747,7 +820,7 @@
|
||||
KnownAssetTags = (
|
||||
New,
|
||||
);
|
||||
LastUpgradeCheck = 0820;
|
||||
LastUpgradeCheck = 0900;
|
||||
ORGANIZATIONNAME = "Background Music contributors";
|
||||
TargetAttributes = {
|
||||
1CB8B3351BBA75EF000E2DD1 = {
|
||||
@@ -853,7 +926,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Append the git HEAD short ID to the build version for snapshot builds. Thanks to\n# Václav Slavík for the initial version of this: http://stackoverflow.com/a/26354117/1091063\n# TODO: Update CFBundleVersion as well?\nTAG=$(/usr/bin/git tag --points-at HEAD 2>/dev/null)\nif [[ $? -eq 0 ]] && [[ \"${TAG}\" == \"\" ]]; then # If HEAD isn't tagged, this is a snapshot build.\n HEAD=$(/usr/bin/git rev-list HEAD --max-count=1 --abbrev-commit)\n INFO_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n if [[ \"${CONFIGURATION}\" != \"Release\" ]]; then\n TYPE=\"DEBUG\"\n else\n TYPE=\"SNAPSHOT\"\n fi\n if [[ -f \"$INFO_PLIST\" ]]; then\n CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"${INFO_PLIST}\")\n BASE_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :BGMBundleVersionBase\" \"${INFO_PLIST}\" 2>/dev/null)\n if [[ $? -ne 0 ]] || [[ \"${BASE_VERSION}\" == \"\" ]]; then\n BASE_VERSION=\"${CURRENT_VERSION}\"\n /usr/libexec/PlistBuddy -c \"Add :BGMBundleVersionBase string ${BASE_VERSION}\" \"${INFO_PLIST}\"\n fi\n NEW_VERSION=\"${BASE_VERSION}-${TYPE}-${HEAD}\"\n if [[ \"${NEW_VERSION}\" != \"${CURRENT_VERSION}\" ]]; then # Only touch the file if we need to.\n /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString ${NEW_VERSION}\" \"${INFO_PLIST}\"\n fi\n fi\nfi";
|
||||
shellScript = "# Append the git HEAD short ID to the build version for snapshot builds. Thanks to\n# Václav Slavík for the initial version of this: http://stackoverflow.com/a/26354117/1091063\n# TODO: Update CFBundleVersion as well?\n\n# If HEAD isn't tagged, or has \"SNAPSHOT\" or \"DEBUG\" in the tag name, this is a snapshot build.\n# If HEAD is tagged more than once, use the most recent.\nTAG=$(/usr/bin/git tag --points-at HEAD --sort='-taggerdate' 2>/dev/null | head -n 1)\nif [[ $? -eq 0 ]] && ( [[ \"${TAG}\" == \"\" ]] || \\\n [[ \"${TAG}\" =~ .*SNAPSHOT.* ]] || \\\n [[ \"${TAG}\" =~ .*DEBUG.* ]] ); then\n HEAD=$(/usr/bin/git rev-list HEAD --max-count=1 --abbrev-commit)\n INFO_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n if [[ \"${CONFIGURATION}\" != \"Release\" ]]; then\n TYPE=\"DEBUG\"\n else\n TYPE=\"SNAPSHOT\"\n fi\n if [[ -f \"$INFO_PLIST\" ]]; then\n CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"${INFO_PLIST}\")\n BASE_VERSION=$(/usr/libexec/PlistBuddy -c \"Print :BGMBundleVersionBase\" \"${INFO_PLIST}\" 2>/dev/null)\n if [[ $? -ne 0 ]] || [[ \"${BASE_VERSION}\" == \"\" ]]; then\n BASE_VERSION=\"${CURRENT_VERSION}\"\n /usr/libexec/PlistBuddy -c \"Add :BGMBundleVersionBase string ${BASE_VERSION}\" \"${INFO_PLIST}\"\n fi\n NEW_VERSION=\"${BASE_VERSION}-${TYPE}-${HEAD}\"\n if [[ \"${NEW_VERSION}\" != \"${CURRENT_VERSION}\" ]]; then # Only touch the file if we need to.\n /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString ${NEW_VERSION}\" \"${INFO_PLIST}\"\n fi\n fi\nfi\n";
|
||||
};
|
||||
276972891CAFCE91007A2F7C /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -866,7 +939,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
shellPath = /bin/bash;
|
||||
shellScript = "/bin/bash $SRCROOT/BGMXPCHelper/post_install.sh";
|
||||
shellScript = "/bin/bash $SRCROOT/BGMXPCHelper/post_install.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -875,7 +948,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */,
|
||||
1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */,
|
||||
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */,
|
||||
1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */,
|
||||
273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */,
|
||||
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */,
|
||||
@@ -886,10 +962,11 @@
|
||||
1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */,
|
||||
1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */,
|
||||
1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */,
|
||||
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.mm in Sources */,
|
||||
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */,
|
||||
1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */,
|
||||
1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */,
|
||||
1CE7064C1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm in Sources */,
|
||||
1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */,
|
||||
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */,
|
||||
2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */,
|
||||
@@ -906,8 +983,11 @@
|
||||
2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */,
|
||||
1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */,
|
||||
2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */,
|
||||
1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */,
|
||||
1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */,
|
||||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */,
|
||||
27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */,
|
||||
1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */,
|
||||
@@ -924,14 +1004,20 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */,
|
||||
1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */,
|
||||
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */,
|
||||
1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */,
|
||||
1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */,
|
||||
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.mm in Sources */,
|
||||
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.m in Sources */,
|
||||
1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */,
|
||||
1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */,
|
||||
1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */,
|
||||
1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
1CD989461ECFFCFC0014BBBF /* BGMMusicPlayers.mm in Sources */,
|
||||
1CD989471ECFFCFC0014BBBF /* BGMScriptingBridge.m in Sources */,
|
||||
1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */,
|
||||
@@ -964,7 +1050,7 @@
|
||||
1CD9893E1ECFFC9E0014BBBF /* CAHALAudioStream.cpp in Sources */,
|
||||
1CD9893F1ECFFC9E0014BBBF /* CAHALAudioSystemObject.cpp in Sources */,
|
||||
1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */,
|
||||
1CCC4F621E584100008053E4 /* BGMAppUITests.m in Sources */,
|
||||
1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */,
|
||||
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */,
|
||||
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */,
|
||||
);
|
||||
@@ -974,7 +1060,25 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
27D643C01C9FB99200737F6E /* BGMXPCHelperService.m in Sources */,
|
||||
1C1AA4B11F9DE3B700BCFB22 /* BGMAudioDevice.cpp in Sources */,
|
||||
1C1AA4B21F9DE3B700BCFB22 /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C1AA4B01F9C673000BCFB22 /* BGM_Utils.cpp in Sources */,
|
||||
1CF2D5941F9447AE008B6E35 /* CACFArray.cpp in Sources */,
|
||||
1CF2D5951F9447AE008B6E35 /* CACFDictionary.cpp in Sources */,
|
||||
1CF2D5961F9447AE008B6E35 /* CACFNumber.cpp in Sources */,
|
||||
1CF2D5971F9447AE008B6E35 /* CACFString.cpp in Sources */,
|
||||
1CF2D5981F9447AE008B6E35 /* CADebugger.cpp in Sources */,
|
||||
1CF2D5991F9447AE008B6E35 /* CADebugMacros.cpp in Sources */,
|
||||
1CF2D59A1F9447AE008B6E35 /* CADebugPrintf.cpp in Sources */,
|
||||
1CF2D59B1F9447AE008B6E35 /* CAHostTimeBase.cpp in Sources */,
|
||||
1CF2D59C1F9447AE008B6E35 /* CAMutex.cpp in Sources */,
|
||||
1CF2D59D1F9447AE008B6E35 /* CAPThread.cpp in Sources */,
|
||||
1CF2D59E1F9447AE008B6E35 /* CARingBuffer.cpp in Sources */,
|
||||
1CF2D5931F94479A008B6E35 /* CAHALAudioStream.cpp in Sources */,
|
||||
1CF2D5901F944789008B6E35 /* CAHALAudioDevice.cpp in Sources */,
|
||||
1CF2D5911F944789008B6E35 /* CAHALAudioObject.cpp in Sources */,
|
||||
1CF2D5921F944789008B6E35 /* CAHALAudioSystemObject.cpp in Sources */,
|
||||
27D643C01C9FB99200737F6E /* BGMXPCHelperService.mm in Sources */,
|
||||
27D643C11C9FB99200737F6E /* main.m in Sources */,
|
||||
277170161CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m in Sources */,
|
||||
);
|
||||
@@ -984,13 +1088,17 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */,
|
||||
1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */,
|
||||
1CEACF4D1F34793700FEC143 /* CAHALAudioDevice.cpp in Sources */,
|
||||
1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */,
|
||||
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
|
||||
27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */,
|
||||
27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */,
|
||||
27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */,
|
||||
2743CA211D86DE780089613B /* BGMDeviceControlSync.cpp in Sources */,
|
||||
2743CA1F1D86DE1C0089613B /* CAHALAudioDevice.cpp in Sources */,
|
||||
2743CA0C1D86D7FA0089613B /* CACFArray.cpp in Sources */,
|
||||
1C837DDA1F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */,
|
||||
2743CA0D1D86D7FA0089613B /* CACFDictionary.cpp in Sources */,
|
||||
2743CA0E1D86D7FA0089613B /* CACFNumber.cpp in Sources */,
|
||||
2743CA0F1D86D7FA0089613B /* CACFString.cpp in Sources */,
|
||||
@@ -1000,20 +1108,22 @@
|
||||
2743CA121D86D7FA0089613B /* CADebugPrintf.cpp in Sources */,
|
||||
1CCC4F4D1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm in Sources */,
|
||||
2743CA141D86D7FA0089613B /* CAHALAudioStream.cpp in Sources */,
|
||||
2743CA151D86D7FA0089613B /* CAHALAudioSystemObject.cpp in Sources */,
|
||||
2743CA161D86D7FA0089613B /* CAHostTimeBase.cpp in Sources */,
|
||||
2743CA171D86D7FA0089613B /* CAMutex.cpp in Sources */,
|
||||
2743CA181D86D7FA0089613B /* CAPThread.cpp in Sources */,
|
||||
2743CA191D86D7FA0089613B /* CARingBuffer.cpp in Sources */,
|
||||
2743CA0A1D86D52D0089613B /* BGMAudioDeviceManager.mm in Sources */,
|
||||
2743CA031D86D41C0089613B /* BGMScriptingBridge.m in Sources */,
|
||||
1CEACF4F1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp in Sources */,
|
||||
2743CA041D86D41C0089613B /* BGMMusicPlayer.m in Sources */,
|
||||
1CD410D61F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */,
|
||||
2743CA051D86D41C0089613B /* BGMDecibel.m in Sources */,
|
||||
2743CA061D86D41C0089613B /* BGMSpotify.m in Sources */,
|
||||
2743CA071D86D41C0089613B /* BGMVLC.m in Sources */,
|
||||
1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */,
|
||||
2743CA081D86D41C0089613B /* BGMVOX.m in Sources */,
|
||||
2743CA091D86D41C0089613B /* BGMUserDefaults.m in Sources */,
|
||||
1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */,
|
||||
2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */,
|
||||
2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */,
|
||||
);
|
||||
@@ -1060,6 +1170,7 @@
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS = 0;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS = 1;
|
||||
CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
|
||||
@@ -1069,7 +1180,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
@@ -1082,6 +1195,8 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
@@ -1103,7 +1218,7 @@
|
||||
"CoreAudio_ThreadStampMessages=0",
|
||||
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
|
||||
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
|
||||
"CoreAudio_StopOnThrow=1",
|
||||
"CoreAudio_StopOnThrow=0",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
@@ -1125,6 +1240,7 @@
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "-fno-omit-frame-pointer";
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
@@ -1156,6 +1272,7 @@
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS = 0;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS = 1;
|
||||
CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
|
||||
@@ -1165,7 +1282,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
@@ -1178,6 +1297,8 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
@@ -1199,7 +1320,7 @@
|
||||
"CoreAudio_ThreadStampMessages=0",
|
||||
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
|
||||
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
|
||||
"CoreAudio_StopOnThrow=1",
|
||||
"CoreAudio_StopOnThrow=0",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
@@ -1221,6 +1342,7 @@
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "-fno-omit-frame-pointer";
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
@@ -1233,6 +1355,7 @@
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS = 0;
|
||||
BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS = 0;
|
||||
CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
|
||||
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
|
||||
@@ -1242,7 +1365,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
@@ -1255,6 +1380,8 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
@@ -1294,6 +1421,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.9;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CFLAGS = "";
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "-Wpartial-availability";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -55,6 +57,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -70,6 +74,13 @@
|
||||
ReferencedContainer = "container:BGMApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "ASAN_OPTIONS"
|
||||
value = "detect_odr_violation=1:use_odr_indicator=1:detect_stack_use_after_return=1:detect_invalid_pointer_pairs=2:check_initialization_order=1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -65,11 +66,11 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
enableAddressSanitizer = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
@@ -82,10 +83,20 @@
|
||||
ReferencedContainer = "container:BGMApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "--show-dock-icon"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--no-persistent-data"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "ASAN_OPTIONS"
|
||||
value = "detect_odr_violation=0,use_odr_indicator=1"
|
||||
value = "detect_odr_violation=1:use_odr_indicator=1:detect_stack_use_after_return=1:detect_invalid_pointer_pairs=2:check_initialization_order=1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMAppDelegate.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Sets up and tears down the app.
|
||||
//
|
||||
@@ -29,12 +29,26 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
// Tags for UI elements in MainMenu.xib
|
||||
static NSInteger const kVolumesHeadingMenuItemTag = 3;
|
||||
static NSInteger const kSeparatorBelowVolumesMenuItemTag = 4;
|
||||
|
||||
@interface BGMAppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate>
|
||||
|
||||
@property (weak) IBOutlet NSMenu* bgmMenu;
|
||||
|
||||
@property (weak) IBOutlet NSView* outputVolumeView;
|
||||
@property (weak) IBOutlet NSTextField* outputVolumeLabel;
|
||||
@property (weak) IBOutlet NSSlider* outputVolumeSlider;
|
||||
|
||||
@property (weak) IBOutlet NSView* systemSoundsView;
|
||||
@property (weak) IBOutlet NSSlider* systemSoundsSlider;
|
||||
|
||||
@property (weak) IBOutlet NSView* appVolumeView;
|
||||
|
||||
@property (weak) IBOutlet NSPanel* aboutPanel;
|
||||
@property (unsafe_unretained) IBOutlet NSTextView* aboutPanelLicenseView;
|
||||
|
||||
@property (weak) IBOutlet NSMenuItem* autoPauseMenuItemUnwrapped;
|
||||
|
||||
@property (readonly) BGMAudioDeviceManager* audioDevices;
|
||||
|
||||
+160
-46
@@ -17,27 +17,38 @@
|
||||
// BGMAppDelegate.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Includes
|
||||
#import "BGMAppDelegate.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
#import "BGMAppVolumes.h"
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMTermination.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 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
|
||||
@@ -50,7 +61,8 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
BGMAutoPauseMusic* autoPauseMusic;
|
||||
BGMAutoPauseMenuItem* autoPauseMenuItem;
|
||||
BGMMusicPlayers* musicPlayers;
|
||||
BGMAppVolumes* appVolumes;
|
||||
BGMSystemSoundsVolume* systemSoundsVolume;
|
||||
BGMAppVolumesController* appVolumes;
|
||||
BGMPreferencesMenu* prefsMenu;
|
||||
BGMXPCListener* xpcListener;
|
||||
}
|
||||
@@ -58,8 +70,9 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
@synthesize audioDevices = audioDevices;
|
||||
|
||||
- (void) awakeFromNib {
|
||||
// Show BGMApp in the dock, if the command-line option for that was passed. This is used by the UI tests.
|
||||
if ([NSProcessInfo.processInfo.arguments indexOfObject:@"--show-dock-icon"] != NSNotFound) {
|
||||
// Show BGMApp in the dock, if the command-line option for that was passed. This is used by the
|
||||
// UI tests.
|
||||
if ([NSProcessInfo.processInfo.arguments indexOfObject:kOptShowDockIcon] != NSNotFound) {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
}
|
||||
|
||||
@@ -71,13 +84,24 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
// Set up the status bar item. (The thing you click to show BGMApp's UI.)
|
||||
- (void) initStatusBarItem {
|
||||
statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||
|
||||
// Set the icon
|
||||
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
|
||||
|
||||
// NSStatusItem doesn't have the "button" property on OS X 10.9.
|
||||
BOOL buttonAvailable = (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10);
|
||||
|
||||
// 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 the icon.
|
||||
NSImage* icon = [NSImage imageNamed:@"FermataIcon"];
|
||||
|
||||
if (icon != nil) {
|
||||
NSRect statusBarItemFrame;
|
||||
|
||||
@@ -134,74 +158,163 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
|
||||
NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]);
|
||||
|
||||
// Set up audioDevices, which coordinates BGMDevice and the output device. It manages
|
||||
// playthrough, volume/mute controls, etc.
|
||||
if (![self initAudioDeviceManager]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make BGMDevice the default device.
|
||||
[self setBGMDeviceAsDefault];
|
||||
|
||||
// Handle some of the unusual reasons BGMApp might have to exit, mostly crashes.
|
||||
BGMTermination::SetUpTerminationCleanUp(audioDevices);
|
||||
|
||||
// Set up the rest of the UI and other external interfaces.
|
||||
|
||||
// audioDevices coordinates BGMDevice and the output device. It manages playthrough, volume/mute controls, etc.
|
||||
{
|
||||
NSError* error;
|
||||
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&error];
|
||||
if (audioDevices == nil) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:error.code];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set Background Music Device as your default audio device."
|
||||
informativeText:@"You might be able to set it yourself."];
|
||||
}
|
||||
}
|
||||
|
||||
BGMUserDefaults* userDefaults = [self createUserDefaults];
|
||||
|
||||
musicPlayers = [[BGMMusicPlayers alloc] initWithAudioDevices:audioDevices
|
||||
userDefaults:userDefaults];
|
||||
|
||||
|
||||
autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices
|
||||
musicPlayers:musicPlayers];
|
||||
|
||||
autoPauseMenuItem = [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
autoPauseMusic:autoPauseMusic
|
||||
musicPlayers:musicPlayers
|
||||
userDefaults:userDefaults];
|
||||
[self setUpMainMenu:userDefaults];
|
||||
|
||||
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
|
||||
helperConnectionErrorHandler:^(NSError* error) {
|
||||
NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) "
|
||||
"BGMXPCHelper connection error: %@",
|
||||
error);
|
||||
|
||||
[self showXPCHelperErrorMessage:error];
|
||||
}];
|
||||
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithMenu:self.bgmMenu
|
||||
appVolumeView:self.appVolumeView
|
||||
audioDevices:audioDevices];
|
||||
|
||||
}
|
||||
|
||||
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
|
||||
- (BOOL) initAudioDeviceManager {
|
||||
NSError* error;
|
||||
audioDevices = [[BGMAudioDeviceManager alloc] initWithError:&error];
|
||||
|
||||
if (!audioDevices) {
|
||||
[self showDeviceNotFoundErrorMessageAndExit:error.code];
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Sets the "Background Music" virtual audio device (BGMDevice) as the user's default audio device.
|
||||
- (void) setBGMDeviceAsDefault {
|
||||
void (^setDefaultDevice)() = ^{
|
||||
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set the Background Music device as your"
|
||||
"default audio device."
|
||||
informativeText:@"You might be able to change it yourself."];
|
||||
}
|
||||
};
|
||||
|
||||
// Skip this if we're compiling on a version of macOS before 10.14 as won't compile and it
|
||||
// isn't needed.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
|
||||
if (@available(macOS 10.14, *)) {
|
||||
// On macOS 10.14+ we need to get the user's permission to use input devices before we can
|
||||
// use BGMDevice for playthrough (see BGMPlayThrough), so we wait until they've given it
|
||||
// before making BGMDevice the default device. This way, if the user is playing audio when
|
||||
// they open Background Music, we won't interrupt it while we're waiting for them to click
|
||||
// OK.
|
||||
//
|
||||
// TODO: This isn't a perfect solution because, if the user takes too long to accept,
|
||||
// BGMPlayThrough will try to use BGMDevice again and log some errors.
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
|
||||
completionHandler:^(BOOL granted) {
|
||||
if (granted) {
|
||||
DebugMsg("BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission granted");
|
||||
setDefaultDevice();
|
||||
} else {
|
||||
NSLog(@"BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission denied");
|
||||
// TODO: If they don't accept, Background Music won't work
|
||||
// at all and the only way to fix it is in System
|
||||
// Preferences, so we should show an error dialog
|
||||
// with instructions.
|
||||
}
|
||||
}];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// We can change the device immediately on older versions of macOS because they don't
|
||||
// require user permission for input devices.
|
||||
setDefaultDevice();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults {
|
||||
autoPauseMenuItem =
|
||||
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
autoPauseMusic:autoPauseMusic
|
||||
musicPlayers:musicPlayers
|
||||
userDefaults:userDefaults];
|
||||
|
||||
[self initVolumesMenuSection];
|
||||
|
||||
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
|
||||
audioDevices:audioDevices
|
||||
musicPlayers:musicPlayers
|
||||
aboutPanel:self.aboutPanel
|
||||
aboutPanelLicenseView:self.aboutPanelLicenseView];
|
||||
|
||||
|
||||
// Handle events about the main menu. (See the NSMenuDelegate methods below.)
|
||||
self.bgmMenu.delegate = self;
|
||||
}
|
||||
|
||||
- (BGMUserDefaults*) createUserDefaults {
|
||||
BOOL persistentDefaults = [NSProcessInfo.processInfo.arguments indexOfObject:@"--no-persistent-data"] == NSNotFound;
|
||||
BOOL persistentDefaults =
|
||||
[NSProcessInfo.processInfo.arguments indexOfObject:kOptNoPersistentData] == NSNotFound;
|
||||
NSUserDefaults* wrappedDefaults = persistentDefaults ? [NSUserDefaults standardUserDefaults] : nil;
|
||||
return [[BGMUserDefaults alloc] initWithDefaults:wrappedDefaults];
|
||||
}
|
||||
|
||||
- (void) initVolumesMenuSection {
|
||||
// Create the menu item with the (main) output volume slider.
|
||||
BGMOutputVolumeMenuItem* outputVolume =
|
||||
[[BGMOutputVolumeMenuItem alloc] initWithAudioDevices:audioDevices
|
||||
view:self.outputVolumeView
|
||||
slider:self.outputVolumeSlider
|
||||
deviceLabel:self.outputVolumeLabel];
|
||||
[audioDevices setOutputVolumeMenuItem:outputVolume];
|
||||
|
||||
NSInteger headingIdx = [self.bgmMenu indexOfItemWithTag:kVolumesHeadingMenuItemTag];
|
||||
|
||||
// Add it to the main menu below the "Volumes" heading.
|
||||
[self.bgmMenu insertItem:outputVolume atIndex:(headingIdx + 1)];
|
||||
|
||||
// Add the volume control for system (UI) sounds to the menu.
|
||||
BGMAudioDevice uiSoundsDevice = [audioDevices bgmDevice].GetUISoundsBGMDeviceInstance();
|
||||
|
||||
systemSoundsVolume =
|
||||
[[BGMSystemSoundsVolume alloc] initWithUISoundsDevice:uiSoundsDevice
|
||||
view:self.systemSoundsView
|
||||
slider:self.systemSoundsSlider];
|
||||
|
||||
[self.bgmMenu insertItem:systemSoundsVolume.menuItem atIndex:(headingIdx + 2)];
|
||||
|
||||
// Add the app volumes to the menu.
|
||||
appVolumes = [[BGMAppVolumesController alloc] initWithMenu:self.bgmMenu
|
||||
appVolumeView:self.appVolumeView
|
||||
audioDevices:audioDevices];
|
||||
}
|
||||
|
||||
- (void) applicationWillTerminate:(NSNotification*)aNotification {
|
||||
#pragma unused (aNotification)
|
||||
|
||||
DebugMsg("BGMAppDelegate::applicationWillTerminate");
|
||||
|
||||
// Change the user's default output device back.
|
||||
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
@@ -224,12 +337,14 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
// TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL and offer to install them if not. Also,
|
||||
// it would be nice if we could restart coreaudiod automatically (using launchd).
|
||||
[alert setMessageText:@"Could not find the Background Music virtual audio device."];
|
||||
[alert setInformativeText:@"Make sure you've installed Background Music.driver to /Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo killall coreaudiod\")."];
|
||||
[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."];
|
||||
}
|
||||
|
||||
|
||||
// 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];
|
||||
});
|
||||
@@ -328,7 +443,6 @@ static float const kStatusBarIconPadding = 0.25;
|
||||
DebugMsg("BGMAppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,19 +17,32 @@
|
||||
// BGMAppVolumes.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppVolumes : NSObject
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu appVolumeView:(NSView*)view audioDevices:(BGMAudioDeviceManager*)audioDevices;
|
||||
- (id) initWithController:(BGMAppVolumesController*)inController
|
||||
bgmMenu:(NSMenu*)inMenu
|
||||
appVolumeView:(NSView*)inView;
|
||||
|
||||
// Pass -1 for initialVolume or initialPan to leave the volume/pan at its default level.
|
||||
- (void) insertMenuItemForApp:(NSRunningApplication*)app
|
||||
initialVolume:(int)volume
|
||||
initialPan:(int)pan;
|
||||
|
||||
- (void) removeMenuItemForApp:(NSRunningApplication*)app;
|
||||
|
||||
- (void) removeAllAppVolumeMenuItems;
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,7 +50,10 @@
|
||||
|
||||
@protocol BGMAppVolumeMenuItemSubview <NSObject>
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)item;
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)item;
|
||||
|
||||
@end
|
||||
|
||||
@@ -54,13 +70,16 @@
|
||||
|
||||
@interface BGMAVM_VolumeSlider : NSSlider <BGMAppVolumeMenuItemSubview>
|
||||
|
||||
- (void) setRelativeVolume:(NSNumber*)relativeVolume;
|
||||
- (void) setRelativeVolume:(int)relativeVolume;
|
||||
|
||||
@end
|
||||
|
||||
@interface BGMAVM_PanSlider : NSSlider <BGMAppVolumeMenuItemSubview>
|
||||
|
||||
- (void) setPanPosition:(NSNumber*)panPosition;
|
||||
- (void) setPanPosition:(int)panPosition;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMAppVolumes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppVolumes.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppDelegate.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
static float const kSlidersSnapWithin = 5;
|
||||
static CGFloat const kAppVolumeViewInitialHeight = 20;
|
||||
static NSString* const kMoreAppsMenuTitle = @"More Apps";
|
||||
|
||||
@implementation BGMAppVolumes {
|
||||
BGMAppVolumesController* controller;
|
||||
|
||||
NSMenu* bgmMenu;
|
||||
NSMenu* moreAppsMenu;
|
||||
|
||||
NSView* appVolumeView;
|
||||
CGFloat appVolumeViewFullHeight;
|
||||
|
||||
// The number of menu items this class has added to bgmMenu. Doesn't include the More Apps menu.
|
||||
NSInteger numMenuItems;
|
||||
}
|
||||
|
||||
- (id) initWithController:(BGMAppVolumesController*)inController
|
||||
bgmMenu:(NSMenu*)inMenu
|
||||
appVolumeView:(NSView*)inView {
|
||||
if ((self = [super init])) {
|
||||
controller = inController;
|
||||
bgmMenu = inMenu;
|
||||
moreAppsMenu = [[NSMenu alloc] initWithTitle:kMoreAppsMenuTitle];
|
||||
appVolumeView = inView;
|
||||
appVolumeViewFullHeight = appVolumeView.frame.size.height;
|
||||
numMenuItems = 0;
|
||||
|
||||
// Add the More Apps menu to the main menu.
|
||||
NSMenuItem* moreAppsMenuItem =
|
||||
[[NSMenuItem alloc] initWithTitle:kMoreAppsMenuTitle action:nil keyEquivalent:@""];
|
||||
moreAppsMenuItem.submenu = moreAppsMenu;
|
||||
|
||||
[bgmMenu insertItem:moreAppsMenuItem atIndex:([self lastMenuItemIndex] + 1)];
|
||||
numMenuItems++;
|
||||
|
||||
// Put an empty menu item above the More Apps menu item to fix its top margin.
|
||||
NSMenuItem* spacer = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
|
||||
spacer.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 4)];
|
||||
spacer.hidden = YES; // Tells accessibility clients to ignore this menu item.
|
||||
|
||||
[bgmMenu insertItem:spacer atIndex:[self lastMenuItemIndex]];
|
||||
numMenuItems++;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark UI Modifications
|
||||
|
||||
- (void) insertMenuItemForApp:(NSRunningApplication*)app
|
||||
initialVolume:(int)volume
|
||||
initialPan:(int)pan {
|
||||
NSMenuItem* appVolItem = [self createBlankAppVolumeMenuItem];
|
||||
|
||||
// Look through the menu item's subviews for the ones we want to set up
|
||||
for (NSView* subview in appVolItem.view.subviews) {
|
||||
if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) {
|
||||
[(NSView<BGMAppVolumeMenuItemSubview>*)subview setUpWithApp:app
|
||||
context:self
|
||||
controller:controller
|
||||
menuItem:appVolItem];
|
||||
}
|
||||
}
|
||||
|
||||
// Store the NSRunningApplication object with the menu item so when the app closes we can find the item to remove it
|
||||
appVolItem.representedObject = app;
|
||||
|
||||
// Set the slider to the volume for this app if we got one from the driver
|
||||
[self setVolumeOfMenuItem:appVolItem relativeVolume:volume panPosition:pan];
|
||||
|
||||
// NSMenuItem didn't implement NSAccessibility before OS X SDK 10.12.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101200 // MAC_OS_X_VERSION_10_12
|
||||
if ([appVolItem respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
// TODO: This doesn't show up in Accessibility Inspector for me. Not sure why.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
appVolItem.accessibilityTitle = [NSString stringWithFormat:@"%@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add the menu item to its menu.
|
||||
if (app.activationPolicy == NSApplicationActivationPolicyRegular) {
|
||||
[bgmMenu insertItem:appVolItem atIndex:[self firstMenuItemIndex]];
|
||||
numMenuItems++;
|
||||
} else if (app.activationPolicy == NSApplicationActivationPolicyAccessory) {
|
||||
[moreAppsMenu insertItem:appVolItem atIndex:0];
|
||||
}
|
||||
}
|
||||
|
||||
// Create a blank menu item to copy as a template.
|
||||
- (NSMenuItem*) createBlankAppVolumeMenuItem {
|
||||
NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
|
||||
|
||||
menuItem.view = appVolumeView;
|
||||
menuItem = [menuItem copy]; // So we can modify a copy of the view, rather than the template itself.
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
- (void) setVolumeOfMenuItem:(NSMenuItem*)menuItem relativeVolume:(int)volume panPosition:(int)pan {
|
||||
// Update the sliders.
|
||||
for (NSView* subview in menuItem.view.subviews) {
|
||||
// Set the volume.
|
||||
if (volume != -1 && [subview isKindOfClass:[BGMAVM_VolumeSlider class]]) {
|
||||
[(BGMAVM_VolumeSlider*)subview setRelativeVolume:volume];
|
||||
}
|
||||
|
||||
// Set the pan position.
|
||||
if (pan != -1 && [subview isKindOfClass:[BGMAVM_PanSlider class]]) {
|
||||
[(BGMAVM_PanSlider*)subview setPanPosition:pan];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger) firstMenuItemIndex {
|
||||
return [self lastMenuItemIndex] - numMenuItems + 1;
|
||||
}
|
||||
|
||||
- (NSInteger) lastMenuItemIndex {
|
||||
return [bgmMenu indexOfItemWithTag:kSeparatorBelowVolumesMenuItemTag] - 1;
|
||||
}
|
||||
|
||||
- (void) removeMenuItemForApp:(NSRunningApplication*)app {
|
||||
// Subtract two extra positions to skip the More Apps menu and the spacer menu item above it.
|
||||
NSInteger lastAppVolumeMenuItemIndex = [self lastMenuItemIndex] - 2;
|
||||
|
||||
// Check each app volume menu item and remove the item that controls the given app.
|
||||
|
||||
// Look through the main menu.
|
||||
for (NSInteger i = [self firstMenuItemIndex]; i <= lastAppVolumeMenuItemIndex; i++) {
|
||||
NSMenuItem* item = [bgmMenu itemAtIndex:i];
|
||||
NSRunningApplication* itemApp = item.representedObject;
|
||||
BGMAssert(itemApp, "!itemApp for %s", item.title.UTF8String);
|
||||
|
||||
if ([itemApp isEqual:app]) {
|
||||
[bgmMenu removeItem:item];
|
||||
numMenuItems--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Look through the More Apps menu.
|
||||
for (NSInteger i = 0; i < [moreAppsMenu numberOfItems]; i++) {
|
||||
NSMenuItem* item = [moreAppsMenu itemAtIndex:i];
|
||||
NSRunningApplication* itemApp = item.representedObject;
|
||||
BGMAssert(itemApp, "!itemApp for %s", item.title.UTF8String);
|
||||
|
||||
if ([itemApp isEqual:app]) {
|
||||
[moreAppsMenu removeItem:item];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showHideExtraControls:(BGMAVM_ShowMoreControlsButton*)button {
|
||||
// Show or hide an app's extra controls, currently only pan, in its App Volumes menu item.
|
||||
|
||||
NSMenuItem* menuItem = button.cell.representedObject;
|
||||
|
||||
BGMAssert(button, "!button");
|
||||
BGMAssert(menuItem, "!menuItem");
|
||||
|
||||
CGFloat width = menuItem.view.frame.size.width;
|
||||
CGFloat height = menuItem.view.frame.size.height;
|
||||
|
||||
#if DEBUG
|
||||
const char* appName = [((NSRunningApplication*)menuItem.representedObject).localizedName UTF8String];
|
||||
#endif
|
||||
|
||||
// Using this function (instead of just ==) shouldn't be necessary, but just in case.
|
||||
BOOL(^nearEnough)(CGFloat x, CGFloat y) = ^BOOL(CGFloat x, CGFloat y) {
|
||||
return fabs(x - y) < 0.01; // We don't need much precision.
|
||||
};
|
||||
|
||||
if (nearEnough(button.frameCenterRotation, 0.0)) {
|
||||
// Hide extra controls
|
||||
DebugMsg("BGMAppVolumes::showHideExtraControls: Hiding extra controls (%s)", appName);
|
||||
|
||||
BGMAssert(nearEnough(height, appVolumeViewFullHeight), "Extra controls were already hidden");
|
||||
|
||||
// Make the menu item shorter to hide the extra controls. Keep the width unchanged.
|
||||
menuItem.view.frameSize = NSMakeSize(width, kAppVolumeViewInitialHeight);
|
||||
// Turn the button upside down so the arrowhead points down.
|
||||
button.frameCenterRotation = 180.0;
|
||||
// Move the button up slightly so it aligns with the volume slider.
|
||||
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y - 1)];
|
||||
|
||||
// Set the extra controls, and anything else below the fold, to hidden so accessibility
|
||||
// clients can skip over them.
|
||||
for (NSView* subview in menuItem.view.subviews) {
|
||||
CGFloat top = subview.frame.origin.y + subview.frame.size.height;
|
||||
if (top <= 0.0) {
|
||||
subview.hidden = YES;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show extra controls
|
||||
DebugMsg("BGMAppVolumes::showHideExtraControls: Showing extra controls (%s)", appName);
|
||||
|
||||
BGMAssert(nearEnough(button.frameCenterRotation, 180.0), "Unexpected button rotation");
|
||||
BGMAssert(nearEnough(height, kAppVolumeViewInitialHeight), "Extra controls were already shown");
|
||||
|
||||
// Make the menu item taller to show the extra controls. Keep the width unchanged.
|
||||
menuItem.view.frameSize = NSMakeSize(width, appVolumeViewFullHeight);
|
||||
// Turn the button rightside up so the arrowhead points up.
|
||||
button.frameCenterRotation = 0.0;
|
||||
// Move the button down slightly, back to it's original position.
|
||||
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y + 1)];
|
||||
|
||||
// Set all of the UI elements in the menu item to "not hidden" for accessibility clients.
|
||||
for (NSView* subview in menuItem.view.subviews) {
|
||||
subview.hidden = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) removeAllAppVolumeMenuItems {
|
||||
// Remove all of the menu items this class adds to the menu except for the last two, which are
|
||||
// the More Apps menu item and the invisible spacer above it.
|
||||
while (numMenuItems > 2) {
|
||||
[bgmMenu removeItemAtIndex:[self firstMenuItemIndex]];
|
||||
numMenuItems--;
|
||||
}
|
||||
|
||||
// The More Apps menu only contains app volume menu items, so we can just remove everything.
|
||||
[moreAppsMenu removeAllItems];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark Custom Classes (IB)
|
||||
|
||||
// Custom classes for the UI elements in the app volume menu items
|
||||
|
||||
@implementation BGMAVM_AppIcon
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, ctrl, menuItem)
|
||||
|
||||
self.image = app.icon;
|
||||
|
||||
// Remove the icon from the accessibility hierarchy.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101000 // MAC_OS_X_VERSION_10_10
|
||||
if ([self.cell respondsToSelector:@selector(setAccessibilityElement:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.cell.accessibilityElement = NO;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_AppNameLabel
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, ctrl, menuItem)
|
||||
|
||||
NSString* name = app.localizedName ? (NSString*)app.localizedName : @"";
|
||||
self.stringValue = name;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_ShowMoreControlsButton
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (app, ctrl)
|
||||
|
||||
// Set up the button that show/hide the extra controls (currently only a pan slider) for the app.
|
||||
self.cell.representedObject = menuItem;
|
||||
self.target = ctx;
|
||||
self.action = @selector(showHideExtraControls:);
|
||||
|
||||
// The menu item starts out with the extra controls visible, so we hide them here.
|
||||
//
|
||||
// TODO: Leave them visible if any of the controls are set to non-default values. The user has no way to
|
||||
// tell otherwise. Maybe we should also make this button look different if the controls are hidden
|
||||
// when they have non-default values.
|
||||
[ctx showHideExtraControls:self];
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = @"More options";
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_VolumeSlider {
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* __nullable appBundleID;
|
||||
BGMAppVolumesController* controller;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
controller = ctrl;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appVolumeChanged);
|
||||
|
||||
appProcessID = app.processIdentifier;
|
||||
appBundleID = app.bundleIdentifier;
|
||||
|
||||
self.maxValue = kAppRelativeVolumeMaxRawValue;
|
||||
self.minValue = kAppRelativeVolumeMinRawValue;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Volume for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
// We have to handle snapping for volume sliders ourselves because adding a tick mark (snap point) in Interface Builder
|
||||
// changes how the slider looks.
|
||||
- (void) snap {
|
||||
// Snap to the 50% point.
|
||||
float midPoint = (float)((self.maxValue + self.minValue) / 2);
|
||||
if (self.floatValue > (midPoint - kSlidersSnapWithin) && self.floatValue < (midPoint + kSlidersSnapWithin)) {
|
||||
self.floatValue = midPoint;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setRelativeVolume:(int)relativeVolume {
|
||||
self.intValue = relativeVolume;
|
||||
[self snap];
|
||||
}
|
||||
|
||||
- (void) appVolumeChanged {
|
||||
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
|
||||
|
||||
DebugMsg("BGMAppVolumes::appVolumeChanged: App volume for %s (%d) changed to %d",
|
||||
appBundleID.UTF8String,
|
||||
appProcessID,
|
||||
self.intValue);
|
||||
|
||||
[self snap];
|
||||
|
||||
// The values from our sliders are in
|
||||
// [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already.
|
||||
[controller setVolume:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_PanSlider {
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* __nullable appBundleID;
|
||||
BGMAppVolumesController* controller;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app
|
||||
context:(BGMAppVolumes*)ctx
|
||||
controller:(BGMAppVolumesController*)ctrl
|
||||
menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
controller = ctrl;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appPanPositionChanged);
|
||||
|
||||
appProcessID = app.processIdentifier;
|
||||
appBundleID = app.bundleIdentifier;
|
||||
|
||||
self.minValue = kAppPanLeftRawValue;
|
||||
self.maxValue = kAppPanRightRawValue;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Pan for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setPanPosition:(int)panPosition {
|
||||
self.intValue = panPosition;
|
||||
}
|
||||
|
||||
- (void) appPanPositionChanged {
|
||||
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
|
||||
|
||||
DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue);
|
||||
|
||||
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already.
|
||||
[controller setPanPosition:self.intValue forAppWithProcessID:appProcessID bundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppVolumes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppVolumes.h"
|
||||
|
||||
// BGM Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CACFDictionary.h"
|
||||
#include "CACFArray.h"
|
||||
#include "CACFString.h"
|
||||
|
||||
|
||||
// Tags for UI elements in MainMenu.xib
|
||||
static NSInteger const kAppVolumesHeadingMenuItemTag = 3;
|
||||
static NSInteger const kSeparatorBelowAppVolumesMenuItemTag = 4;
|
||||
|
||||
static float const kSlidersSnapWithin = 5;
|
||||
|
||||
static CGFloat const kAppVolumeViewInitialHeight = 20;
|
||||
|
||||
@implementation BGMAppVolumes {
|
||||
NSMenu* bgmMenu;
|
||||
|
||||
NSView* appVolumeView;
|
||||
CGFloat appVolumeViewFullHeight;
|
||||
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
}
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu appVolumeView:(NSView*)view audioDevices:(BGMAudioDeviceManager*)devices {
|
||||
if ((self = [super init])) {
|
||||
bgmMenu = menu;
|
||||
appVolumeView = view;
|
||||
appVolumeViewFullHeight = appVolumeView.frame.size.height;
|
||||
audioDevices = devices;
|
||||
|
||||
// Create the menu items for controlling app volumes
|
||||
[self insertMenuItemsForApps:[[NSWorkspace sharedWorkspace] runningApplications]];
|
||||
|
||||
// Register for notifications when the user opens or closes apps, so we can update the menu
|
||||
[[NSWorkspace sharedWorkspace] addObserver:self
|
||||
forKeyPath:@"runningApplications"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
context:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[[NSWorkspace sharedWorkspace] removeObserver:self forKeyPath:@"runningApplications" context:nil];
|
||||
}
|
||||
|
||||
#pragma mark UI Modifications
|
||||
|
||||
- (void) insertMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
|
||||
NSAssert([NSThread isMainThread], @"insertMenuItemsForApps is not thread safe");
|
||||
|
||||
#ifndef NS_BLOCK_ASSERTIONS // If assertions are enabled
|
||||
auto numMenuItems = [&self]() {
|
||||
NSInteger headingIdx = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag];
|
||||
NSInteger separatorIdx = [bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag];
|
||||
return separatorIdx - headingIdx - 1;
|
||||
};
|
||||
|
||||
NSInteger numMenuItemsBeforeInsert = numMenuItems();
|
||||
NSUInteger numApps = 0;
|
||||
#endif
|
||||
|
||||
// Get the app volumes currently set on the device
|
||||
CACFArray appVolumesOnDevice((CFArrayRef)[audioDevices bgmDevice].GetPropertyData_CFType(kBGMAppVolumesAddress), false);
|
||||
|
||||
NSInteger index = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag] + 1;
|
||||
|
||||
// Add a volume-control menu item for each app
|
||||
for (NSRunningApplication* app in apps) {
|
||||
// Only show apps that appear in the dock (at first)
|
||||
// TODO: Would it be better to only show apps that are registered as HAL clients?
|
||||
if (app.activationPolicy != NSApplicationActivationPolicyRegular) continue;
|
||||
|
||||
#ifndef NS_BLOCK_ASSERTIONS // If assertions are enabled
|
||||
// Count how many apps we should add menu items for so we can check it at the end of the method
|
||||
numApps++;
|
||||
#endif
|
||||
|
||||
NSMenuItem* appVolItem = [self createBlankAppVolumeMenuItem];
|
||||
|
||||
// Look through the menu item's subviews for the ones we want to set up
|
||||
for (NSView* subview in appVolItem.view.subviews) {
|
||||
if ([subview conformsToProtocol:@protocol(BGMAppVolumeMenuItemSubview)]) {
|
||||
[(NSView<BGMAppVolumeMenuItemSubview>*)subview setUpWithApp:app context:self menuItem:appVolItem];
|
||||
}
|
||||
}
|
||||
|
||||
// Store the NSRunningApplication object with the menu item so when the app closes we can find the item to remove it
|
||||
appVolItem.representedObject = app;
|
||||
|
||||
// Set the slider to the volume for this app if we got one from the driver
|
||||
[self setVolumeOfMenuItem:appVolItem fromAppVolumes:appVolumesOnDevice];
|
||||
|
||||
// NSMenuItem didn't implement NSAccessibility before OS X SDK 10.12.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101200 // MAC_OS_X_VERSION_10_12
|
||||
if ([appVolItem respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
// TODO: This doesn't show up in Accessibility Inspector for me. Not sure why.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
appVolItem.accessibilityTitle = [NSString stringWithFormat:@"%@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
#endif
|
||||
|
||||
[bgmMenu insertItem:appVolItem atIndex:index];
|
||||
}
|
||||
|
||||
NSAssert3(numMenuItems() == (numMenuItemsBeforeInsert + numApps),
|
||||
@"Added more/fewer menu items than there were apps. Items before: %ld, items after: %ld, apps: %lu",
|
||||
(long)numMenuItemsBeforeInsert,
|
||||
(long)numMenuItems(),
|
||||
(unsigned long)numApps);
|
||||
}
|
||||
|
||||
// Create a blank menu item to copy as a template.
|
||||
- (NSMenuItem*) createBlankAppVolumeMenuItem {
|
||||
NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
|
||||
|
||||
menuItem.view = appVolumeView;
|
||||
menuItem = [menuItem copy]; // So we can modify a copy of the view, rather than the template itself.
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
- (void) removeMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
|
||||
NSAssert([NSThread isMainThread], @"removeMenuItemsForApps is not thread safe");
|
||||
|
||||
NSInteger firstItemIndex = [bgmMenu indexOfItemWithTag:kAppVolumesHeadingMenuItemTag] + 1;
|
||||
NSInteger lastItemIndex = [bgmMenu indexOfItemWithTag:kSeparatorBelowAppVolumesMenuItemTag] - 1;
|
||||
|
||||
// Check each app volume menu item, removing the items that control one of the given apps
|
||||
for (NSInteger i = firstItemIndex; i <= lastItemIndex; i++) {
|
||||
NSMenuItem* item = [bgmMenu itemAtIndex:i];
|
||||
|
||||
for (NSRunningApplication* appToBeRemoved in apps) {
|
||||
NSRunningApplication* itemApp = item.representedObject;
|
||||
|
||||
if ([itemApp isEqual:appToBeRemoved]) {
|
||||
[bgmMenu removeItem:item];
|
||||
// Correct i to account for the item we removed, since we're editing the menu in place
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setVolumeOfMenuItem:(NSMenuItem*)menuItem fromAppVolumes:(CACFArray&)appVolumes {
|
||||
// Set menuItem's volume slider to the volume of the app in appVolumes that menuItem represents
|
||||
// Leaves menuItem unchanged if it doesn't match any of the apps in appVolumes
|
||||
NSRunningApplication* representedApp = menuItem.representedObject;
|
||||
|
||||
for (UInt32 i = 0; i < appVolumes.GetNumberItems(); i++) {
|
||||
CACFDictionary appVolume(false);
|
||||
appVolumes.GetCACFDictionary(i, appVolume);
|
||||
|
||||
// Match the app to the menu item by pid or bundle id
|
||||
CACFString bundleID;
|
||||
bundleID.DontAllowRelease();
|
||||
appVolume.GetCACFString(CFSTR(kBGMAppVolumesKey_BundleID), bundleID);
|
||||
|
||||
pid_t pid;
|
||||
appVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), pid);
|
||||
|
||||
if ((representedApp.processIdentifier == pid) ||
|
||||
[representedApp.bundleIdentifier isEqualToString:(__bridge NSString*)bundleID.GetCFString()]) {
|
||||
CFTypeRef relativeVolume;
|
||||
appVolume.GetCFType(CFSTR(kBGMAppVolumesKey_RelativeVolume), relativeVolume);
|
||||
|
||||
CFTypeRef panPosition;
|
||||
appVolume.GetCFType(CFSTR(kBGMAppVolumesKey_PanPosition), panPosition);
|
||||
|
||||
// Update the slider
|
||||
for (NSView* subview in menuItem.view.subviews) {
|
||||
if ([subview respondsToSelector:@selector(setRelativeVolume:)]) {
|
||||
[subview performSelector:@selector(setRelativeVolume:) withObject:(__bridge NSNumber*)relativeVolume];
|
||||
}
|
||||
if ([subview respondsToSelector:@selector(setPanPosition:)]) {
|
||||
[subview performSelector:@selector(setPanPosition:) withObject:(__bridge NSNumber*)panPosition];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showHideExtraControls:(BGMAVM_ShowMoreControlsButton*)button {
|
||||
// Show or hide an app's extra controls, currently only pan, in its App Volumes menu item.
|
||||
|
||||
NSMenuItem* menuItem = button.cell.representedObject;
|
||||
|
||||
BGMAssert(button, "!button");
|
||||
BGMAssert(menuItem, "!menuItem");
|
||||
|
||||
CGFloat width = menuItem.view.frame.size.width;
|
||||
CGFloat height = menuItem.view.frame.size.height;
|
||||
|
||||
#if DEBUG
|
||||
const char* appName = [((NSRunningApplication*)menuItem.representedObject).localizedName UTF8String];
|
||||
#endif
|
||||
|
||||
auto nearEnough = [](CGFloat x, CGFloat y) { // Shouldn't be necessary, but just in case.
|
||||
return fabs(x - y) < 0.01; // We don't need much precision.
|
||||
};
|
||||
|
||||
if (nearEnough(button.frameCenterRotation, 0.0)) {
|
||||
// Hide extra controls
|
||||
DebugMsg("BGMAppVolumes::showHideExtraControls: Hiding extra controls (%s)", appName);
|
||||
|
||||
BGMAssert(nearEnough(height, appVolumeViewFullHeight), "Extra controls were already hidden");
|
||||
|
||||
// Make the menu item shorter to hide the extra controls. Keep the width unchanged.
|
||||
menuItem.view.frameSize = { width, kAppVolumeViewInitialHeight };
|
||||
// Turn the button upside down so the arrowhead points down.
|
||||
button.frameCenterRotation = 180.0;
|
||||
// Move the button up slightly so it aligns with the volume slider.
|
||||
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y - 1)];
|
||||
} else {
|
||||
// Show extra controls
|
||||
DebugMsg("BGMAppVolumes::showHideExtraControls: Showing extra controls (%s)", appName);
|
||||
|
||||
BGMAssert(nearEnough(button.frameCenterRotation, 180.0), "Unexpected button rotation");
|
||||
BGMAssert(nearEnough(height, kAppVolumeViewInitialHeight), "Extra controls were already shown");
|
||||
|
||||
// Make the menu item taller to show the extra controls. Keep the width unchanged.
|
||||
menuItem.view.frameSize = { width, appVolumeViewFullHeight };
|
||||
// Turn the button rightside up so the arrowhead points up.
|
||||
button.frameCenterRotation = 0.0;
|
||||
// Move the button down slightly, back to it's original position.
|
||||
[button setFrameOrigin:NSMakePoint(button.frame.origin.x, button.frame.origin.y + 1)];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark KVO
|
||||
|
||||
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
#pragma unused (object, context)
|
||||
|
||||
// KVO callback for the apps currently running on the system. Adds/removes the associated menu items.
|
||||
if ([keyPath isEqualToString:@"runningApplications"]) {
|
||||
NSArray<NSRunningApplication*>* newApps = [change objectForKey:NSKeyValueChangeNewKey];
|
||||
NSArray<NSRunningApplication*>* oldApps = [change objectForKey:NSKeyValueChangeOldKey];
|
||||
|
||||
int changeKind = [[change valueForKey:NSKeyValueChangeKindKey] intValue];
|
||||
switch (changeKind) {
|
||||
case NSKeyValueChangeInsertion:
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeRemoval:
|
||||
[self removeMenuItemsForApps:oldApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeReplacement:
|
||||
[self removeMenuItemsForApps:oldApps];
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeSetting:
|
||||
[bgmMenu removeAllItems];
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark BGMDevice Communication
|
||||
|
||||
- (void) sendVolumeChangeToBGMDevice:(SInt32)newVolume appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
|
||||
CACFDictionary appVolumeChange(true);
|
||||
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
|
||||
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), (__bridge CFStringRef)appBundleID);
|
||||
// The values from our sliders are in [kAppRelativeVolumeMinRawValue, kAppRelativeVolumeMaxRawValue] already
|
||||
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume), newVolume);
|
||||
|
||||
CACFArray appVolumeChanges(true);
|
||||
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
|
||||
|
||||
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
|
||||
}
|
||||
|
||||
- (void) sendPanPositionChangeToBGMDevice:(SInt32)newPanPosition appProcessID:(pid_t)appProcessID appBundleID:(NSString*)appBundleID {
|
||||
CACFDictionary appVolumeChange(true);
|
||||
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), appProcessID);
|
||||
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), (__bridge CFStringRef)appBundleID);
|
||||
|
||||
// The values from our sliders are in [kAppPanLeftRawValue, kAppPanRightRawValue] already
|
||||
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_PanPosition), newPanPosition);
|
||||
|
||||
CACFArray appVolumeChanges(true);
|
||||
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
|
||||
|
||||
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMAppVolumesAddress, appVolumeChanges.AsPropertyList());
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark Custom Classes (IB)
|
||||
|
||||
// Custom classes for the UI elements in the app volume menu items
|
||||
|
||||
@implementation BGMAVM_AppIcon
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
self.image = app.icon;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_AppNameLabel
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (ctx, menuItem)
|
||||
|
||||
NSString* name = app.localizedName ? (NSString*)app.localizedName : @"";
|
||||
self.stringValue = name;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_ShowMoreControlsButton
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (app)
|
||||
|
||||
// Set up the button that show/hide the extra controls (currently only a pan slider) for the app.
|
||||
self.cell.representedObject = menuItem;
|
||||
self.target = ctx;
|
||||
self.action = @selector(showHideExtraControls:);
|
||||
|
||||
// The menu item starts out with the extra controls visible, so we hide them here.
|
||||
//
|
||||
// TODO: Leave them visible if any of the controls are set to non-default values. The user has no way to
|
||||
// tell otherwise. Maybe we should also make this button look different if the controls are hidden
|
||||
// when they have non-default values.
|
||||
[ctx showHideExtraControls:self];
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = @"More options";
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_VolumeSlider {
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* appBundleID;
|
||||
BGMAppVolumes* context;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (menuItem)
|
||||
|
||||
context = ctx;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appVolumeChanged);
|
||||
|
||||
appProcessID = app.processIdentifier;
|
||||
appBundleID = app.bundleIdentifier;
|
||||
|
||||
self.maxValue = kAppRelativeVolumeMaxRawValue;
|
||||
self.minValue = kAppRelativeVolumeMinRawValue;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Volume for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
// We have to handle snapping for volume sliders ourselves because adding a tick mark (snap point) in Interface Builder
|
||||
// changes how the slider looks.
|
||||
- (void) snap {
|
||||
// Snap to the 50% point.
|
||||
float midPoint = static_cast<float>((self.maxValue + self.minValue) / 2);
|
||||
if (self.floatValue > (midPoint - kSlidersSnapWithin) && self.floatValue < (midPoint + kSlidersSnapWithin)) {
|
||||
self.floatValue = midPoint;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setRelativeVolume:(NSNumber*)relativeVolume {
|
||||
self.intValue = relativeVolume.intValue;
|
||||
[self snap];
|
||||
}
|
||||
|
||||
- (void) appVolumeChanged {
|
||||
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
|
||||
|
||||
DebugMsg("BGMAppVolumes::appVolumeChanged: App volume for %s changed to %d", appBundleID.UTF8String, self.intValue);
|
||||
|
||||
[self snap];
|
||||
|
||||
[context sendVolumeChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation BGMAVM_PanSlider {
|
||||
// Will be set to -1 for apps without a pid
|
||||
pid_t appProcessID;
|
||||
NSString* appBundleID;
|
||||
BGMAppVolumes* context;
|
||||
}
|
||||
|
||||
- (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx menuItem:(NSMenuItem*)menuItem {
|
||||
#pragma unused (menuItem)
|
||||
|
||||
context = ctx;
|
||||
|
||||
self.target = self;
|
||||
self.action = @selector(appPanPositionChanged);
|
||||
|
||||
appProcessID = app.processIdentifier;
|
||||
appBundleID = app.bundleIdentifier;
|
||||
|
||||
self.minValue = kAppPanLeftRawValue;
|
||||
self.maxValue = kAppPanRightRawValue;
|
||||
|
||||
if ([self respondsToSelector:@selector(setAccessibilityTitle:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
self.accessibilityTitle = [NSString stringWithFormat:@"Pan for %@", [app localizedName]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setPanPosition:(NSNumber *)panPosition {
|
||||
self.intValue = panPosition.intValue;
|
||||
}
|
||||
|
||||
- (void) appPanPositionChanged {
|
||||
// TODO: This (sending updates to the driver) should probably be rate-limited. It uses a fair bit of CPU for me.
|
||||
|
||||
DebugMsg("BGMAppVolumes::appPanPositionChanged: App pan position for %s changed to %d", appBundleID.UTF8String, self.intValue);
|
||||
|
||||
[context sendPanPositionChangeToBGMDevice:self.intValue appProcessID:appProcessID appBundleID:appBundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMAppVolumesController.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMAppVolumesController : NSObject
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)audioDevices;
|
||||
|
||||
// See BGMBackgroundMusicDevice::SetAppVolume.
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
// See BGMBackgroundMusicDevice::SetPanVolume.
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMAppVolumesController.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017, 2018 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAppVolumesController.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAppVolumes.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CACFArray.h"
|
||||
#import "CACFDictionary.h"
|
||||
#import "CACFString.h"
|
||||
|
||||
// System Includes
|
||||
#include <libproc.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
typedef struct BGMAppVolumeAndPan {
|
||||
int volume;
|
||||
int pan;
|
||||
} BGMAppVolumeAndPan;
|
||||
|
||||
@implementation BGMAppVolumesController {
|
||||
// The App Volumes UI.
|
||||
BGMAppVolumes* appVolumes;
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
}
|
||||
|
||||
#pragma mark Initialisation
|
||||
|
||||
- (id) initWithMenu:(NSMenu*)menu
|
||||
appVolumeView:(NSView*)view
|
||||
audioDevices:(BGMAudioDeviceManager*)devices {
|
||||
if ((self = [super init])) {
|
||||
audioDevices = devices;
|
||||
appVolumes = [[BGMAppVolumes alloc] initWithController:self
|
||||
bgmMenu:menu
|
||||
appVolumeView:view];
|
||||
|
||||
// Create the menu items for controlling app volumes.
|
||||
NSArray<NSRunningApplication*>* apps = [[NSWorkspace sharedWorkspace] runningApplications];
|
||||
[self insertMenuItemsForApps:apps];
|
||||
|
||||
// Register for notifications when the user opens or closes apps, so we can update the menu.
|
||||
auto opts = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
|
||||
[[NSWorkspace sharedWorkspace] addObserver:self
|
||||
forKeyPath:@"runningApplications"
|
||||
options:opts
|
||||
context:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[[NSWorkspace sharedWorkspace] removeObserver:self
|
||||
forKeyPath:@"runningApplications"
|
||||
context:nil];
|
||||
}
|
||||
|
||||
// Adds a volume control menu item for each given app.
|
||||
- (void) insertMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
|
||||
NSAssert([NSThread isMainThread], @"insertMenuItemsForApps is not thread safe");
|
||||
|
||||
// TODO: Handle the C++ exceptions this method can throw. They can cause crashes because this
|
||||
// method is called in a KVO handler.
|
||||
|
||||
// Get the app volumes currently set on the device
|
||||
CACFArray volumesFromBGMDevice([audioDevices bgmDevice].GetAppVolumes(), false);
|
||||
|
||||
for (NSRunningApplication* app in apps) {
|
||||
if ([self shouldBeIncludedInMenu:app]) {
|
||||
BGMAppVolumeAndPan initial = [self getVolumeAndPanForApp:app
|
||||
fromVolumes:volumesFromBGMDevice];
|
||||
[appVolumes insertMenuItemForApp:app
|
||||
initialVolume:initial.volume
|
||||
initialPan:initial.pan];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app
|
||||
fromVolumes:(const CACFArray&)volumes {
|
||||
BGMAppVolumeAndPan volumeAndPan = {
|
||||
.volume = -1,
|
||||
.pan = -1
|
||||
};
|
||||
|
||||
for (UInt32 i = 0; i < volumes.GetNumberItems(); i++) {
|
||||
CACFDictionary appVolume(false);
|
||||
volumes.GetCACFDictionary(i, appVolume);
|
||||
|
||||
// Match the app to the volume/pan by pid or bundle ID.
|
||||
CACFString bundleID;
|
||||
bundleID.DontAllowRelease();
|
||||
appVolume.GetCACFString(CFSTR(kBGMAppVolumesKey_BundleID), bundleID);
|
||||
|
||||
pid_t pid;
|
||||
appVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), pid);
|
||||
|
||||
if ((app.processIdentifier == pid) ||
|
||||
[app.bundleIdentifier isEqualToString:(__bridge NSString*)bundleID.GetCFString()]) {
|
||||
// Found a match, so read the volume and pan.
|
||||
appVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_RelativeVolume), volumeAndPan.volume);
|
||||
appVolume.GetSInt32(CFSTR(kBGMAppVolumesKey_PanPosition), volumeAndPan.pan);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return volumeAndPan;
|
||||
}
|
||||
|
||||
- (BOOL) shouldBeIncludedInMenu:(NSRunningApplication*)app {
|
||||
// Ignore hidden apps and Background Music itself.
|
||||
// TODO: Would it be better to only show apps that are registered as HAL clients?
|
||||
BOOL isHidden = app.activationPolicy != NSApplicationActivationPolicyRegular &&
|
||||
app.activationPolicy != NSApplicationActivationPolicyAccessory;
|
||||
|
||||
NSString* bundleID = app.bundleIdentifier;
|
||||
BOOL isBGMApp = bundleID && [@kBGMAppBundleID isEqualToString:BGMNN(bundleID)];
|
||||
|
||||
return !isHidden && !isBGMApp;
|
||||
}
|
||||
|
||||
- (void) removeMenuItemsForApps:(NSArray<NSRunningApplication*>*)apps {
|
||||
NSAssert([NSThread isMainThread], @"removeMenuItemsForApps is not thread safe");
|
||||
|
||||
for (NSRunningApplication* app in apps) {
|
||||
[appVolumes removeMenuItemForApp:app];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
- (void) setVolume:(SInt32)volume
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
// Update the app's volume.
|
||||
audioDevices.bgmDevice.SetAppVolume(volume, processID, (__bridge_retained CFStringRef)bundleID);
|
||||
|
||||
// If this volume is for FaceTime, set the volume for the avconferenced process as well. This
|
||||
// works around FaceTime not playing its own audio. It plays UI sounds through
|
||||
// systemsoundserverd and call audio through avconferenced.
|
||||
//
|
||||
// This isn't ideal because other apps might play audio through avconferenced, but I don't see a
|
||||
// good way we could find out which app is actually playing the audio. We could probably figure
|
||||
// it out from reading avconferenced's logs, at least, if it turns out to be important. See
|
||||
// https://github.com/kyleneideck/BackgroundMusic/issues/139.
|
||||
if ([bundleID isEqual:@"com.apple.FaceTime"]) {
|
||||
[self setAvconferencedVolume:volume];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setAvconferencedVolume:(SInt32)volume {
|
||||
// TODO: This volume will be lost if avconferenced is restarted.
|
||||
pid_t pids[1024];
|
||||
size_t procCount = proc_listallpids(pids, 1024);
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
|
||||
for (int i = 0; i < procCount; i++) {
|
||||
pid_t pid = pids[i];
|
||||
|
||||
if (proc_pidpath(pid, path, sizeof(path)) > 0 &&
|
||||
strncmp(path, "/usr/libexec/avconferenced", sizeof(path)) == 0) {
|
||||
DebugMsg("Setting avconferenced volume: %d", volume);
|
||||
audioDevices.bgmDevice.SetAppVolume(volume, pid, nullptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LogWarning("Failed to set avconferenced volume.");
|
||||
}
|
||||
|
||||
- (void) setPanPosition:(SInt32)pan
|
||||
forAppWithProcessID:(pid_t)processID
|
||||
bundleID:(NSString* __nullable)bundleID {
|
||||
audioDevices.bgmDevice.SetAppPanPosition(pan,
|
||||
processID,
|
||||
(__bridge_retained CFStringRef)bundleID);
|
||||
}
|
||||
|
||||
#pragma mark KVO
|
||||
|
||||
- (void) observeValueForKeyPath:(NSString* __nullable)keyPath
|
||||
ofObject:(id __nullable)object
|
||||
change:(NSDictionary* __nullable)change
|
||||
context:(void* __nullable)context
|
||||
{
|
||||
#pragma unused (object, context)
|
||||
|
||||
// KVO callback for the apps currently running on the system. Adds/removes the associated menu
|
||||
// items.
|
||||
if (keyPath && change && [keyPath isEqualToString:@"runningApplications"]) {
|
||||
NSArray<NSRunningApplication*>* newApps = change[NSKeyValueChangeNewKey];
|
||||
NSArray<NSRunningApplication*>* oldApps = change[NSKeyValueChangeOldKey];
|
||||
|
||||
int changeKind = [change[NSKeyValueChangeKindKey] intValue];
|
||||
|
||||
switch (changeKind) {
|
||||
case NSKeyValueChangeInsertion:
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeRemoval:
|
||||
[self removeMenuItemsForApps:oldApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeReplacement:
|
||||
[self removeMenuItemsForApps:oldApps];
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
|
||||
case NSKeyValueChangeSetting:
|
||||
[appVolumes removeAllAppVolumeMenuItems];
|
||||
[self insertMenuItemsForApps:newApps];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -30,10 +30,6 @@
|
||||
#include <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
|
||||
// master element."
|
||||
static const AudioObjectPropertyElement kMasterChannel = 0;
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMAudioDevice::BGMAudioDevice(AudioObjectID inAudioDevice)
|
||||
@@ -61,14 +57,17 @@ BGMAudioDevice::~BGMAudioDevice()
|
||||
bool BGMAudioDevice::CanBeOutputDeviceInBGMApp() const
|
||||
{
|
||||
CFStringRef uid = CopyDeviceUID();
|
||||
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
|
||||
bool isNullDevice = CFEqual(uid, CFSTR(kBGMNullDeviceUID));
|
||||
CFRelease(uid);
|
||||
|
||||
bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0;
|
||||
bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
|
||||
|
||||
return !isBGMDevice && !isNullDevice && !IsHidden() && hasOutputChannels && canBeDefault;
|
||||
return !IsBGMDeviceInstance() &&
|
||||
!isNullDevice &&
|
||||
!IsHidden() &&
|
||||
hasOutputChannels &&
|
||||
canBeDefault;
|
||||
}
|
||||
|
||||
#pragma mark Available Controls
|
||||
@@ -331,6 +330,27 @@ bool BGMAudioDevice::GetVirtualMasterBalance(AudioObjectPropertyScope inScope
|
||||
&outVirtualMasterBalance);
|
||||
}
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
bool BGMAudioDevice::IsBGMDevice(bool inIncludeUISoundsInstance) const
|
||||
{
|
||||
bool isBGMDevice = false;
|
||||
|
||||
if(GetObjectID() != kAudioObjectUnknown)
|
||||
{
|
||||
// Check the device's UID to see whether it's BGMDevice.
|
||||
CFStringRef uid = CopyDeviceUID();
|
||||
|
||||
isBGMDevice =
|
||||
CFEqual(uid, CFSTR(kBGMDeviceUID)) ||
|
||||
(inIncludeUISoundsInstance && CFEqual(uid, CFSTR(kBGMDeviceUID_UISounds)));
|
||||
|
||||
CFRelease(uid);
|
||||
}
|
||||
|
||||
return isBGMDevice;
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus BGMAudioDevice::AHSGetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
|
||||
@@ -37,6 +37,13 @@ class BGMAudioDevice
|
||||
|
||||
public:
|
||||
BGMAudioDevice(AudioObjectID inAudioDevice);
|
||||
/*!
|
||||
Creates a BGMAudioDevice with the Audio Object ID of the device whose UID is inUID or, if no
|
||||
such device is found, kAudioObjectUnknown.
|
||||
|
||||
@throws CAException If the HAL returns an error when queried for the device's ID.
|
||||
@see kAudioPlugInPropertyTranslateUIDToDevice in AudioHardwareBase.h.
|
||||
*/
|
||||
BGMAudioDevice(CFStringRef inUID);
|
||||
BGMAudioDevice(const CAHALAudioDevice& inDevice);
|
||||
virtual ~BGMAudioDevice();
|
||||
@@ -51,7 +58,23 @@ public:
|
||||
|
||||
operator AudioObjectID() const { return GetObjectID(); }
|
||||
|
||||
/*! @throws CAException */
|
||||
/*!
|
||||
@return True if this device is BGMDevice. (Specifically, the main instance of BGMDevice.)
|
||||
@throws CAException If the HAL returns an error when queried.
|
||||
*/
|
||||
bool IsBGMDevice() const { return IsBGMDevice(false); };
|
||||
/*!
|
||||
@return True if this device is either the main instance of BGMDevice (the device named
|
||||
"Background Music") or the instance used for UI sounds (the device named "Background
|
||||
Music (UI Sounds)").
|
||||
@throws CAException If the HAL returns an error when queried.
|
||||
*/
|
||||
bool IsBGMDeviceInstance() const { return IsBGMDevice(true); };
|
||||
|
||||
/*!
|
||||
@return True if this device can be set as the output device in BGMApp.
|
||||
@throws CAException If the HAL returns an error when queried.
|
||||
*/
|
||||
bool CanBeOutputDeviceInBGMApp() const;
|
||||
|
||||
#pragma mark Available Controls
|
||||
@@ -77,7 +100,11 @@ public:
|
||||
bool GetVirtualMasterBalance(AudioObjectPropertyScope inScope,
|
||||
Float32& outVirtualMasterBalance) const;
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
private:
|
||||
bool IsBGMDevice(bool inIncludingUISoundsInstance) const;
|
||||
|
||||
static OSStatus AHSGetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32* ioDataSize,
|
||||
|
||||
@@ -17,32 +17,43 @@
|
||||
// BGMAudioDeviceManager.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Manages the BGMDevice and the output device. Sets the system's current default device as the
|
||||
// output device on init, then starts playthrough and mirroring the devices' controls. The output
|
||||
// device can be changed but the BGMDevice is fixed.
|
||||
// Manages 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.
|
||||
//
|
||||
|
||||
#if defined(__cplusplus)
|
||||
|
||||
// Local Includes
|
||||
#import "BGMBackgroundMusicDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#ifdef __cplusplus
|
||||
#import "CAHALAudioDevice.h"
|
||||
#endif
|
||||
|
||||
#endif /* defined(__cplusplus) */
|
||||
|
||||
// System Includes
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreAudio/AudioHardwareBase.h>
|
||||
|
||||
// Forward Declarations
|
||||
@class BGMOutputVolumeMenuItem;
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
extern int const kBGMErrorCode_BGMDeviceNotFound;
|
||||
extern int const kBGMErrorCode_OutputDeviceNotFound;
|
||||
static const int kBGMErrorCode_BGMDeviceNotFound = 1;
|
||||
static const int kBGMErrorCode_OutputDeviceNotFound = 2;
|
||||
static const int kBGMErrorCode_ReturningEarly = 3;
|
||||
|
||||
@interface BGMAudioDeviceManager : NSObject
|
||||
|
||||
- (instancetype) initWithError:(NSError**)error;
|
||||
|
||||
// Set the BGMOutputVolumeMenuItem to be notified when the output device is changed.
|
||||
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item;
|
||||
|
||||
// Set BGMDevice as the default audio device for all processes
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
||||
// Replace BGMDevice as the default device with the output device
|
||||
@@ -50,7 +61,7 @@ extern int const kBGMErrorCode_OutputDeviceNotFound;
|
||||
|
||||
#ifdef __cplusplus
|
||||
// The virtual device published by BGMDriver.
|
||||
- (CAHALAudioDevice) bgmDevice;
|
||||
- (BGMBackgroundMusicDevice) bgmDevice;
|
||||
|
||||
// The device BGMApp will play audio through, making it, from the user's perspective, the system's
|
||||
// default output device.
|
||||
@@ -79,8 +90,16 @@ extern int const kBGMErrorCode_OutputDeviceNotFound;
|
||||
dataSourceID:(UInt32)dataSourceID
|
||||
revertOnFailure:(BOOL)revertOnFailure;
|
||||
|
||||
// Blocks until IO has started running on the output device (for playthrough).
|
||||
- (OSStatus) waitForOutputDeviceToStart;
|
||||
// Start playthrough synchronously. Blocks until IO has started on the output device and playthrough
|
||||
// is running. See BGMPlayThrough.
|
||||
//
|
||||
// Returns one of the error codes defined by this class or BGMPlayThrough, or an AudioHardware error
|
||||
// code received from the HAL.
|
||||
- (OSStatus) startPlayThroughSync:(BOOL)forUISoundsDevice;
|
||||
|
||||
// When the output device is changed, BGMAudioDeviceManager will send the ID of the new output
|
||||
// device to BGMXPCHelper through this connection.
|
||||
- (void) setBGMXPCHelperConnection:(NSXPCConnection* __nullable)connection;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -17,47 +17,65 @@
|
||||
// BGMAudioDeviceManager.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
#include "BGMDeviceControlSync.h"
|
||||
#include "BGMPlayThrough.h"
|
||||
#include "BGMAudioDevice.h"
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMDeviceControlSync.h"
|
||||
#import "BGMPlayThrough.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
#import "BGMXPCProtocols.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAAutoDisposer.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAAtomic.h"
|
||||
|
||||
|
||||
int const kBGMErrorCode_BGMDeviceNotFound = 0;
|
||||
int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMAudioDeviceManager {
|
||||
BGMAudioDevice bgmDevice;
|
||||
// This ivar is a pointer so that BGMBackgroundMusicDevice's constructor doesn't get called
|
||||
// during [BGMAudioDeviceManager alloc] when the ivars are initialised. It queries the HAL for
|
||||
// BGMDevice's AudioObject ID, which might throw a CAException, most likely because BGMDevice
|
||||
// isn't installed.
|
||||
//
|
||||
// That would be the only way for [BGMAudioDeviceManager alloc] to throw a CAException, so we
|
||||
// could wrap that call in a try/catch block instead, but it would make the code a bit
|
||||
// confusing.
|
||||
BGMBackgroundMusicDevice* bgmDevice;
|
||||
BGMAudioDevice outputDevice;
|
||||
|
||||
BGMDeviceControlSync deviceControlSync;
|
||||
BGMPlayThrough playThrough;
|
||||
|
||||
BGMPlayThrough playThrough_UISounds;
|
||||
|
||||
// A connection to BGMXPCHelper so we can send it the ID of the output device.
|
||||
NSXPCConnection* __nullable bgmXPCHelperConnection;
|
||||
|
||||
BGMOutputVolumeMenuItem* __nullable outputVolumeMenuItem;
|
||||
|
||||
NSRecursiveLock* stateLock;
|
||||
}
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
- (id) initWithError:(NSError**)error {
|
||||
- (instancetype) initWithError:(NSError** __nullable)error {
|
||||
if ((self = [super init])) {
|
||||
stateLock = [NSRecursiveLock new];
|
||||
|
||||
bgmDevice = BGMAudioDevice(CFSTR(kBGMDeviceUID));
|
||||
|
||||
if (bgmDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found");
|
||||
bgmXPCHelperConnection = nil;
|
||||
outputVolumeMenuItem = nil;
|
||||
|
||||
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];
|
||||
@@ -66,12 +84,13 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
[self initOutputDevice];
|
||||
|
||||
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: output device not found");
|
||||
|
||||
|
||||
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];
|
||||
}
|
||||
@@ -84,180 +103,146 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
if (bgmDevice) {
|
||||
delete bgmDevice;
|
||||
bgmDevice = nullptr;
|
||||
}
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
// Throws a CAException if it fails to set the output device.
|
||||
- (void) initOutputDevice {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
|
||||
AudioObjectID defaultDeviceID = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
|
||||
if (defaultDeviceID == bgmDevice.GetObjectID()) {
|
||||
// TODO: If BGMDevice is already the default (because BGMApp didn't shutdown properly or it was set manually)
|
||||
// we should temporarily disable BGMDevice so we can find out what the previous default was.
|
||||
|
||||
// For now, just pick the device with the lowest latency
|
||||
UInt32 numDevices = audioSystem.GetNumberAudioDevices();
|
||||
if (numDevices > 0) {
|
||||
SInt32 minLatencyDeviceIdx = -1;
|
||||
UInt32 minLatency = UINT32_MAX;
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
for (UInt32 i = 0; i < numDevices; i++) {
|
||||
BGMAudioDevice device(devices[i]);
|
||||
|
||||
BOOL isBGMDevice = device.GetObjectID() == bgmDevice.GetObjectID();
|
||||
BOOL hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
|
||||
|
||||
if (!isBGMDevice && hasOutputChannels) {
|
||||
if (minLatencyDeviceIdx == -1) {
|
||||
// First, look for any device other than BGMDevice
|
||||
minLatencyDeviceIdx = i;
|
||||
} else if (device.GetLatency(false) < minLatency) {
|
||||
// Then compare the devices by their latencies
|
||||
minLatencyDeviceIdx = i;
|
||||
minLatency = device.GetLatency(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
|
||||
"setOutputDeviceWithID:devices[minLatencyDeviceIdx]", [&]() {
|
||||
// TODO: On error, try a different output device.
|
||||
[self setOutputDeviceWithID:devices[minLatencyDeviceIdx] revertOnFailure:NO];
|
||||
});
|
||||
}
|
||||
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 {
|
||||
BGMLogUnexpectedExceptionsMsg("BGMAudioDeviceManager::initOutputDevice",
|
||||
"setOutputDeviceWithID:defaultDeviceID", [&]() {
|
||||
// TODO: Return the error from setOutputDeviceWithID so it can be returned by initWithError.
|
||||
[self setOutputDeviceWithID:defaultDeviceID revertOnFailure:NO];
|
||||
});
|
||||
// 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));
|
||||
}
|
||||
|
||||
assert(outputDevice.GetObjectID() != bgmDevice.GetObjectID());
|
||||
|
||||
// Log message
|
||||
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
|
||||
DebugMsg("BGMAudioDeviceManager::initDevices: Set output device to %s",
|
||||
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
|
||||
CFRelease(outputDeviceUID);
|
||||
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;
|
||||
}
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
// Note that there are two different "default" output devices on OS X: "output" and "system output". See
|
||||
// AudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h.
|
||||
// kAudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h.
|
||||
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault {
|
||||
DebugMsg("BGMAudioDeviceManager::setBGMDeviceAsOSDefault: Setting the system's default audio "
|
||||
"device to BGMDevice");
|
||||
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
if (bgmDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
try {
|
||||
AudioDeviceID currentDefault = audioSystem.GetDefaultAudioDevice(false, true);
|
||||
|
||||
try {
|
||||
if (currentDefault == outputDeviceID) {
|
||||
// The default system device was the same as the default device, so change that as well
|
||||
audioSystem.SetDefaultAudioDevice(false, true, bgmDeviceID);
|
||||
}
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, false, bgmDeviceID);
|
||||
} catch (CAException e) {
|
||||
NSLog(@"SetDefaultAudioDevice threw CAException (%d)", e.GetError());
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
}
|
||||
} catch (...) {
|
||||
NSLog(@"Unexpected exception");
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:-1 userInfo:nil];
|
||||
// Intentionally avoid taking stateLock before making calls to the HAL. See
|
||||
// startPlayThroughSync.
|
||||
CAMemoryBarrier();
|
||||
bgmDevice->SetAsOSDefault();
|
||||
} catch (const CAException& e) {
|
||||
BGMLogExceptionIn("BGMAudioDeviceManager::setBGMDeviceAsOSDefault", e);
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
}
|
||||
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSError* __nullable) unsetBGMDeviceAsOSDefault {
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
bool bgmDeviceIsDefault = true;
|
||||
bool bgmDeviceIsSystemDefault = true;
|
||||
|
||||
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
|
||||
|
||||
// Copy the devices so we can call the HAL without holding stateLock. See startPlayThroughSync.
|
||||
BGMBackgroundMusicDevice* bgmDeviceCopy;
|
||||
AudioDeviceID outputDeviceID;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
bgmDeviceCopy = bgmDevice;
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
|
||||
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&]() {
|
||||
bgmDeviceIsDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, false) == bgmDeviceID);
|
||||
|
||||
bgmDeviceIsSystemDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, true) == bgmDeviceID);
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
if (outputDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID
|
||||
code:kBGMErrorCode_OutputDeviceNotFound
|
||||
userInfo:nil];
|
||||
}
|
||||
if (bgmDeviceID == kAudioObjectUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
if (bgmDeviceIsDefault) {
|
||||
DebugMsg("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault: Setting the system's default output "
|
||||
"device back to device %d", outputDeviceID);
|
||||
|
||||
try {
|
||||
audioSystem.SetDefaultAudioDevice(false, false, outputDeviceID);
|
||||
} catch (CAException e) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
} catch (...) {
|
||||
BGMLogUnexpectedExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault "
|
||||
"SetDefaultAudioDevice (output)");
|
||||
}
|
||||
}
|
||||
|
||||
// If we changed the default system output device to BGMDevice, which we only do if it's set to
|
||||
// the same device as the default output device, change it back to the previous device.
|
||||
if (bgmDeviceIsSystemDefault) {
|
||||
DebugMsg("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault: Setting the system's default system "
|
||||
"output device back to device %d", outputDeviceID);
|
||||
|
||||
try {
|
||||
audioSystem.SetDefaultAudioDevice(false, true, outputDeviceID);
|
||||
} catch (CAException e) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
} catch (...) {
|
||||
BGMLogUnexpectedExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault "
|
||||
"SetDefaultAudioDevice (system output)");
|
||||
}
|
||||
|
||||
try {
|
||||
bgmDeviceCopy->UnsetAsOSDefault(outputDeviceID);
|
||||
} catch (const CAException& e) {
|
||||
BGMLogExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault", e);
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil];
|
||||
}
|
||||
|
||||
return nil;
|
||||
@@ -265,8 +250,8 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
- (CAHALAudioDevice) bgmDevice {
|
||||
return bgmDevice;
|
||||
- (BGMBackgroundMusicDevice) bgmDevice {
|
||||
return *bgmDevice;
|
||||
}
|
||||
|
||||
- (CAHALAudioDevice) outputDevice {
|
||||
@@ -283,6 +268,8 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
}
|
||||
|
||||
- (BOOL) isOutputDataSource:(UInt32)dataSourceID {
|
||||
BOOL isOutputDataSource = NO;
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
@@ -290,17 +277,20 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
|
||||
UInt32 channel = 0;
|
||||
|
||||
return outputDevice.HasDataSourceControl(scope, channel) &&
|
||||
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
|
||||
} catch (CAException e) {
|
||||
isOutputDataSource =
|
||||
outputDevice.HasDataSourceControl(scope, channel) &&
|
||||
(dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel));
|
||||
} catch (const CAException& e) {
|
||||
BGMLogException(e);
|
||||
return false;
|
||||
}
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
return isOutputDataSource;
|
||||
}
|
||||
|
||||
#pragma mark Output Device
|
||||
|
||||
- (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID
|
||||
revertOnFailure:(BOOL)revertOnFailure {
|
||||
@@ -320,35 +310,18 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
- (NSError* __nullable) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID
|
||||
dataSourceID:(UInt32* __nullable)dataSourceID
|
||||
revertOnFailure:(BOOL)revertOnFailure {
|
||||
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting output device. newDeviceID=%u",
|
||||
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Setting output device. newDeviceID=%u",
|
||||
newDeviceID);
|
||||
|
||||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (GetObjectID doesn't throw.)
|
||||
|
||||
// Set up playthrough and control sync
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
|
||||
AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.)
|
||||
|
||||
try {
|
||||
// Re-read the device ID after entering the monitor. (The initial read is because
|
||||
// currentDeviceID is used in the catch blocks.)
|
||||
currentDeviceID = outputDevice.GetObjectID();
|
||||
|
||||
if (newDeviceID != currentDeviceID) {
|
||||
// Deactivate playthrough rather than stopping it so it can't be started by HAL
|
||||
// notifications while we're updating deviceControlSync.
|
||||
playThrough.Deactivate();
|
||||
|
||||
deviceControlSync.SetDevices(bgmDevice, newOutputDevice);
|
||||
deviceControlSync.Activate();
|
||||
|
||||
// Stream audio from BGMDevice to the new output device. This blocks while the old device
|
||||
// stops IO.
|
||||
playThrough.SetDevices(&bgmDevice, &newOutputDevice);
|
||||
playThrough.Activate();
|
||||
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
[self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice];
|
||||
outputDevice = newOutputDevice;
|
||||
}
|
||||
|
||||
@@ -364,10 +337,12 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
// playing. (If we only changed the data source, playthrough will already be running if it
|
||||
// needs to be.)
|
||||
playThrough.Start();
|
||||
playThrough_UISounds.Start();
|
||||
// But stop playthrough if audio isn't playing, since it uses CPU.
|
||||
playThrough.StopIfIdle();
|
||||
playThrough_UISounds.StopIfIdle();
|
||||
}
|
||||
} catch (CAException e) {
|
||||
} catch (const CAException& e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
@@ -379,6 +354,8 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
|
||||
[self propagateOutputDeviceChange];
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
@@ -386,8 +363,31 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice)device {
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", [&]() {
|
||||
// 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 {
|
||||
// Deactivate playthrough rather than stopping it so it can't be started by HAL notifications
|
||||
// while we're updating deviceControlSync.
|
||||
playThrough.Deactivate();
|
||||
playThrough_UISounds.Deactivate();
|
||||
|
||||
deviceControlSync.SetDevices(*bgmDevice, newOutputDevice);
|
||||
deviceControlSync.Activate();
|
||||
|
||||
// Stream audio from BGMDevice to the new output device. This blocks while the old device stops
|
||||
// IO.
|
||||
playThrough.SetDevices(bgmDevice, &newOutputDevice);
|
||||
playThrough.Activate();
|
||||
|
||||
// TODO: Support setting different devices as the default output device and the default system
|
||||
// output device the way OS X does?
|
||||
BGMAudioDevice uiSoundsDevice = bgmDevice->GetUISoundsBGMDeviceInstance();
|
||||
playThrough_UISounds.SetDevices(&uiSoundsDevice, &newOutputDevice);
|
||||
playThrough_UISounds.Activate();
|
||||
}
|
||||
|
||||
- (void) setDataSource:(UInt32)dataSourceID device:(BGMAudioDevice&)device {
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", ([&] {
|
||||
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
|
||||
UInt32 channel = 0;
|
||||
|
||||
@@ -397,13 +397,22 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
|
||||
device.SetCurrentDataSourceByID(scope, channel, dataSourceID);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) propagateOutputDeviceChange {
|
||||
// Tell BGMXPCHelper that the output device has changed.
|
||||
[self sendOutputDeviceToBGMXPCHelper];
|
||||
|
||||
// Update the menu item for the volume of the output device.
|
||||
[outputVolumeMenuItem outputDeviceDidChange];
|
||||
}
|
||||
|
||||
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID
|
||||
errorCode:(OSStatus)errorCode
|
||||
revertTo:(AudioDeviceID*)revertTo {
|
||||
// Using LogWarning from PublicUtility instead of NSLog here crashes from a bad access. Not sure why.
|
||||
// TODO: Possibly caused by a bug in CADebugMacros.cpp. See commit ab9d4cd.
|
||||
NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Couldn't set device with ID %u as output device. "
|
||||
"%s%d. %@",
|
||||
deviceID,
|
||||
@@ -429,9 +438,9 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:errorCode userInfo:info];
|
||||
}
|
||||
|
||||
- (OSStatus) waitForOutputDeviceToStart {
|
||||
- (OSStatus) startPlayThroughSync:(BOOL)forUISoundsDevice {
|
||||
// We can only try for stateLock because setOutputDeviceWithID might have already taken it, then made a
|
||||
// HAL request to BGMDevice and is now waiting for the response. Some of the requests setOutputDeviceWithID
|
||||
// HAL request to BGMDevice and be waiting for the response. Some of the requests setOutputDeviceWithID
|
||||
// makes to BGMDevice block in the HAL if another thread is in BGM_Device::StartIO.
|
||||
//
|
||||
// Since BGM_Device::StartIO calls this method (via XPC), waiting for setOutputDeviceWithID to release
|
||||
@@ -445,27 +454,40 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
gotLock = [stateLock tryLock];
|
||||
|
||||
if (gotLock) {
|
||||
err = playThrough.WaitForOutputDeviceToStart();
|
||||
} else {
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Didn't get state lock. Returning "
|
||||
"early with kDeviceNotStarting.");
|
||||
err = BGMPlayThrough::kDeviceNotStarting;
|
||||
}
|
||||
|
||||
if (err == BGMPlayThrough::kDeviceNotStarting) {
|
||||
// I'm not sure if this block is currently reachable, but BGMDriver only starts waiting on the
|
||||
// output device when IO is starting, so we should start playthrough even if BGMApp hasn't been
|
||||
// notified by the HAL yet.
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Playthrough wasn't starting the "
|
||||
"output device. Will tell it to and then return early with kDeviceNotStarting.");
|
||||
BGMPlayThrough& pt = (forUISoundsDevice ? playThrough_UISounds : playThrough);
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
// Playthrough might not have been notified that BGMDevice is starting yet, so make sure
|
||||
// playthrough is starting. This way we won't drop any frames while waiting for the HAL to send
|
||||
// that notification. We can't be completely sure this is safe from deadlocking, though, since
|
||||
// CoreAudio is closed-source.
|
||||
//
|
||||
// TODO: Test this on older OS X versions. Differences in the CoreAudio implementations could
|
||||
// cause deadlocks.
|
||||
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::startPlayThroughSync",
|
||||
"Starting playthrough", [&] {
|
||||
pt.Start();
|
||||
});
|
||||
|
||||
err = pt.WaitForOutputDeviceToStart();
|
||||
BGMAssert(err != BGMPlayThrough::kDeviceNotStarting, "Playthrough didn't start");
|
||||
} else {
|
||||
LogWarning("BGMAudioDeviceManager::startPlayThroughSync: Didn't get state lock. Returning "
|
||||
"early with kBGMErrorCode_ReturningEarly.");
|
||||
err = kBGMErrorCode_ReturningEarly;
|
||||
|
||||
dispatch_async(BGMGetDispatchQueue_PriorityUserInteractive(), ^{
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
BGMPlayThrough& pt = (forUISoundsDevice ? playThrough_UISounds : playThrough);
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::waitForOutputDeviceToStart", [&]() {
|
||||
playThrough.Start();
|
||||
playThrough.StopIfIdle();
|
||||
BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::startPlayThroughSync",
|
||||
"Starting playthrough (dispatched)", [&] {
|
||||
pt.Start();
|
||||
});
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::startPlayThroughSync", [&] {
|
||||
pt.StopIfIdle();
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
@@ -481,5 +503,33 @@ int const kBGMErrorCode_OutputDeviceNotFound = 1;
|
||||
return err;
|
||||
}
|
||||
|
||||
#pragma mark BGMXPCHelper Communication
|
||||
|
||||
- (void) setBGMXPCHelperConnection:(NSXPCConnection* __nullable)connection {
|
||||
bgmXPCHelperConnection = connection;
|
||||
|
||||
// Tell BGMXPCHelper which device is the output device, since it might not be up-to-date.
|
||||
[self sendOutputDeviceToBGMXPCHelper];
|
||||
}
|
||||
|
||||
- (void) sendOutputDeviceToBGMXPCHelper {
|
||||
NSXPCConnection* __nullable connection = bgmXPCHelperConnection;
|
||||
|
||||
if (connection)
|
||||
{
|
||||
id<BGMXPCHelperXPCProtocol> helperProxy =
|
||||
[connection remoteObjectProxyWithErrorHandler:^(NSError* error) {
|
||||
// We could wait a bit and try again, but it isn't that important.
|
||||
NSLog(@"BGMAudioDeviceManager::sendOutputDeviceToBGMXPCHelper: Connection"
|
||||
"error: %@", error);
|
||||
}];
|
||||
|
||||
[helperProxy setOutputDeviceToMakeDefaultOnAbnormalTermination:outputDevice.GetObjectID()];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ static UInt64 const kPauseDelayNSec = 1500 * NSEC_PER_MSEC;
|
||||
// immediately if we haven't been paused for long and the non-music-player client stops IO? That would usually indicate that
|
||||
// it doesn't intend to start playing audio again soon. We'd also have to deal with music players that don't stop IO when
|
||||
// they're paused.
|
||||
static UInt64 const kMaxUnpauseDelayNSec = 3000 * NSEC_PER_MSEC;
|
||||
static UInt64 const kMaxUnpauseDelayNSec = 3500 * NSEC_PER_MSEC;
|
||||
static UInt64 const kMinUnpauseDelayNSec = kMaxUnpauseDelayNSec / 10;
|
||||
// We multiply the time spent paused by this factor to calculate the delay before we consider unpausing.
|
||||
static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
|
||||
static Float32 const kUnpauseDelayWeightingFactor = 0.1f;
|
||||
|
||||
@implementation BGMAutoPauseMusic {
|
||||
BOOL enabled;
|
||||
@@ -118,7 +118,7 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
|
||||
// so we have to check them all
|
||||
for (int i = 0; i < inNumberAddresses; i++) {
|
||||
if (inAddresses[i].mSelector == kAudioDeviceCustomPropertyDeviceAudibleState) {
|
||||
SInt32 audibleState = [weakSelf deviceAudibleState];
|
||||
BGMDeviceAudibleState audibleState = [weakSelf deviceAudibleState];
|
||||
|
||||
#if DEBUG
|
||||
const char audibleStateStr[5] = CA4CCToCString(audibleState);
|
||||
@@ -144,15 +144,8 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
|
||||
};
|
||||
}
|
||||
|
||||
- (SInt32) deviceAudibleState {
|
||||
SInt32 audibleState;
|
||||
CFNumberRef audibleStateRef =
|
||||
static_cast<CFNumberRef>([audioDevices bgmDevice].GetPropertyData_CFType(kBGMAudibleStateAddress));
|
||||
|
||||
CFNumberGetValue(audibleStateRef, kCFNumberSInt32Type, &audibleState);
|
||||
CFRelease(audibleStateRef);
|
||||
|
||||
return audibleState;
|
||||
- (BGMDeviceAudibleState) deviceAudibleState {
|
||||
return [audioDevices bgmDevice].GetAudibleState();
|
||||
}
|
||||
|
||||
- (void) queuePauseBlock {
|
||||
@@ -188,8 +181,8 @@ static Float32 const kUnpauseDelayWeightingFactor = 0.25f;
|
||||
// Unpause sooner if we've only been paused for a short time. This is so a notification sound causing an auto-pause is
|
||||
// less of an interruption.
|
||||
//
|
||||
// TODO: Would it help much if we ignored all audio played on the "system default" device rather than the "default"
|
||||
// device? IIRC apps are supposed to use the former for UI sounds.
|
||||
// TODO: Fading in and out would make short pauses a lot less jarring because, if they were short enough, we wouldn't
|
||||
// actually pause the music player. So you'd hear a dip in the music's volume rather than a gap.
|
||||
UInt64 unpauseDelayNsec =
|
||||
static_cast<UInt64>((wentSilent - wentAudible) * kUnpauseDelayWeightingFactor);
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMBackgroundMusicDevice.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2017 Andrew Tonner
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGMBackgroundMusicDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CACFArray.h"
|
||||
#include "CACFDictionary.h"
|
||||
|
||||
// STL Includes
|
||||
#include <map>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMBackgroundMusicDevice::BGMBackgroundMusicDevice()
|
||||
:
|
||||
BGMAudioDevice(CFSTR(kBGMDeviceUID)),
|
||||
mUISoundsBGMDevice(CFSTR(kBGMDeviceUID_UISounds))
|
||||
{
|
||||
if((GetObjectID() == kAudioObjectUnknown) || (mUISoundsBGMDevice == kAudioObjectUnknown))
|
||||
{
|
||||
LogError("BGMBackgroundMusicDevice::BGMBackgroundMusicDevice: Error getting BGMDevice ID");
|
||||
Throw(CAException(kAudioHardwareIllegalOperationError));
|
||||
}
|
||||
};
|
||||
|
||||
BGMBackgroundMusicDevice::~BGMBackgroundMusicDevice()
|
||||
{
|
||||
}
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
void BGMBackgroundMusicDevice::SetAsOSDefault()
|
||||
{
|
||||
DebugMsg("BGMBackgroundMusicDevice::SetAsOSDefault: Setting the system's default audio device "
|
||||
"to BGMDevice");
|
||||
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
AudioDeviceID defaultDevice = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
AudioDeviceID systemDefaultDevice = audioSystem.GetDefaultAudioDevice(false, true);
|
||||
|
||||
if(systemDefaultDevice == defaultDevice)
|
||||
{
|
||||
// The default system device is the same as the default device, so change both of them.
|
||||
//
|
||||
// Use the UI sounds instance of BGMDevice because the default system output device is the
|
||||
// device "to use for system related sound". The allows BGMDriver to tell when the audio it
|
||||
// receives is UI-related.
|
||||
audioSystem.SetDefaultAudioDevice(false, true, mUISoundsBGMDevice);
|
||||
}
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, false, GetObjectID());
|
||||
}
|
||||
|
||||
void BGMBackgroundMusicDevice::UnsetAsOSDefault(AudioDeviceID inOutputDeviceID)
|
||||
{
|
||||
CAHALAudioSystemObject audioSystem;
|
||||
|
||||
// Set BGMApp's output device as OS X's default output device.
|
||||
bool bgmDeviceIsDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, false) == GetObjectID());
|
||||
|
||||
if(bgmDeviceIsDefault)
|
||||
{
|
||||
DebugMsg("BGMBackgroundMusicDevice::UnsetAsOSDefault: Setting the system's default output "
|
||||
"device back to device %d", inOutputDeviceID);
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, false, inOutputDeviceID);
|
||||
}
|
||||
|
||||
// Set BGMApp's output device as OS X's default system output device.
|
||||
bool bgmDeviceIsSystemDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, true) == mUISoundsBGMDevice);
|
||||
|
||||
// If we changed the default system output device to BGMDevice, which we only do if it's set to
|
||||
// the same device as the default output device, change it back to the previous device.
|
||||
if(bgmDeviceIsSystemDefault)
|
||||
{
|
||||
DebugMsg("BGMBackgroundMusicDevice::UnsetAsOSDefault: Setting the system's default system "
|
||||
"output device back to device %d", inOutputDeviceID);
|
||||
|
||||
audioSystem.SetDefaultAudioDevice(false, true, inOutputDeviceID);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark App Volumes
|
||||
|
||||
CFArrayRef BGMBackgroundMusicDevice::GetAppVolumes() const
|
||||
{
|
||||
CFTypeRef appVolumes = GetPropertyData_CFType(kBGMAppVolumesAddress);
|
||||
|
||||
ThrowIfNULL(appVolumes,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetAppVolumes: !appVolumes");
|
||||
ThrowIf(CFGetTypeID(appVolumes) != CFArrayGetTypeID(),
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetAppVolumes: Expected CFArray value");
|
||||
|
||||
return static_cast<CFArrayRef>(appVolumes);
|
||||
}
|
||||
|
||||
void BGMBackgroundMusicDevice::SetAppVolume(SInt32 inVolume,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID)
|
||||
{
|
||||
BGMAssert((kAppRelativeVolumeMinRawValue <= inVolume) &&
|
||||
(inVolume <= kAppRelativeVolumeMaxRawValue),
|
||||
"BGMBackgroundMusicDevice::SetAppVolume: Volume out of bounds");
|
||||
|
||||
// Clamp the volume to [kAppRelativeVolumeMinRawValue, kAppPanRightRawValue].
|
||||
inVolume = std::max(kAppRelativeVolumeMinRawValue, inVolume);
|
||||
inVolume = std::min(kAppRelativeVolumeMaxRawValue, inVolume);
|
||||
|
||||
SendAppVolumeOrPanToBGMDevice(inVolume,
|
||||
CFSTR(kBGMAppVolumesKey_RelativeVolume),
|
||||
inAppProcessID,
|
||||
inAppBundleID);
|
||||
}
|
||||
|
||||
void BGMBackgroundMusicDevice::SetAppPanPosition(SInt32 inPanPosition,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID)
|
||||
{
|
||||
BGMAssert((kAppPanLeftRawValue <= inPanPosition) && (inPanPosition <= kAppPanRightRawValue),
|
||||
"BGMBackgroundMusicDevice::SetAppPanPosition: Pan position out of bounds");
|
||||
|
||||
// Clamp the pan position to [kAppPanLeftRawValue, kAppPanRightRawValue].
|
||||
inPanPosition = std::max(kAppPanLeftRawValue, inPanPosition);
|
||||
inPanPosition = std::min(kAppPanRightRawValue, inPanPosition);
|
||||
|
||||
SendAppVolumeOrPanToBGMDevice(inPanPosition,
|
||||
CFSTR(kBGMAppVolumesKey_PanPosition),
|
||||
inAppProcessID,
|
||||
inAppBundleID);
|
||||
}
|
||||
|
||||
void BGMBackgroundMusicDevice::SendAppVolumeOrPanToBGMDevice(SInt32 inNewValue,
|
||||
CFStringRef inVolumeTypeKey,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID)
|
||||
{
|
||||
CACFArray appVolumeChanges(true);
|
||||
|
||||
auto addVolumeChange = [&] (pid_t pid, CFStringRef bundleID)
|
||||
{
|
||||
CACFDictionary appVolumeChange(true);
|
||||
|
||||
appVolumeChange.AddSInt32(CFSTR(kBGMAppVolumesKey_ProcessID), pid);
|
||||
appVolumeChange.AddString(CFSTR(kBGMAppVolumesKey_BundleID), bundleID);
|
||||
appVolumeChange.AddSInt32(inVolumeTypeKey, inNewValue);
|
||||
|
||||
appVolumeChanges.AppendDictionary(appVolumeChange.GetDict());
|
||||
};
|
||||
|
||||
addVolumeChange(inAppProcessID, inAppBundleID);
|
||||
|
||||
// Add the same change for each process the app is responsible for.
|
||||
for(CACFString responsibleBundleID : ResponsibleBundleIDsOf(CACFString(inAppBundleID)))
|
||||
{
|
||||
// Send -1 as the PID so this volume will only ever be matched by bundle ID.
|
||||
addVolumeChange(-1, responsibleBundleID.GetCFString());
|
||||
}
|
||||
|
||||
CFPropertyListRef changesPList = appVolumeChanges.AsPropertyList();
|
||||
|
||||
// Send the change to BGMDevice.
|
||||
SetPropertyData_CFType(kBGMAppVolumesAddress, changesPList);
|
||||
|
||||
// Also send it to the instance of BGMDevice that handles UI sounds.
|
||||
mUISoundsBGMDevice.SetPropertyData_CFType(kBGMAppVolumesAddress, changesPList);
|
||||
}
|
||||
|
||||
// This is a temporary solution that lets us control the volumes of some multiprocess apps, i.e.
|
||||
// apps that play their audio from a process with a different bundle ID.
|
||||
//
|
||||
// We can't just check the child processes of the apps' main processes because they're usually
|
||||
// created with launchd rather than being actual child processes. There's a private API to get the
|
||||
// processes that an app is "responsible for", so we'll try to use it in the proper fix and only use
|
||||
// this list if the API doesn't work.
|
||||
//
|
||||
// static
|
||||
std::vector<CACFString>
|
||||
BGMBackgroundMusicDevice::ResponsibleBundleIDsOf(CACFString inParentBundleID)
|
||||
{
|
||||
if(!inParentBundleID.IsValid())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::map<CACFString, std::vector<CACFString>> bundleIDMap = {
|
||||
// Finder
|
||||
{ "com.apple.finder",
|
||||
{ "com.apple.quicklook.ui.helper",
|
||||
"com.apple.quicklook.QuickLookUIService" } },
|
||||
// Safari
|
||||
{ "com.apple.Safari", { "com.apple.WebKit.WebContent" } },
|
||||
// Firefox
|
||||
{ "org.mozilla.firefox", { "org.mozilla.plugincontainer" } },
|
||||
// Firefox Nightly
|
||||
{ "org.mozilla.nightly", { "org.mozilla.plugincontainer" } },
|
||||
// VMWare Fusion
|
||||
{ "com.vmware.fusion", { "com.vmware.vmware-vmx" } },
|
||||
// Parallels
|
||||
{ "com.parallels.desktop.console", { "com.parallels.vm" } },
|
||||
// MPlayer OSX Extended
|
||||
{ "hu.mplayerhq.mplayerosx.extended",
|
||||
{ "ch.sttz.mplayerosx.extended.binaries.officialsvn" } },
|
||||
// Discord
|
||||
{ "com.hnc.Discord", { "com.hnc.Discord.helper" } },
|
||||
// Skype
|
||||
{ "com.skype.skype", { "com.skype.skype.Helper" } }
|
||||
};
|
||||
|
||||
// Parallels' VM "dock helper" apps have bundle IDs like
|
||||
// com.parallels.winapp.87f6bfc236d64d70a81c47f6243add4c.f5a25fdede514f7aa0a475a1873d3287.fs
|
||||
if(inParentBundleID.StartsWith(CFSTR("com.parallels.winapp.")))
|
||||
{
|
||||
return { "com.parallels.vm" };
|
||||
}
|
||||
|
||||
return bundleIDMap[inParentBundleID];
|
||||
}
|
||||
|
||||
#pragma mark Audible State
|
||||
|
||||
BGMDeviceAudibleState BGMBackgroundMusicDevice::GetAudibleState() const
|
||||
{
|
||||
CFTypeRef propertyDataRef = GetPropertyData_CFType(kBGMAudibleStateAddress);
|
||||
|
||||
ThrowIfNULL(propertyDataRef,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetAudibleState: !propertyDataRef");
|
||||
|
||||
ThrowIf(CFGetTypeID(propertyDataRef) != CFNumberGetTypeID(),
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetAudibleState: Property was not a CFNumber");
|
||||
|
||||
CFNumberRef audibleStateRef = static_cast<CFNumberRef>(propertyDataRef);
|
||||
|
||||
BGMDeviceAudibleState audibleState;
|
||||
Boolean success = CFNumberGetValue(audibleStateRef, kCFNumberSInt32Type, &audibleState);
|
||||
CFRelease(audibleStateRef);
|
||||
|
||||
ThrowIf(!success,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetMusicPlayerProcessID: CFNumberGetValue failed");
|
||||
|
||||
return audibleState;
|
||||
}
|
||||
|
||||
#pragma mark Music Player
|
||||
|
||||
pid_t BGMBackgroundMusicDevice::GetMusicPlayerProcessID() const
|
||||
{
|
||||
CFTypeRef propertyDataRef = GetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress);
|
||||
|
||||
ThrowIfNULL(propertyDataRef,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetMusicPlayerProcessID: !propertyDataRef");
|
||||
|
||||
ThrowIf(CFGetTypeID(propertyDataRef) != CFNumberGetTypeID(),
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetMusicPlayerProcessID: Property was not a CFNumber");
|
||||
|
||||
CFNumberRef pidRef = static_cast<CFNumberRef>(propertyDataRef);
|
||||
|
||||
pid_t pid;
|
||||
Boolean success = CFNumberGetValue(pidRef, kCFNumberIntType, &pid);
|
||||
CFRelease(pidRef);
|
||||
|
||||
ThrowIf(!success,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetMusicPlayerProcessID: CFNumberGetValue failed");
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
CFStringRef BGMBackgroundMusicDevice::GetMusicPlayerBundleID() const
|
||||
{
|
||||
CFStringRef bundleID = GetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress);
|
||||
|
||||
ThrowIfNULL(bundleID,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGMBackgroundMusicDevice::GetMusicPlayerBundleID: !bundleID");
|
||||
|
||||
return bundleID;
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMBackgroundMusicDevice.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// The interface to BGMDevice, the main virtual device published by BGMDriver, and the second
|
||||
// instance of that device, which handles UI-related audio. In most cases, users of this class
|
||||
// should be able to think of it as representing a single device.
|
||||
//
|
||||
// BGMDevice is the device that appears as "Background Music" in programs that list the output
|
||||
// devices, e.g. System Preferences. It receives the system's audio, processes it and sends it to
|
||||
// BGMApp by publishing an input stream. BGMApp then plays the audio on the user's real output
|
||||
// device.
|
||||
//
|
||||
// See BGMDriver/BGMDriver/BGM_Device.h.
|
||||
//
|
||||
|
||||
#ifndef BGMApp__BGMBackgroundMusicDevice
|
||||
#define BGMApp__BGMBackgroundMusicDevice
|
||||
|
||||
// Superclass Includes
|
||||
#include "BGMAudioDevice.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CACFString.h"
|
||||
|
||||
// STL Includes
|
||||
#include <vector>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGMBackgroundMusicDevice
|
||||
:
|
||||
public BGMAudioDevice
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
/*!
|
||||
@throws CAException If BGMDevice is not found or the HAL returns an error when queried for
|
||||
BGMDevice's current Audio Object ID.
|
||||
*/
|
||||
BGMBackgroundMusicDevice();
|
||||
virtual ~BGMBackgroundMusicDevice();
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
public:
|
||||
/*!
|
||||
Set BGMDevice as the default audio device for all processes.
|
||||
|
||||
@throws CAException If the HAL responds with an error.
|
||||
*/
|
||||
void SetAsOSDefault();
|
||||
/*!
|
||||
Replace BGMDevice as the default device with the output device.
|
||||
|
||||
@throws CAException If the HAL responds with an error.
|
||||
*/
|
||||
void UnsetAsOSDefault(AudioDeviceID inOutputDeviceID);
|
||||
|
||||
#pragma mark App Volumes
|
||||
|
||||
public:
|
||||
/*!
|
||||
@return The current value of BGMDevice's kAudioDeviceCustomPropertyAppVolumes property. See
|
||||
BGM_Types.h.
|
||||
@throws CAException If the HAL returns an error or a non-array type. Callers are responsible
|
||||
for validating and type-checking the values contained in the array.
|
||||
*/
|
||||
CFArrayRef GetAppVolumes() const;
|
||||
/*!
|
||||
@param inVolume A value between kAppRelativeVolumeMinRawValue and kAppRelativeVolumeMaxRawValue
|
||||
from BGM_Types.h. See kBGMAppVolumesKey_RelativeVolume in BGM_Types.h.
|
||||
@param inAppProcessID The ID of app's main process (or the process it uses to play audio, if
|
||||
you've managed to figure that out). If an app has multiple audio
|
||||
processes, you can just set the volume for each of them. Pass -1 to omit
|
||||
this param.
|
||||
@param inAppBundleID The app's bundle ID. Pass null to omit this param.
|
||||
@throws CAException If the HAL returns an error when this function sends the volume change to
|
||||
BGMDevice.
|
||||
*/
|
||||
void SetAppVolume(SInt32 inVolume,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID);
|
||||
/*!
|
||||
@param inPanPosition A value between kAppPanLeftRawValue and kAppPanRightRawValue from
|
||||
BGM_Types.h. A negative value has a higher proportion of left channel, and
|
||||
a positive value has a higher proportion of right channel.
|
||||
@param inAppProcessID The ID of app's main process (or the process it uses to play audio, if
|
||||
you've managed to figure that out). If an app has multiple audio
|
||||
processes, you can just set the pan position for each of them. Pass -1 to
|
||||
omit this param.
|
||||
@param inAppBundleID The app's bundle ID. Pass null to omit this param.
|
||||
@throws CAException If the HAL returns an error when this function sends the pan position
|
||||
change to BGMDevice.
|
||||
*/
|
||||
void SetAppPanPosition(SInt32 inPanPosition,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID);
|
||||
|
||||
private:
|
||||
void SendAppVolumeOrPanToBGMDevice(SInt32 inNewValue,
|
||||
CFStringRef inVolumeTypeKey,
|
||||
pid_t inAppProcessID,
|
||||
CFStringRef __nullable inAppBundleID);
|
||||
|
||||
static std::vector<CACFString>
|
||||
ResponsibleBundleIDsOf(CACFString inParentBundleID);
|
||||
|
||||
#pragma mark Audible State
|
||||
|
||||
public:
|
||||
/*!
|
||||
@return BGMDevice's current "audible state", which can be either silent, silent except for the
|
||||
user's music player or audible, meaning a program other than the music player is
|
||||
playing audio.
|
||||
@throws CAException If the HAL returns an error or invalid data when queried.
|
||||
@see kAudioDeviceCustomPropertyDeviceAudibleState in BGM_Types.h.
|
||||
*/
|
||||
BGMDeviceAudibleState GetAudibleState() const;
|
||||
|
||||
#pragma mark Music Player
|
||||
|
||||
public:
|
||||
/*!
|
||||
@return The value of BGMDevice's property for the selected music player's process ID. Zero if
|
||||
the property is unset. (We assume kernel_task will never be the user's music player.)
|
||||
@throws CAException If the HAL returns an error or an invalid PID when queried.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerProcessID in BGM_Types.h.
|
||||
*/
|
||||
virtual pid_t GetMusicPlayerProcessID() const;
|
||||
/*!
|
||||
Set the value of BGMDevice's property for the selected music player's process ID. Pass zero to
|
||||
unset the property. Setting this property will unset the bundle ID version of the property.
|
||||
|
||||
@throws CAException If the HAL returns an error.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerProcessID in BGM_Types.h.
|
||||
*/
|
||||
virtual void SetMusicPlayerProcessID(CFNumberRef inProcessID) {
|
||||
SetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress, inProcessID); }
|
||||
/*!
|
||||
@return The value of BGMDevice's property for the selected music player's bundle ID. The empty
|
||||
string if the property is unset.
|
||||
@throws CAException If the HAL returns an error or an invalid bundle ID when queried.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerBundleID in BGM_Types.h.
|
||||
*/
|
||||
virtual CFStringRef GetMusicPlayerBundleID() const;
|
||||
/*!
|
||||
Set the value of BGMDevice's property for the selected music player's bundle ID. Pass the empty
|
||||
string to unset the property. Setting this property will unset the process ID version of the
|
||||
property.
|
||||
|
||||
@throws CAException If the HAL returns an error.
|
||||
@see kAudioDeviceCustomPropertyMusicPlayerBundleID in BGM_Types.h.
|
||||
*/
|
||||
virtual void SetMusicPlayerBundleID(CFStringRef inBundleID) {
|
||||
SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress, inBundleID); }
|
||||
|
||||
#pragma mark UI Sounds Instance
|
||||
|
||||
public:
|
||||
/*! @return The instance of BGMDevice that handles UI sounds. */
|
||||
BGMAudioDevice GetUISoundsBGMDeviceInstance() { return mUISoundsBGMDevice; }
|
||||
|
||||
private:
|
||||
/*! The instance of BGMDevice that handles UI sounds. */
|
||||
BGMAudioDevice mUISoundsBGMDevice;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMApp__BGMBackgroundMusicDevice */
|
||||
|
||||
@@ -47,8 +47,7 @@ BGMDeviceControlsList::BGMDeviceControlsList(AudioObjectID inBGMDevice,
|
||||
mBGMDevice(inBGMDevice),
|
||||
mAudioSystem(inAudioSystem)
|
||||
{
|
||||
BGMAssert((mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)) ||
|
||||
mBGMDevice.GetObjectID() == kAudioObjectUnknown),
|
||||
BGMAssert((mBGMDevice.IsBGMDevice() || mBGMDevice.GetObjectID() == kAudioObjectUnknown),
|
||||
"BGMDeviceControlsList::BGMDeviceControlsList: Given device is not BGMDevice");
|
||||
|
||||
#pragma clang diagnostic push
|
||||
@@ -69,11 +68,15 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
return;
|
||||
}
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::~BGMDeviceControlsList", [&] {
|
||||
mAudioSystem.RemovePropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
mListenerBlock);
|
||||
});
|
||||
if(mListenerQueue && mListenerBlock)
|
||||
{
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::~BGMDeviceControlsList", ([&] {
|
||||
mAudioSystem.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
mListenerBlock);
|
||||
}));
|
||||
}
|
||||
|
||||
// If we're in the middle of toggling the default device, block until we've finished.
|
||||
if(mDisableNullDeviceBlock && mDeviceToggleState != ToggleState::NotToggling)
|
||||
@@ -90,7 +93,7 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
// worry about ending up waiting for mDisableNullDeviceBlock when it isn't queued.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
bool timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout);
|
||||
long timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
if(timedOut)
|
||||
@@ -105,8 +108,15 @@ BGMDeviceControlsList::~BGMDeviceControlsList()
|
||||
DestroyBlock(mDeviceToggleBackBlock);
|
||||
DestroyBlock(mDisableNullDeviceBlock);
|
||||
|
||||
Block_release(mListenerBlock);
|
||||
dispatch_release(mListenerQueue);
|
||||
if(mListenerBlock)
|
||||
{
|
||||
Block_release(mListenerBlock);
|
||||
}
|
||||
|
||||
if(mListenerQueue)
|
||||
{
|
||||
dispatch_release(BGM_Utils::NN(mListenerQueue));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
@@ -117,7 +127,7 @@ void BGMDeviceControlsList::SetBGMDevice(AudioObjectID inBGMDeviceID)
|
||||
|
||||
mBGMDevice = inBGMDeviceID;
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
BGMAssert(mBGMDevice.IsBGMDevice(),
|
||||
"BGMDeviceControlsList::SetBGMDevice: Given device is not BGMDevice");
|
||||
}
|
||||
|
||||
@@ -127,7 +137,7 @@ bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID)
|
||||
{
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mBGMDevice.GetObjectID() != mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)))
|
||||
if(!mBGMDevice.IsBGMDevice())
|
||||
{
|
||||
LogWarning("BGMDeviceControlsList::MatchControlsListOf: BGMDevice ID not set");
|
||||
return false;
|
||||
@@ -291,14 +301,10 @@ void BGMDeviceControlsList::InitDeviceToggling()
|
||||
return;
|
||||
}
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
BGMAssert(mBGMDevice.IsBGMDevice(),
|
||||
"BGMDeviceControlsList::InitDeviceToggling: mBGMDevice device is not set to "
|
||||
"BGMDevice's ID");
|
||||
|
||||
mDeviceToggleBlock = CreateDeviceToggleBlock();
|
||||
mDeviceToggleBackBlock = CreateDeviceToggleBackBlock();
|
||||
mDisableNullDeviceBlock = CreateDisableNullDeviceBlock();
|
||||
|
||||
// Register a listener to find out when the Null Device becomes available/unavailable. See
|
||||
// ToggleDefaultDevice.
|
||||
#pragma clang diagnostic push
|
||||
@@ -308,7 +314,7 @@ void BGMDeviceControlsList::InitDeviceToggling()
|
||||
#pragma clang diagnostic pop
|
||||
mListenerQueue = dispatch_queue_create("com.bearisdriving.BGM.BGMDeviceControlsList", attr);
|
||||
|
||||
mListenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
auto listenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// Ignore the notification if we're not toggling the default device, which would just mean
|
||||
// the default device has been changed for an unrelated reason.
|
||||
if(mDeviceToggleState == ToggleState::NotToggling)
|
||||
@@ -334,15 +340,27 @@ void BGMDeviceControlsList::InitDeviceToggling()
|
||||
|
||||
// Changing the default device too quickly after enabling the Null Device
|
||||
// seems to cause problems with some programs. Not sure why.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceInitialDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBlock);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
if(mDeviceToggleBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
kToggleDeviceInitialDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
BGM_Utils::NN(mDeviceToggleBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mListenerBlock = Block_copy(listenerBlock);
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMDeviceControlsList::InitDeviceToggling", [&] {
|
||||
mAudioSystem.AddPropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||
mListenerQueue,
|
||||
@@ -380,9 +398,15 @@ void BGMDeviceControlsList::ToggleDefaultDevice()
|
||||
|
||||
mDeviceToggleBackBlock = CreateDeviceToggleBackBlock();
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
mDeviceToggleBackBlock);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
if(mDeviceToggleBackBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||
BGM_Utils::NN(mDeviceToggleBackBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::SetNullDeviceEnabled(bool inEnabled)
|
||||
@@ -405,12 +429,12 @@ void BGMDeviceControlsList::SetNullDeviceEnabled(bool inEnabled)
|
||||
(inEnabled ? kCFBooleanTrue : kCFBooleanFalse));
|
||||
}
|
||||
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
dispatch_block_t __nullable toggleBlock = dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState == ToggleState::SettingNullDeviceAsDefault)
|
||||
@@ -421,11 +445,23 @@ dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if(!toggleBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDeviceToggleBlock: !toggleBlock");
|
||||
}
|
||||
|
||||
return toggleBlock;
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_block_t __nullable toggleBackBlock =
|
||||
dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState != ToggleState::SettingBGMDeviceAsDefault)
|
||||
@@ -446,15 +482,33 @@ dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
|
||||
|
||||
mDisableNullDeviceBlock = CreateDisableNullDeviceBlock();
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
mDisableNullDeviceBlock);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
if(mDisableNullDeviceBlock)
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
BGM_Utils::NN(mDisableNullDeviceBlock));
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
});
|
||||
|
||||
if(!toggleBackBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDeviceToggleBackBlock: !toggleBackBlock");
|
||||
}
|
||||
|
||||
return toggleBackBlock;
|
||||
}
|
||||
|
||||
dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
dispatch_block_t __nullable BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
{
|
||||
return dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_block_t __nullable disableNullDeviceBlock =
|
||||
dispatch_block_create((dispatch_block_flags_t)0, ^{
|
||||
#pragma clang diagnostic pop
|
||||
CAMutex::Locker locker(mMutex);
|
||||
|
||||
if(mDeviceToggleState != ToggleState::DisablingNullDevice)
|
||||
@@ -471,9 +525,16 @@ dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
|
||||
SetNullDeviceEnabled(false);
|
||||
}));
|
||||
|
||||
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
|
||||
"BGMDevice's AudioObjectID changed");
|
||||
BGMAssert(mBGMDevice.IsBGMDevice(), "BGMDevice's AudioObjectID changed");
|
||||
});
|
||||
|
||||
if(!disableNullDeviceBlock)
|
||||
{
|
||||
// Pretty sure this should never happen, but the docs aren't completely clear.
|
||||
LogError("BGMDeviceControlsList::CreateDisableNullDeviceBlock: !disableNullDeviceBlock");
|
||||
}
|
||||
|
||||
return disableNullDeviceBlock;
|
||||
}
|
||||
|
||||
void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
|
||||
@@ -483,6 +544,8 @@ void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
dispatch_block_t& blockNN = (dispatch_block_t&)block;
|
||||
if(!dispatch_block_testcancel(blockNN))
|
||||
{
|
||||
@@ -499,9 +562,8 @@ void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
|
||||
Block_release(block);
|
||||
block = nullptr;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop /* -Wpartial-availability */
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
|
||||
@@ -101,9 +101,9 @@ private:
|
||||
*/
|
||||
void SetNullDeviceEnabled(bool inEnabled);
|
||||
|
||||
dispatch_block_t CreateDeviceToggleBlock();
|
||||
dispatch_block_t CreateDeviceToggleBackBlock();
|
||||
dispatch_block_t CreateDisableNullDeviceBlock();
|
||||
dispatch_block_t __nullable CreateDeviceToggleBlock();
|
||||
dispatch_block_t __nullable CreateDeviceToggleBackBlock();
|
||||
dispatch_block_t __nullable CreateDisableNullDeviceBlock();
|
||||
|
||||
void DestroyBlock(dispatch_block_t __nullable & block);
|
||||
|
||||
@@ -122,12 +122,13 @@ private:
|
||||
};
|
||||
BGMDeviceControlsList::ToggleState mDeviceToggleState = ToggleState::NotToggling;
|
||||
|
||||
dispatch_block_t mDeviceToggleBlock;
|
||||
dispatch_block_t mDeviceToggleBackBlock;
|
||||
dispatch_block_t mDisableNullDeviceBlock;
|
||||
dispatch_block_t __nullable mDeviceToggleBlock = nullptr;
|
||||
dispatch_block_t __nullable mDeviceToggleBackBlock = nullptr;
|
||||
dispatch_block_t __nullable mDisableNullDeviceBlock = nullptr;
|
||||
|
||||
dispatch_queue_t mListenerQueue;
|
||||
AudioObjectPropertyListenerBlock mListenerBlock;
|
||||
// These will only ever be null after construction on 10.9, since toggling will be disabled.
|
||||
dispatch_queue_t __nullable mListenerQueue = nullptr;
|
||||
AudioObjectPropertyListenerBlock __nullable mListenerBlock = nullptr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMOutputVolumeMenuItem.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMOutputVolumeMenuItem : NSMenuItem
|
||||
|
||||
// A menu item with a slider for controlling the volume of the output device. Similar to the one in
|
||||
// macOS's Volume menu extra.
|
||||
//
|
||||
// view, slider and deviceLabel are the UI elements from MainMenu.xib.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
view:(NSView*)view
|
||||
slider:(NSSlider*)slider
|
||||
deviceLabel:(NSTextField*)label;
|
||||
|
||||
- (void) outputDeviceDidChange;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMOutputVolumeMenuItem.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017, 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAException.h"
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
// System Includes
|
||||
#import <CoreAudio/AudioHardware.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
const float kSliderEpsilon = 1e-10f;
|
||||
const AudioObjectPropertyScope kScope = kAudioDevicePropertyScopeOutput;
|
||||
NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||
|
||||
@implementation BGMOutputVolumeMenuItem {
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
NSTextField* deviceLabel;
|
||||
NSSlider* volumeSlider;
|
||||
BGMAudioDevice outputDevice;
|
||||
AudioObjectPropertyListenerBlock updateSliderListenerBlock;
|
||||
AudioObjectPropertyListenerBlock updateLabelListenerBlock;
|
||||
}
|
||||
|
||||
// TODO: Show the output device's icon next to its name.
|
||||
// TODO: Should the menu (bgmMenu) hide after you change the output volume slider, like the normal
|
||||
// menu bar volume slider does?
|
||||
// TODO: Move the output devices from Preferences to the main menu so they're slightly easier to
|
||||
// access?
|
||||
// TODO: Update the screenshot in the README at some point.
|
||||
- (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices
|
||||
view:(NSView*)view
|
||||
slider:(NSSlider*)slider
|
||||
deviceLabel:(NSTextField*)label {
|
||||
if ((self = [super initWithTitle:@"" action:nil keyEquivalent:@""])) {
|
||||
audioDevices = devices;
|
||||
deviceLabel = label;
|
||||
volumeSlider = slider;
|
||||
outputDevice = audioDevices.outputDevice;
|
||||
|
||||
// These are initialised in the methods called below.
|
||||
updateSliderListenerBlock = nil;
|
||||
updateLabelListenerBlock = nil;
|
||||
|
||||
// Apply our custom view from MainMenu.xib.
|
||||
self.view = view;
|
||||
|
||||
// Set up the UI components in the view.
|
||||
[self initSlider];
|
||||
[self updateLabelAndToolTip];
|
||||
|
||||
// Register a listener so we can update if the output device's data source changes.
|
||||
[self addOutputDeviceDataSourceListener];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
- (void) initSlider {
|
||||
BGMAssert([NSThread isMainThread],
|
||||
"initSlider must be called from the main thread because it calls UI functions.");
|
||||
|
||||
volumeSlider.target = self;
|
||||
volumeSlider.action = @selector(sliderChanged:);
|
||||
|
||||
// Initialise the slider.
|
||||
[self updateVolumeSlider];
|
||||
|
||||
// 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);
|
||||
}));
|
||||
}
|
||||
|
||||
// Updates the value of the output volume slider. Should only be called on the main thread because
|
||||
// it calls UI functions.
|
||||
- (void) updateVolumeSlider {
|
||||
BGMAssert([[NSThread currentThread] isMainThread], "updateVolumeSlider on non-main thread.");
|
||||
|
||||
BGMAudioDevice bgmDevice = [audioDevices bgmDevice];
|
||||
|
||||
// BGMDevice should never return an error for these calls, so we just swallow any exceptions and
|
||||
// give up. (That said, we do check mute last so that, if it did throw, it wouldn't affect the
|
||||
// more important calls.)
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::updateVolumeSlider", ([&] {
|
||||
BOOL hasVolume = bgmDevice.HasSettableMasterVolume(kScope);
|
||||
|
||||
// If the device doesn't have a master volume control, we disable the slider and set it to
|
||||
// full (or to zero, if muted).
|
||||
volumeSlider.enabled = hasVolume;
|
||||
|
||||
if (hasVolume) {
|
||||
// Set the slider to the current output volume. The slider values and volume values are
|
||||
// both from 0 to 1, so we can use the volume as is.
|
||||
volumeSlider.doubleValue =
|
||||
bgmDevice.GetVolumeControlScalarValue(kScope, kMasterChannel);
|
||||
} else {
|
||||
volumeSlider.doubleValue = 1.0;
|
||||
}
|
||||
|
||||
// Set the slider to zero if the device is muted.
|
||||
if (bgmDevice.HasSettableMasterMute(kScope) &&
|
||||
bgmDevice.GetMuteControlValue(kScope, kMasterChannel)) {
|
||||
volumeSlider.doubleValue = 0.0;
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
- (void) addOutputDeviceDataSourceListener {
|
||||
// Create the block that updates deviceLabel when the output device's data source changes, e.g.
|
||||
// from Internal Speakers to Headphones.
|
||||
if (!updateLabelListenerBlock) {
|
||||
BGMOutputVolumeMenuItem* __weak weakSelf = self;
|
||||
|
||||
updateLabelListenerBlock =
|
||||
^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
|
||||
// The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain
|
||||
// at least one property the block is listening to, so there's no need to check it.
|
||||
#pragma unused (inNumberAddresses, inAddresses)
|
||||
[weakSelf updateLabelAndToolTip];
|
||||
};
|
||||
}
|
||||
|
||||
// Register the listener.
|
||||
//
|
||||
// Instead of swallowing exceptions, we could try again later, but I doubt it would be worth the
|
||||
// effort. And the documentation doesn't actually explain what could cause this to fail.
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::addOutputDeviceDataSourceListener", ([&] {
|
||||
outputDevice.AddPropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateLabelListenerBlock);
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) removeOutputDeviceDataSourceListener {
|
||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::removeOutputDeviceDataSourceListener",
|
||||
([&] {
|
||||
outputDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
dispatch_get_main_queue(),
|
||||
updateLabelListenerBlock);
|
||||
}));
|
||||
}
|
||||
|
||||
- (void) outputDeviceDidChange {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Remove the data source listener from the previous output device.
|
||||
[self removeOutputDeviceDataSourceListener];
|
||||
|
||||
// Add it to the new output device.
|
||||
outputDevice = audioDevices.outputDevice;
|
||||
[self addOutputDeviceDataSourceListener];
|
||||
|
||||
// Update the label to use the name of the new output device.
|
||||
[self updateLabelAndToolTip];
|
||||
|
||||
// Set the slider to the volume of the new device.
|
||||
[self updateVolumeSlider];
|
||||
});
|
||||
}
|
||||
|
||||
// Sets the label to the output device's name or, if it has one, its current datasource. If it has a
|
||||
// datasource, the device's name is set as this menu item's tooltip. Falls back to a generic name if
|
||||
// the device returns an error when queried.
|
||||
- (void) updateLabelAndToolTip {
|
||||
BOOL didSetLabel = NO;
|
||||
|
||||
try {
|
||||
if (outputDevice.HasDataSourceControl(kScope, kMasterChannel)) {
|
||||
// The device has datasources, so use the current datasource's name like macOS does.
|
||||
UInt32 dataSourceID = outputDevice.GetCurrentDataSourceID(kScope, kMasterChannel);
|
||||
|
||||
deviceLabel.stringValue =
|
||||
(__bridge_transfer NSString*)outputDevice.CopyDataSourceNameForID(kScope,
|
||||
kMasterChannel,
|
||||
dataSourceID);
|
||||
didSetLabel = YES; // So we know not to change the text if setting the tooltip fails.
|
||||
|
||||
// Set the tooltip of the menu item (the container) rather than the label because menu
|
||||
// items' tooltips will still appear when a different app is focused and, as far as I
|
||||
// know, BGMApp should never be the foreground app.
|
||||
self.toolTip = (__bridge_transfer NSString*)outputDevice.CopyName();
|
||||
} else {
|
||||
deviceLabel.stringValue = (__bridge_transfer NSString*)outputDevice.CopyName();
|
||||
self.toolTip = nil;
|
||||
}
|
||||
} catch (const CAException& e) {
|
||||
BGMLogException(e);
|
||||
|
||||
// The device returned an error, so set the label to a generic device name, since we don't
|
||||
// want to leave it set to the previous device's name.
|
||||
self.toolTip = nil;
|
||||
|
||||
if (!didSetLabel) {
|
||||
deviceLabel.stringValue = kGenericOutputDeviceName;
|
||||
}
|
||||
}
|
||||
|
||||
DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Label: '%s' Tooltip: '%s'",
|
||||
deviceLabel.stringValue.UTF8String,
|
||||
self.toolTip.UTF8String);
|
||||
|
||||
// Take the label out of the accessibility hierarchy, which also moves the slider up a level.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101000 // MAC_OS_X_VERSION_10_10
|
||||
if ([deviceLabel.cell respondsToSelector:@selector(setAccessibilityElement:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
deviceLabel.cell.accessibilityElement = NO;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Called when the user slides the slider.
|
||||
- (IBAction) sliderChanged:(NSSlider*)sender {
|
||||
float newValue = sender.floatValue;
|
||||
|
||||
DebugMsg("BGMOutputVolumeMenuItem::sliderChanged: New value: %f", newValue);
|
||||
|
||||
// Update BGMDevice's volume to the new value selected by the user.
|
||||
try {
|
||||
// The slider values and volume values are both from 0.0f to 1.0f, so we can use the slider
|
||||
// value as is.
|
||||
audioDevices.bgmDevice.SetVolumeControlScalarValue(kScope, kMasterChannel, newValue);
|
||||
|
||||
// Mute BGMDevice if they set the slider to zero, and unmute it for non-zero. Muting makes
|
||||
// sure the audio doesn't play very quietly instead being completely silent. This matches
|
||||
// the behaviour of the Volume menu built-in to macOS.
|
||||
if (audioDevices.bgmDevice.HasMuteControl(kScope, kMasterChannel)) {
|
||||
audioDevices.bgmDevice.SetMuteControlValue(kScope,
|
||||
kMasterChannel,
|
||||
(newValue < kSliderEpsilon));
|
||||
}
|
||||
} catch (const CAException& e) {
|
||||
NSLog(@"BGMOutputVolumeMenuItem::sliderChanged: Failed to set volume (%d)", e.GetError());
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -47,7 +47,7 @@ static const UInt32 kStopIOProcTimeoutInIOCycles = 600;
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGMPlayThrough::BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
|
||||
BGMPlayThrough::BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice)
|
||||
:
|
||||
mInputDevice(inInputDevice),
|
||||
mOutputDevice(inOutputDevice)
|
||||
@@ -68,7 +68,7 @@ BGMPlayThrough::~BGMPlayThrough()
|
||||
}
|
||||
}
|
||||
|
||||
void BGMPlayThrough::Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
|
||||
void BGMPlayThrough::Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice)
|
||||
{
|
||||
BGMAssert(mInputDeviceIOProcState.is_lock_free(),
|
||||
"BGMPlayThrough::BGMPlayThrough: !mInputDeviceIOProcState.is_lock_free()");
|
||||
@@ -151,7 +151,7 @@ void BGMPlayThrough::Activate()
|
||||
|
||||
bool isBGMDevice = true;
|
||||
CATry
|
||||
isBGMDevice = IsBGMDevice(mInputDevice);
|
||||
isBGMDevice = mInputDevice.IsBGMDeviceInstance();
|
||||
CACatch
|
||||
|
||||
if(isBGMDevice)
|
||||
@@ -164,7 +164,7 @@ void BGMPlayThrough::Activate()
|
||||
{
|
||||
LogWarning("BGMPlayThrough::Activate: Playthrough activated with an output device other "
|
||||
"than BGMDevice. This hasn't been tested and is almost definitely a bug.");
|
||||
BGMAssert(false, "BGMPlayThrough::Activate: !IsBGMDevice(mInputDevice)");
|
||||
BGMAssert(false, "BGMPlayThrough::Activate: !mInputDevice.IsBGMDeviceInstance()");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ void BGMPlayThrough::Deactivate()
|
||||
bool inputDeviceIsBGMDevice = true;
|
||||
|
||||
CATry
|
||||
inputDeviceIsBGMDevice = IsBGMDevice(mInputDevice);
|
||||
inputDeviceIsBGMDevice = mInputDevice.IsBGMDeviceInstance();
|
||||
CACatch
|
||||
|
||||
// Unregister notification listeners.
|
||||
@@ -188,30 +188,30 @@ void BGMPlayThrough::Deactivate()
|
||||
{
|
||||
// There's not much we can do if these calls throw. The docs for AudioObjectRemovePropertyListener
|
||||
// just say that means it failed.
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
|
||||
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning),
|
||||
&BGMPlayThrough::BGMDeviceListenerProc,
|
||||
this);
|
||||
});
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
|
||||
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload),
|
||||
&BGMPlayThrough::BGMDeviceListenerProc,
|
||||
this);
|
||||
});
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
|
||||
mInputDevice.RemovePropertyListener(kBGMRunningSomewhereOtherThanBGMAppAddress,
|
||||
&BGMPlayThrough::BGMDeviceListenerProc,
|
||||
this);
|
||||
});
|
||||
}
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
|
||||
Stop();
|
||||
});
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] {
|
||||
DestroyIOProcIDs();
|
||||
});
|
||||
|
||||
@@ -240,15 +240,6 @@ void BGMPlayThrough::AllocateBuffer()
|
||||
mOutputDevice.GetIOBufferSize() * 20);
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGMPlayThrough::IsBGMDevice(CAHALAudioDevice inDevice)
|
||||
{
|
||||
CFStringRef uid = inDevice.CopyDeviceUID();
|
||||
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
|
||||
CFRelease(uid);
|
||||
return isBGMDevice;
|
||||
}
|
||||
|
||||
void BGMPlayThrough::CreateIOProcIDs()
|
||||
{
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
@@ -326,7 +317,7 @@ void BGMPlayThrough::DestroyIOProcIDs()
|
||||
|
||||
DebugMsg("BGMPlayThrough::DestroyIOProcIDs: Destroying IOProcs");
|
||||
|
||||
auto destroy = [](CAHALAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) {
|
||||
auto destroy = [](BGMAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) {
|
||||
#if !DEBUG
|
||||
#pragma unused (deviceName)
|
||||
#endif
|
||||
@@ -382,8 +373,8 @@ bool BGMPlayThrough::CheckIOProcsAreStopped() const noexcept
|
||||
return statesOK;
|
||||
}
|
||||
|
||||
void BGMPlayThrough::SetDevices(CAHALAudioDevice* __nullable inInputDevice,
|
||||
CAHALAudioDevice* __nullable inOutputDevice)
|
||||
void BGMPlayThrough::SetDevices(const BGMAudioDevice* __nullable inInputDevice,
|
||||
const BGMAudioDevice* __nullable inOutputDevice)
|
||||
{
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
|
||||
@@ -423,7 +414,12 @@ void BGMPlayThrough::Start()
|
||||
if(mPlayingThrough)
|
||||
{
|
||||
DebugMsg("BGMPlayThrough::Start: Already started/starting.");
|
||||
ReleaseThreadsWaitingForOutputToStart();
|
||||
|
||||
if(mOutputDeviceIOProcState == IOState::Running)
|
||||
{
|
||||
ReleaseThreadsWaitingForOutputToStart();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -515,7 +511,7 @@ OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept
|
||||
return kAudioHardwareBadDeviceError;
|
||||
}
|
||||
}
|
||||
catch(CAException e)
|
||||
catch(const CAException& e)
|
||||
{
|
||||
BGMLogException(e);
|
||||
return e.GetError();
|
||||
@@ -729,7 +725,7 @@ void BGMPlayThrough::StopIfIdle()
|
||||
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
|
||||
BGMAssert(IsBGMDevice(mInputDevice),
|
||||
BGMAssert(mInputDevice.IsBGMDeviceInstance(),
|
||||
"BGMDevice not set as input device. StopIfIdle can't tell if other devices are idle.");
|
||||
|
||||
if(!IsRunningSomewhereOtherThanBGMApp(mInputDevice))
|
||||
@@ -752,7 +748,7 @@ void BGMPlayThrough::StopIfIdle()
|
||||
"queuedAt=", queuedAt);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, waitNsec),
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
||||
^{
|
||||
// Check the BGMPlayThrough instance hasn't been destructed since it queued this block
|
||||
if(mActive)
|
||||
@@ -842,12 +838,12 @@ void BGMPlayThrough::HandleBGMDeviceIsRunning(BGMPlayThrough* refCon)
|
||||
|
||||
// This is dispatched because it can block and
|
||||
// - we might be on a real-time thread, or
|
||||
// - BGMXPCListener::waitForOutputDeviceToStartWithReply might get called on the same thread just
|
||||
// - BGMXPCListener::startPlayThroughSyncWithReply might get called on the same thread just
|
||||
// before this and time out waiting for this to run.
|
||||
//
|
||||
// TODO: We should find a way to do this without dispatching because dispatching isn't actually
|
||||
// real-time safe.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
dispatch_async(BGMGetDispatchQueue_PriorityUserInteractive(), ^{
|
||||
if(refCon->mActive)
|
||||
{
|
||||
CAMutex::Locker stateLocker(refCon->mStateMutex);
|
||||
@@ -856,7 +852,6 @@ void BGMPlayThrough::HandleBGMDeviceIsRunning(BGMPlayThrough* refCon)
|
||||
// try to start playthrough anyway.
|
||||
bool isRunningSomewhereOtherThanBGMApp = true;
|
||||
|
||||
|
||||
BGMLogAndSwallowExceptions("HandleBGMDeviceIsRunning", [&]() {
|
||||
// IsRunning doesn't always return true when IO is starting. Using
|
||||
// RunningSomewhereOtherThanBGMApp instead seems to be working so far.
|
||||
@@ -888,7 +883,7 @@ void BGMPlayThrough::HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlay
|
||||
DebugMsg("BGMPlayThrough::HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp: Got notification");
|
||||
|
||||
// These notifications don't need to be handled quickly, so we can always dispatch.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// TODO: Handle expected exceptions (mostly CAExceptions from PublicUtility classes) in StopIfIdle.
|
||||
BGMLogUnexpectedExceptions("HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp", [&refCon]() {
|
||||
if(refCon->mActive)
|
||||
@@ -900,7 +895,7 @@ void BGMPlayThrough::HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlay
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGMPlayThrough::IsRunningSomewhereOtherThanBGMApp(const CAHALAudioDevice& inBGMDevice)
|
||||
bool BGMPlayThrough::IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice)
|
||||
{
|
||||
return CFBooleanGetValue(
|
||||
static_cast<CFBooleanRef>(
|
||||
@@ -1074,7 +1069,7 @@ OSStatus BGMPlayThrough::OutputDeviceIOProc(AudioObjectID inDevice,
|
||||
bool BGMPlayThrough::UpdateIOProcState(const char* __nullable callerName,
|
||||
std::atomic<IOState>& inState,
|
||||
AudioDeviceIOProcID __nullable inIOProcID,
|
||||
CAHALAudioDevice& inDevice,
|
||||
BGMAudioDevice& inDevice,
|
||||
IOState& outNewState)
|
||||
{
|
||||
BGMAssert(inIOProcID != nullptr, "BGMPlayThrough::UpdateIOProcState: !inIOProcID");
|
||||
|
||||
@@ -39,9 +39,11 @@
|
||||
#ifndef BGMApp__BGMPlayThrough
|
||||
#define BGMApp__BGMPlayThrough
|
||||
|
||||
// Local Includes
|
||||
#include "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CARingBuffer.h"
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAMutex.h"
|
||||
|
||||
// STL Includes
|
||||
@@ -62,7 +64,7 @@ public:
|
||||
static const OSStatus kDeviceNotStarting = 100;
|
||||
|
||||
public:
|
||||
BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice);
|
||||
~BGMPlayThrough();
|
||||
// Disallow copying
|
||||
BGMPlayThrough(const BGMPlayThrough&) = delete;
|
||||
@@ -76,7 +78,7 @@ public:
|
||||
|
||||
private:
|
||||
/*! @throws CAException */
|
||||
void Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
void Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice);
|
||||
|
||||
public:
|
||||
/*! @throws CAException */
|
||||
@@ -86,8 +88,6 @@ public:
|
||||
|
||||
private:
|
||||
void AllocateBuffer();
|
||||
|
||||
static bool IsBGMDevice(CAHALAudioDevice inDevice);
|
||||
|
||||
/*! @throws CAException */
|
||||
void CreateIOProcIDs();
|
||||
@@ -104,8 +104,8 @@ public:
|
||||
Pass null for either param to only change one of the devices.
|
||||
@throws CAException
|
||||
*/
|
||||
void SetDevices(CAHALAudioDevice* __nullable inInputDevice,
|
||||
CAHALAudioDevice* __nullable inOutputDevice);
|
||||
void SetDevices(const BGMAudioDevice* __nullable inInputDevice,
|
||||
const BGMAudioDevice* __nullable inOutputDevice);
|
||||
|
||||
/*! @throws CAException */
|
||||
void Start();
|
||||
@@ -130,7 +130,7 @@ private:
|
||||
static void HandleBGMDeviceIsRunning(BGMPlayThrough* refCon);
|
||||
static void HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlayThrough* refCon);
|
||||
|
||||
static bool IsRunningSomewhereOtherThanBGMApp(const CAHALAudioDevice& inBGMDevice);
|
||||
static bool IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice);
|
||||
|
||||
static OSStatus InputDeviceIOProc(AudioObjectID inDevice,
|
||||
const AudioTimeStamp* inNow,
|
||||
@@ -159,7 +159,7 @@ private:
|
||||
static bool UpdateIOProcState(const char* __nullable callerName,
|
||||
std::atomic<IOState>& inState,
|
||||
AudioDeviceIOProcID __nullable inIOProcID,
|
||||
CAHALAudioDevice& inDevice,
|
||||
BGMAudioDevice& inDevice,
|
||||
IOState& outNewState);
|
||||
|
||||
static void HandleRingBufferError(CARingBufferError err,
|
||||
@@ -169,11 +169,11 @@ private:
|
||||
private:
|
||||
CARingBuffer mBuffer;
|
||||
|
||||
AudioDeviceIOProcID __nullable mInputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID;
|
||||
AudioDeviceIOProcID __nullable mInputDeviceIOProcID { nullptr };
|
||||
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID { nullptr };
|
||||
|
||||
CAHALAudioDevice mInputDevice { kAudioObjectUnknown };
|
||||
CAHALAudioDevice mOutputDevice { kAudioObjectUnknown };
|
||||
BGMAudioDevice mInputDevice { kAudioObjectUnknown };
|
||||
BGMAudioDevice mOutputDevice { kAudioObjectUnknown };
|
||||
|
||||
CAMutex mStateMutex { "Playthrough state" };
|
||||
|
||||
@@ -183,13 +183,13 @@ private:
|
||||
bool mActive = false;
|
||||
bool mPlayingThrough = false;
|
||||
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice;
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice { 0 };
|
||||
|
||||
std::atomic<IOState> mInputDeviceIOProcState { IOState::Stopped };
|
||||
std::atomic<IOState> mOutputDeviceIOProcState { IOState::Stopped };
|
||||
|
||||
// For debug logging.
|
||||
UInt64 mToldOutputDeviceToStartAt;
|
||||
UInt64 mToldOutputDeviceToStartAt { 0 };
|
||||
|
||||
// IOProc vars. (Should only be used inside IOProcs.)
|
||||
|
||||
@@ -199,7 +199,7 @@ private:
|
||||
Float64 mLastOutputSampleTime = -1;
|
||||
|
||||
// Subtract this from the output time to get the input time.
|
||||
Float64 mInToOutSampleOffset;
|
||||
Float64 mInToOutSampleOffset { 0.0 };
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMSystemSoundsVolume.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// The menu item with the volume slider that controls system-related sounds. The slider is used to
|
||||
// set the volume of the instance of BGMDevice that system sounds are played on, i.e. the audio
|
||||
// device returned by BGMBackgroundMusicDevice::GetUISoundsBGMDeviceInstance.
|
||||
//
|
||||
// System sounds are any sounds played using the audio device macOS is set to use as the device
|
||||
// "for system related sound from the alert sound to digital call progress". See
|
||||
// kAudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h. They can be played by any
|
||||
// app, though most apps use systemsoundserverd to play their system sounds, which means BGMDriver
|
||||
// can't tell which app is actually playing the sounds.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDevice.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMSystemSoundsVolume : NSObject
|
||||
|
||||
// The volume level of uiSoundsDevice will be used to set the slider's initial position and will be
|
||||
// updated when the user moves the slider. view and slider are the UI elements from MainMenu.xib.
|
||||
- (instancetype) initWithUISoundsDevice:(BGMAudioDevice)uiSoundsDevice
|
||||
view:(NSView*)view
|
||||
slider:(NSSlider*)slider;
|
||||
|
||||
// The menu item with the volume slider for system sounds.
|
||||
@property (readonly) NSMenuItem* menuItem;
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMSystemSoundsVolume.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
// TODO: It's a bit confusing that this slider's default position is all the way right, but the App
|
||||
// Volumes sliders default to 50%. After you move the slider there's no way to tell how to put
|
||||
// it back to its normal position.
|
||||
|
||||
NSString* const kMenuItemToolTip =
|
||||
@"Alerts, notification sounds, etc. Usually short. Can be played by any app.";
|
||||
|
||||
@implementation BGMSystemSoundsVolume {
|
||||
BGMAudioDevice uiSoundsDevice;
|
||||
NSSlider* volumeSlider;
|
||||
}
|
||||
|
||||
- (instancetype) initWithUISoundsDevice:(BGMAudioDevice)inUISoundsDevice
|
||||
view:(NSView*)inView
|
||||
slider:(NSSlider*)inSlider {
|
||||
if ((self = [super init])) {
|
||||
uiSoundsDevice = inUISoundsDevice;
|
||||
volumeSlider = inSlider;
|
||||
|
||||
_menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
|
||||
_menuItem.toolTip = kMenuItemToolTip;
|
||||
|
||||
// Apply our custom view from MainMenu.xib. It's very similar to the one for app volumes.
|
||||
_menuItem.view = inView;
|
||||
|
||||
try {
|
||||
volumeSlider.floatValue =
|
||||
uiSoundsDevice.GetVolumeControlScalarValue(kAudioObjectPropertyScopeOutput,
|
||||
kMasterChannel);
|
||||
} catch (const CAException& e) {
|
||||
BGMLogException(e);
|
||||
volumeSlider.floatValue = 1.0f; // Full volume
|
||||
}
|
||||
|
||||
volumeSlider.target = self;
|
||||
volumeSlider.action = @selector(systemSoundsSliderChanged:);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) systemSoundsSliderChanged:(id)sender {
|
||||
#pragma unused(sender)
|
||||
|
||||
float sliderLevel = volumeSlider.floatValue;
|
||||
|
||||
BGMAssert((sliderLevel >= 0.0f) && (sliderLevel <= 1.0f), "Invalid value from slider");
|
||||
DebugMsg("BGMSystemSoundsVolume::systemSoundsSliderChanged: UI sounds volume: %f", sliderLevel);
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMSystemSoundsVolume::systemSoundsSliderChanged", ([&] {
|
||||
uiSoundsDevice.SetVolumeControlScalarValue(kAudioObjectPropertyScopeOutput,
|
||||
kMasterChannel,
|
||||
sliderLevel);
|
||||
}));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMTermination.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// Cleans up if BGMApp crashes because of an uncaught C++ or Objective C exception, or is sent
|
||||
// SIGINT/SIGTERM/SIGQUIT. Currently, it just changes the default output device from BGMDevice to
|
||||
// the real output device and records debug info for some types of crashes.
|
||||
//
|
||||
// BGMXPCHelper also changes the default device if BGMApp disconnects and leaves BGMDevice as the
|
||||
// default. This handles cases like segfaults where it wouldn't be safe to clean up from the
|
||||
// crashing process.
|
||||
//
|
||||
|
||||
#ifndef BGMApp__BGMTermination
|
||||
#define BGMApp__BGMTermination
|
||||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAPThread.h"
|
||||
|
||||
// STL Includes
|
||||
#import <exception>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGMTermination
|
||||
{
|
||||
|
||||
public:
|
||||
/*!
|
||||
Starts a thread that will clean up before exiting if BGMApp receives SIGINT, SIGTERM or
|
||||
SIGQUIT. Sets a similar clean up function to run if BGMApp terminates due to an uncaught
|
||||
exception.
|
||||
*/
|
||||
static void SetUpTerminationCleanUp(BGMAudioDeviceManager* inAudioDevices);
|
||||
|
||||
/*! Some commented out ways to have BGMApp crash for testing. Does nothing if unmodified. */
|
||||
static void TestCrash() __attribute__((noinline));
|
||||
|
||||
private:
|
||||
static void StartExitSignalsThread();
|
||||
|
||||
static void CleanUpAudioDevices();
|
||||
|
||||
/*! Adds some info about the uncaught exception that caused a crash to the crash report. */
|
||||
static void AddCurrentExceptionToCrashReport();
|
||||
|
||||
/*! The entry point for sExitSignalsThread. */
|
||||
static void* __nullable ExitSignalsProc(void* __nullable ignored);
|
||||
|
||||
/*! The thread that handles SIGQUIT, SIGTERM and SIGINT. Never destroyed. */
|
||||
static CAPThread* const sExitSignalsThread;
|
||||
static sigset_t sExitSignals;
|
||||
|
||||
/*! The function that handles std::terminate by default. */
|
||||
static std::terminate_handler sOriginalTerminateHandler;
|
||||
|
||||
/*! The audio device manager. (Must be static to be accessed in our std::terminate_handler.) */
|
||||
static BGMAudioDeviceManager* __nullable sAudioDevices;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMApp__BGMTermination */
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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/>.
|
||||
|
||||
//
|
||||
// BGMTermination.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMTermination.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
// STL Includes
|
||||
#import <string>
|
||||
|
||||
// System Includes
|
||||
#import <signal.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
std::terminate_handler BGMTermination::sOriginalTerminateHandler = std::get_terminate();
|
||||
|
||||
CAPThread* const BGMTermination::sExitSignalsThread = new CAPThread(ExitSignalsProc, nullptr);
|
||||
sigset_t BGMTermination::sExitSignals;
|
||||
|
||||
BGMAudioDeviceManager* __nullable BGMTermination::sAudioDevices = nullptr;
|
||||
|
||||
// If BGMApp crashes, CrashReporter will read this string from our process' memory and include it in
|
||||
// the crash report.
|
||||
const char* __nullable __crashreporter_info__ = nullptr;
|
||||
// Set the REFERENCED_DYNAMICALLY bit so the symbol doesn't get stripped from the binary. See
|
||||
// <https://developer.apple.com/library/content/documentation/DeveloperTools/Reference/Assembler/040-Assembler_Directives/asm_directives.html>
|
||||
// and
|
||||
// <https://github.com/aidansteele/osx-abi-macho-file-format-reference#symbol-table-and-related-data-structures>
|
||||
// (Ctrl+F "REFERENCED_DYNAMICALLY").
|
||||
asm(".desc ___crashreporter_info__, 0x10");
|
||||
|
||||
|
||||
// static
|
||||
void BGMTermination::TestCrash()
|
||||
{
|
||||
// To give BGMApp a few seconds to finish launching and then crash:
|
||||
//
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
|
||||
// dispatch_get_main_queue(),
|
||||
// ^{
|
||||
// BGMTermination::TestCrash();
|
||||
// });
|
||||
|
||||
// throw CAException(kAudioHardwareBadDeviceError);
|
||||
// throw BGM_InvalidClientRelativeVolumeException();
|
||||
// std::string().at(1);
|
||||
// *reinterpret_cast<int*>(0x1234) = 9;
|
||||
// [NSException raise:@"ObjC Test Exception" format:@"The description of the test exception."];
|
||||
}
|
||||
|
||||
// static
|
||||
void BGMTermination::SetUpTerminationCleanUp(BGMAudioDeviceManager* inAudioDevices)
|
||||
{
|
||||
sAudioDevices = inAudioDevices;
|
||||
|
||||
StartExitSignalsThread();
|
||||
|
||||
// Wrap the default handler for std::terminate, which is called if BGMApp crashes because of an
|
||||
// uncaught C++ or Objective C exception, so we can clean up first.
|
||||
sOriginalTerminateHandler = std::get_terminate();
|
||||
|
||||
std::set_terminate([] {
|
||||
CleanUpAudioDevices();
|
||||
|
||||
AddCurrentExceptionToCrashReport();
|
||||
|
||||
// Call the default terminate handler to finish crashing normally.
|
||||
sOriginalTerminateHandler();
|
||||
});
|
||||
}
|
||||
|
||||
// static
|
||||
void BGMTermination::StartExitSignalsThread()
|
||||
{
|
||||
// Block the signals the thread will handle, so they can be unblocked for just that thread.
|
||||
sigemptyset(&sExitSignals);
|
||||
sigaddset(&sExitSignals, SIGQUIT);
|
||||
sigaddset(&sExitSignals, SIGTERM);
|
||||
sigaddset(&sExitSignals, SIGINT);
|
||||
|
||||
if(pthread_sigmask(SIG_BLOCK, &sExitSignals, nullptr) != 0)
|
||||
{
|
||||
perror("pthread_sigmask");
|
||||
return; // This would just mean the signals would be handled by the default handlers.
|
||||
}
|
||||
|
||||
// Start the thread.
|
||||
sExitSignalsThread->Start();
|
||||
}
|
||||
|
||||
// static
|
||||
void BGMTermination::CleanUpAudioDevices()
|
||||
{
|
||||
// BGMXPCHelper would set the output device back if we didn't do it here, but in general
|
||||
// it's better for things to work even if BGMXPCHelper isn't installed.
|
||||
if(sAudioDevices)
|
||||
{
|
||||
[sAudioDevices unsetBGMDeviceAsOSDefault];
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
void BGMTermination::AddCurrentExceptionToCrashReport()
|
||||
{
|
||||
std::exception_ptr exceptionPtr = std::current_exception();
|
||||
|
||||
if(exceptionPtr)
|
||||
{
|
||||
// The message to add to the crash report (and log).
|
||||
std::string* msg = new std::string("");
|
||||
|
||||
// Throw the exception again and catch it so we can get some info if it's a CAException. If
|
||||
// it's a std::exception, the default terminate handler will do the same thing, so we can
|
||||
// just ignore it here.
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(exceptionPtr);
|
||||
}
|
||||
catch(const CAException& e)
|
||||
{
|
||||
OSStatus err = e.GetError();
|
||||
const char err4CC[5] = CA4CCToCString(err);
|
||||
|
||||
msg = new std::string("Uncaught CAException. Error code: '");
|
||||
msg->append(err4CC);
|
||||
msg->append("' (");
|
||||
msg->append(std::to_string(err));
|
||||
msg->append(").");
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
}
|
||||
|
||||
// CrashReporter will read the contents of __crashreporter_info__.
|
||||
__crashreporter_info__ = msg->c_str();
|
||||
NSLog(@"%s", msg->c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The entry point for the thread that handles SIGQUIT, SIGTERM and SIGINT.
|
||||
// static
|
||||
void* __nullable BGMTermination::ExitSignalsProc(void* __nullable ignored)
|
||||
{
|
||||
#pragma unused (ignored)
|
||||
|
||||
DebugMsg("BGMTermination::ExitSignalsProc: Thread started.");
|
||||
|
||||
int signal = -1;
|
||||
|
||||
// Wait until we receive a signal.
|
||||
while((signal != SIGINT) && (signal != SIGTERM) && (signal != SIGQUIT))
|
||||
{
|
||||
if(sigwait(&sExitSignals, &signal) != 0)
|
||||
{
|
||||
perror("sigwait");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if(signal == SIGINT)
|
||||
{
|
||||
NSLog(@"Interrupted.");
|
||||
}
|
||||
else if(signal == SIGTERM)
|
||||
{
|
||||
NSLog(@"Exiting.");
|
||||
}
|
||||
|
||||
CleanUpAudioDevices();
|
||||
|
||||
// Unblock the signal and resend it to ourselves so it will be handled by the default handler
|
||||
// and exit BGMApp.
|
||||
if(pthread_sigmask(SIG_UNBLOCK, &sExitSignals, nullptr) != 0)
|
||||
{
|
||||
perror("pthread_sigmask");
|
||||
abort();
|
||||
}
|
||||
|
||||
raise(signal);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// BGMXPCListener.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMXPCListener.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMPlayThrough.h" // For kDeviceNotStarting.
|
||||
|
||||
|
||||
@@ -52,6 +54,9 @@
|
||||
|
||||
// Set up the connection to BGMXPCHelper.
|
||||
[self initHelperConnectionWithErrorHandler:errorHandler];
|
||||
|
||||
// Pass the connection to the audio device manager so it can tell BGMXPCHelper the output device's ID.
|
||||
[audioDevices setBGMXPCHelperConnection:helperConnection];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -142,6 +147,9 @@
|
||||
retryAfterTimeout(nil);
|
||||
};
|
||||
}];
|
||||
|
||||
// Pass the new connection to the audio device manager.
|
||||
[audioDevices setBGMXPCHelperConnection:helperConnection];
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
@@ -175,19 +183,19 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) waitForOutputDeviceToStartWithReply:(void (^)(NSError*))reply {
|
||||
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI {
|
||||
NSString* description;
|
||||
OSStatus err;
|
||||
|
||||
try {
|
||||
err = [audioDevices waitForOutputDeviceToStart];
|
||||
err = [audioDevices startPlayThroughSync:isUI];
|
||||
} catch (CAException e) {
|
||||
// waitForOutputDeviceToStart should never throw a CAException, but check anyway in case we change that at some point.
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
// startPlayThroughSync should never throw a CAException, but check anyway in case we change that at some point.
|
||||
LogError("BGMXPCListener::startPlayThroughSyncWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
e.GetError());
|
||||
err = kBGMXPC_HardwareError;
|
||||
} catch (...) {
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
LogError("BGMXPCListener::startPlayThroughSyncWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
err = kBGMXPC_InternalError;
|
||||
#if DEBUG
|
||||
throw;
|
||||
@@ -210,10 +218,10 @@
|
||||
err = kBGMXPC_HardwareError;
|
||||
break;
|
||||
|
||||
case BGMPlayThrough::kDeviceNotStarting:
|
||||
case kBGMErrorCode_ReturningEarly:
|
||||
// We have to send a more specific error in this case because BGMDevice handles this case differently.
|
||||
description = @"The output device is not starting.";
|
||||
err = kBGMXPC_HardwareNotStartingError;
|
||||
description = @"BGMApp could not wait for the output device to be ready for IO.";
|
||||
err = kBGMXPC_ReturningEarlyError;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<development version="7000" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12121"/>
|
||||
<development version="8000" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -20,6 +21,11 @@
|
||||
<outlet property="appVolumeView" destination="MWB-XH-kFI" id="eFA-RN-VMC"/>
|
||||
<outlet property="autoPauseMenuItemUnwrapped" destination="nHv-T8-1nb" id="Lie-Cx-jw6"/>
|
||||
<outlet property="bgmMenu" destination="8AN-nh-rEe" id="UWn-BX-eLy"/>
|
||||
<outlet property="outputVolumeLabel" destination="wfC-C6-SLv" id="Nuf-mo-osG"/>
|
||||
<outlet property="outputVolumeSlider" destination="9Ru-Sc-dqC" id="wv0-Md-BwF"/>
|
||||
<outlet property="outputVolumeView" destination="JOz-H1-mj9" id="xeJ-fk-NMI"/>
|
||||
<outlet property="systemSoundsSlider" destination="gyd-WV-2ju" id="NEe-5W-EI5"/>
|
||||
<outlet property="systemSoundsView" destination="dBD-CE-4dw" id="4SD-Z1-akp"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
@@ -27,10 +33,10 @@
|
||||
<items>
|
||||
<menuItem title="Auto-pause Music" tag="2" id="nHv-T8-1nb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<accessibility description="Enable to automatically pause your selected music player when a different app starts playing audio." identifier="Auto-pause enabled"/>
|
||||
<accessibility help="Enable to automatically pause your selected music player when a different app starts playing audio." identifier="Auto-pause enabled"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="ZGd-Pq-YeA"/>
|
||||
<menuItem title="App Volumes" tag="3" enabled="NO" id="8PP-wA-Pae">
|
||||
<menuItem title="Volumes" tag="3" enabled="NO" id="8PP-wA-Pae">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" tag="4" id="rkf-nx-H2J"/>
|
||||
@@ -93,7 +99,7 @@
|
||||
<accessibility description="Pan"/>
|
||||
</slider>
|
||||
<button verticalHuggingPriority="750" fixedFrame="YES" tag="1" springLoaded="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vTG-n6-GxY" customClass="BGMAVM_ShowMoreControlsButton">
|
||||
<rect key="frame" x="243" y="27" width="15" height="17"/>
|
||||
<rect key="frame" x="243" y="27" width="16" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<contentFilters>
|
||||
<ciFilter name="CIAffineTransform">
|
||||
@@ -108,7 +114,7 @@
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<textField toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9jc-9i-jw2">
|
||||
<textField identifier="PanLeft" toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9jc-9i-jw2">
|
||||
<rect key="frame" x="162" y="-1" width="12" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="L" id="hgE-7A-bez">
|
||||
@@ -116,9 +122,8 @@
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<accessibility description="Pan left"/>
|
||||
</textField>
|
||||
<textField toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1lZ-hX-6Kl">
|
||||
<textField identifier="PanRight" toolTip="Pan" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1lZ-hX-6Kl">
|
||||
<rect key="frame" x="228" y="-1" width="12" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="R" id="lzr-NO-0Na">
|
||||
@@ -126,12 +131,11 @@
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<accessibility description="Pan right"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<point key="canvasLocation" x="117.5" y="-117.5"/>
|
||||
<point key="canvasLocation" x="117" y="-45"/>
|
||||
</customView>
|
||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="Cf4-3V-gl1" customClass="NSPanel">
|
||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" animationBehavior="default" id="Cf4-3V-gl1" customClass="NSPanel">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" topStrut="YES"/>
|
||||
<rect key="contentRect" x="248" y="350" width="1002" height="335"/>
|
||||
@@ -152,7 +156,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.1.1" id="FDH-7l-wFf">
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Version 0.2.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"/>
|
||||
@@ -171,15 +175,6 @@
|
||||
<rect key="frame" x="383" y="93" width="5" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
|
||||
<rect key="frame" x="18" y="75" width="346" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016, 2017 Background Music contributors" placeholderString="" id="ctF-95-uVu">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nx6-kQ-N8Z" customClass="BGMLinkField">
|
||||
<rect key="frame" x="36" y="50" width="310" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
@@ -208,14 +203,15 @@
|
||||
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eqz-ap-PAC">
|
||||
<rect key="frame" x="415" y="45" width="567" height="245"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
|
||||
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="Cdb-RA-YK0">
|
||||
<rect key="frame" x="1" y="1" width="565" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" id="LSG-PF-cl8">
|
||||
<rect key="frame" x="-5.5" y="0.0" width="575" height="243"/>
|
||||
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" id="LSG-PF-cl8">
|
||||
<rect key="frame" x="-6" y="0.0" width="577" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="minSize" width="565" height="243"/>
|
||||
<size key="maxSize" width="594" height="10000000"/>
|
||||
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -224,13 +220,12 @@
|
||||
</allowedInputSourceLocales>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="3jV-aP-AB7">
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="3jV-aP-AB7">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
|
||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
|
||||
<rect key="frame" x="550" y="1" width="16" height="243"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
@@ -244,6 +239,15 @@
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
|
||||
<rect key="frame" x="18" y="75" width="346" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016-2018 Background Music contributors" placeholderString="" id="ctF-95-uVu">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
</view>
|
||||
<point key="canvasLocation" x="-200" y="232.5"/>
|
||||
@@ -260,58 +264,109 @@
|
||||
</textFieldCell>
|
||||
<point key="canvasLocation" x="-559" y="-118"/>
|
||||
</textField>
|
||||
<customView id="JOz-H1-mj9" userLabel="Output Volume View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="269" height="47"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<slider identifier="Output Volume" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9Ru-Sc-dqC" userLabel="Output Volume Slider">
|
||||
<rect key="frame" x="20" y="4" width="220" height="19"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<sliderCell key="cell" continuous="YES" state="on" alignment="left" maxValue="1" tickMarkPosition="above" sliderType="linear" id="MzM-fe-nKb"/>
|
||||
<accessibility description="Output Volume" help="Sets the volume of your audio output device." identifier="Output Volume"/>
|
||||
</slider>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wfC-C6-SLv">
|
||||
<rect key="frame" x="20" y="25" width="226" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Volume" id="60O-ju-B5C">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<point key="canvasLocation" x="117" y="-219"/>
|
||||
</customView>
|
||||
<customView id="dBD-CE-4dw" userLabel="Output Volume View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="269" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<slider identifier="System Sounds Volume" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gyd-WV-2ju" userLabel="Output Volume Slider">
|
||||
<rect key="frame" x="163" y="0.0" width="74" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<sliderCell key="cell" controlSize="small" continuous="YES" state="on" alignment="left" maxValue="1" doubleValue="1" tickMarkPosition="above" sliderType="linear" id="VDn-d8-XK3"/>
|
||||
<accessibility description="System Sounds Volume" help="Volume of alerts, notification sounds, etc. Usually short. Can be played by any app."/>
|
||||
</slider>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iKs-df-Hp6">
|
||||
<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"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView identifier="SystemSoundsIcon" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="V2D-eM-8yN">
|
||||
<rect key="frame" x="20" y="1" width="16" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSComputer" id="8Xp-9S-hOz"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<point key="canvasLocation" x="116.5" y="-133"/>
|
||||
</customView>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="FermataIcon" width="284" height="284"/>
|
||||
<image name="NSComputer" width="128" height="128"/>
|
||||
<image name="buttonCell:IXo-C7-3uE:image" width="1" height="1">
|
||||
<mutableData key="keyedArchiveRepresentation">
|
||||
YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU
|
||||
GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw
|
||||
c1dOU0NvbG9ygAKADRIgwwAAgAOAC1Z7MSwgMX3SFQoWGFpOUy5vYmplY3RzoReABIAK0hUKGh2iGxyA
|
||||
BYAGgAkQANIgCiEiXxAUTlNUSUZGUmVwcmVzZW50YXRpb26AB4AITxEIrE1NACoAAAAKAAAADgEAAAMA
|
||||
AAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEGAAMAAAABAAEAAAERAAQA
|
||||
AAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEWAAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMA
|
||||
AAABAAEAAAFSAAMAAAABAAEAAAFTAAMAAAACAAEAAYdzAAcAAAf0AAAAuAAAAAAAAAf0YXBwbAIgAABt
|
||||
bnRyR1JBWVhZWiAH0AACAA4ADAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYA
|
||||
AQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVk
|
||||
ZXNjAAAAwAAAAG9kc2NtAAABMAAABmZjcHJ0AAAHmAAAADh3dHB0AAAH0AAAABRrVFJDAAAH5AAAAA5k
|
||||
ZXNjAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9m
|
||||
aWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAA
|
||||
AAAfAAAADHNrU0sAAAAqAAABhGVuVVMAAAAoAAABrmNhRVMAAAAsAAAB1nZpVk4AAAAsAAACAnB0QlIA
|
||||
AAAqAAACLnVrVUEAAAAsAAACWGZyRlUAAAAqAAAChGh1SFUAAAAuAAACrnpoVFcAAAAQAAAC3G5iTk8A
|
||||
AAAsAAAC7GtvS1IAAAAYAAADGGNzQ1oAAAAkAAADMGhlSUwAAAAgAAADVHJvUk8AAAAkAAADdGRlREUA
|
||||
AAA6AAADmGl0SVQAAAAuAAAD0nN2U0UAAAAuAAAEAHpoQ04AAAAQAAAELmphSlAAAAAWAAAEPmVsR1IA
|
||||
AAAkAAAEVHB0UE8AAAA4AAAEeG5sTkwAAAAqAAAEsGVzRVMAAAAoAAAE2nRoVEgAAAAkAAAFAnRyVFIA
|
||||
AAAiAAAFJmZpRkkAAAAsAAAFSGhySFIAAAA6AAAFdHBsUEwAAAA2AAAFrnJ1UlUAAAAmAAAF5GFyRUcA
|
||||
AAAoAAAGCmRhREsAAAA0AAAGMgBWAWEAZQBvAGIAZQBjAG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkA
|
||||
bABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAFAAcgBvAGYAaQBsAGUAUABlAHIAZgBpAGwAIABkAGUA
|
||||
IABnAHIAaQBzACAAZwBlAG4A6AByAGkAYwBDHqUAdQAgAGgA7ABuAGgAIABNAOAAdQAgAHgA4QBtACAA
|
||||
QwBoAHUAbgBnAFAAZQByAGYAaQBsACAAQwBpAG4AegBhACAARwBlAG4A6QByAGkAYwBvBBcEMAQzBDAE
|
||||
OwRMBD0EOAQ5ACAEPwRABD4ERAQwBDkEOwAgAEcAcgBhAHkAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIA
|
||||
aQBxAHUAZQAgAGcAcgBpAHMAwQBsAHQAYQBsAOEAbgBvAHMAIABzAHoA/AByAGsAZQAgAHAAcgBvAGYA
|
||||
aQBskBp1KHBwlo6Ccl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QB0AG8AbgBlAHAAcgBvAGYA
|
||||
aQBsx3y8GAAgAEcAcgBhAHkAINUEuFzTDMd8AE8AYgBlAGMAbgD9ACABYQBlAGQA/QAgAHAAcgBvAGYA
|
||||
aQBsBeQF6AXVBeQF2QXcACAARwByAGEAeQAgBdsF3AXcBdkAUAByAG8AZgBpAGwAIABnAHIAaQAgAGcA
|
||||
ZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAARwByAGEAdQBzAHQAdQBmAGUAbgAtAFAA
|
||||
cgBvAGYAaQBsAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwBHAGUA
|
||||
bgBlAHIAaQBzAGsAIABnAHIA5QBzAGsAYQBsAGUAcAByAG8AZgBpAGxmbpAacHBepmPPj/Blh072TgCC
|
||||
LDCwMOwwpDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA8ADwQO/A8YDrwO7ACADswO6A8EDuQBQAGUA
|
||||
cgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMAaQBuAHoAZQBuAHQAbwBzAEEAbABnAGUA
|
||||
bQBlAGUAbgAgAGcAcgBpAGoAcwBwAHIAbwBmAGkAZQBsAFAAZQByAGYAaQBsACAAZwByAGkAcwAgAGcA
|
||||
ZQBuAOkAcgBpAGMAbw5CDhsOIw5EDh8OJQ5MDioONQ5ADhcOMg4XDjEOSA4nDkQOGwBHAGUAbgBlAGwA
|
||||
IABHAHIAaQAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAHAAcgBvAGYA
|
||||
aQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAHAAcgBvAGYAaQBsACAAcwBpAHYAaQBoACAAdABvAG4A
|
||||
bwB2AGEAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABzAHoAYQByAG8BWwBjAGkE
|
||||
HgQxBEkEOAQ5ACAEQQQ1BEAESwQ5ACAEPwRABD4ERAQ4BDsETAZFBkQGQQAgBioGOQYxBkoGQQAgAEcA
|
||||
cgBhAHkAIAYnBkQGOQYnBkUARwBlAG4AZQByAGUAbAAgAGcAcgDlAHQAbwBuAGUAYgBlAHMAawByAGkA
|
||||
dgBlAGwAcwBlAAB0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdodHMgcmVz
|
||||
ZXJ2ZWQuAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAAAAABAc0AANIlJicoWiRjbGFzc25hbWVY
|
||||
JGNsYXNzZXNfEBBOU0JpdG1hcEltYWdlUmVwoycpKlpOU0ltYWdlUmVwWE5TT2JqZWN00iUmLC1XTlNB
|
||||
cnJheaIsKtIlJi8wXk5TTXV0YWJsZUFycmF5oy8sKtMyMwo0NTZXTlNXaGl0ZVxOU0NvbG9yU3BhY2VE
|
||||
MCAwABADgAzSJSY4OVdOU0NvbG9yojgq0iUmOzxXTlNJbWFnZaI7Kl8QD05TS2V5ZWRBcmNoaXZlctE/
|
||||
QFRyb290gAEACAARABoAIwAtADIANwBGAEwAVwBeAGUAcgB5AIEAgwCFAIoAjACOAJUAmgClAKcAqQCr
|
||||
ALAAswC1ALcAuQC7AMAA1wDZANsJiwmQCZsJpAm3CbsJxgnPCdQJ3AnfCeQJ8wn3Cf4KBgoTChgKGgoc
|
||||
CiEKKQosCjEKOQo8Ck4KUQpWAAAAAAAAAgEAAAAAAAAAQQAAAAAAAAAAAAAAAAAAClg
|
||||
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
|
||||
AAAAAAAAAAAAAAAACnA
|
||||
</mutableData>
|
||||
</image>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.1</string>
|
||||
<string>0.2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -28,12 +28,16 @@
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>Background Music needs to control your music player app if you want it to automatically pause your music.</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2018 Background Music contributors</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The "Background Music" virtual audio device sends system audio to Background Music (the app) through a virtual input device.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSServices</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMDecibel.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
@@ -31,9 +31,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -42,11 +40,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"A9790CD5-4886-47C7-9FFC-DD70743CF2BF"]
|
||||
name:@"Decibel"
|
||||
bundleID:@"org.sbooth.Decibel"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -56,6 +54,11 @@
|
||||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.decibel.running;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMHermes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,9 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -41,12 +39,12 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen. (Command line tool.)
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"0CDC67B0-56D3-4D94-BC06-6E380D8F5E34"]
|
||||
name:@"Hermes"
|
||||
bundleID:@"com.alexcrichton.Hermes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -56,6 +54,11 @@
|
||||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.hermes is nil (i.e. Hermes isn't running).
|
||||
return self.hermes.running;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
// The base classes and protocol for objects that represent a music player app.
|
||||
//
|
||||
@@ -84,6 +84,9 @@
|
||||
// BGMMusicPlayers could pass a pointer to itself to createInstances.
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
// True if this is currently the selected music player.
|
||||
@property (readonly) BOOL selected;
|
||||
|
||||
// The state of the music player.
|
||||
//
|
||||
// True if the music player app is open.
|
||||
@@ -97,10 +100,14 @@
|
||||
// BGMApp paused it.
|
||||
@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;
|
||||
|
||||
// Pause the music player. Does nothing if the music player is already paused or isn't running.
|
||||
// Returns YES if the music player is paused now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) pause;
|
||||
|
||||
// Unpause the music player. Does nothing if the music player is already playing or isn't running.
|
||||
// Returns YES if the music player is playing now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) unpause;
|
||||
@@ -130,6 +137,9 @@
|
||||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@property NSNumber* __nullable pid;
|
||||
@property (readonly) BOOL selected;
|
||||
- (void) onSelect;
|
||||
- (void) onDeselect;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -17,16 +17,14 @@
|
||||
// BGMMusicPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -37,6 +35,7 @@
|
||||
@synthesize name = _name;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
@synthesize selected = _selected;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
@@ -58,6 +57,7 @@
|
||||
_name = name;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -84,6 +84,14 @@
|
||||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
_selected = YES;
|
||||
}
|
||||
|
||||
- (void) onDeselect {
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayers.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
@@ -33,6 +33,7 @@
|
||||
#import "BGMVOX.h"
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMHermes.h"
|
||||
#import "BGMSwinsian.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -54,7 +55,8 @@
|
||||
[BGMSpotify class],
|
||||
[BGMiTunes class],
|
||||
[BGMDecibel class],
|
||||
[BGMHermes class] ]
|
||||
[BGMHermes class],
|
||||
[BGMSwinsian class] ]
|
||||
userDefaults:defaults];
|
||||
}
|
||||
|
||||
@@ -133,8 +135,8 @@
|
||||
// backwards compatability.
|
||||
|
||||
NSString* __nullable bundleID =
|
||||
(__bridge_transfer NSString* __nullable)[audioDevices bgmDevice].GetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress);
|
||||
|
||||
(__bridge_transfer NSString* __nullable)[audioDevices bgmDevice].GetMusicPlayerBundleID();
|
||||
|
||||
DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromBGMDevice: "
|
||||
"Trying to set selected music player by bundle ID (from BGMDriver). bundleID=%s",
|
||||
(bundleID ? bundleID.UTF8String : "(null)"));
|
||||
@@ -204,6 +206,16 @@
|
||||
@"BGMMusicPlayers::setSelectedMusicPlayerImpl: Only the music players in the musicPlayers array can be selected. "
|
||||
"newSelectedMusicPlayer=%@",
|
||||
newSelectedMusicPlayer.name);
|
||||
|
||||
if (_selectedMusicPlayer == newSelectedMusicPlayer) {
|
||||
DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: %s is already the selected music "
|
||||
"player.",
|
||||
_selectedMusicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the current music player (object) a different player has been selected.
|
||||
[_selectedMusicPlayer onDeselect];
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
@@ -215,6 +227,9 @@
|
||||
|
||||
// Save the new setting in user defaults.
|
||||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
|
||||
// Tell the music player (object) it's been selected.
|
||||
[_selectedMusicPlayer onSelect];
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
@@ -224,13 +239,11 @@
|
||||
@"BGMMusicPlayers::updateBGMDeviceMusicPlayerProperties: Music player has neither bundle ID nor PID");
|
||||
|
||||
if (self.selectedMusicPlayer.pid) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFType(kBGMMusicPlayerProcessIDAddress,
|
||||
(__bridge CFNumberRef)self.selectedMusicPlayer.pid);
|
||||
[audioDevices bgmDevice].SetMusicPlayerProcessID((__bridge CFNumberRef)self.selectedMusicPlayer.pid);
|
||||
}
|
||||
|
||||
if (self.selectedMusicPlayer.bundleID) {
|
||||
[audioDevices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress,
|
||||
(__bridge CFStringRef)self.selectedMusicPlayer.bundleID);
|
||||
[audioDevices bgmDevice].SetMusicPlayerBundleID((__bridge CFStringRef)self.selectedMusicPlayer.bundleID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMScriptingBridge.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
// A wrapper around Scripting Bridge's SBApplication that tries to avoid ever launching the application.
|
||||
//
|
||||
@@ -29,6 +29,9 @@
|
||||
// unless the music player app is running. That way messages sent while the app is closed are ignored.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
@@ -38,12 +41,19 @@
|
||||
|
||||
@interface BGMScriptingBridge : NSObject <SBApplicationDelegate>
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID;
|
||||
// Only keeps a weak ref to musicPlayer.
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer;
|
||||
|
||||
// If the music player application is running, this property is the Scripting Bridge object representing
|
||||
// it. If not, it's set to nil. Used to send Apple events to the music player app.
|
||||
@property (readonly) __kindof SBApplication* __nullable application;
|
||||
|
||||
// macOS 10.14 requires the user's permission to send Apple Events. If the music player that owns
|
||||
// this object (i.e. the one passed to initWithMusicPlayer) is currently the selected music player
|
||||
// and the user hasn't already given us permission to send it Apple Events, this method asks the
|
||||
// user for permission.
|
||||
- (void) ensurePermission;
|
||||
|
||||
// SBApplicationDelegate
|
||||
|
||||
// On 10.11, SBApplicationDelegate.h declares eventDidFail with a non-null return type, but the docs
|
||||
|
||||
@@ -17,31 +17,32 @@
|
||||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMScriptingBridge {
|
||||
NSString* bundleID;
|
||||
id<BGMMusicPlayer> __weak _musicPlayer;
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id didLaunchToken, didTerminateToken;
|
||||
id _didLaunchToken, _didTerminateToken;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)inBundleID {
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer {
|
||||
if ((self = [super init])) {
|
||||
bundleID = inBundleID;
|
||||
_musicPlayer = musicPlayer;
|
||||
|
||||
[self initApplication];
|
||||
}
|
||||
@@ -50,9 +51,17 @@
|
||||
}
|
||||
|
||||
- (void) initApplication {
|
||||
NSString* bundleID = _musicPlayer.bundleID;
|
||||
BGMAssert(bundleID, "Music players need a bundle ID to use ScriptingBridge");
|
||||
|
||||
BGMScriptingBridge* __weak weakSelf = self;
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
_application.delegate = self;
|
||||
BGMScriptingBridge* __strong 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.
|
||||
strongSelf->_application.delegate = strongSelf;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
@@ -68,26 +77,30 @@
|
||||
// "For applications that declare themselves to have a dynamic scripting interface, this method will
|
||||
// launch the application if it is not already running."
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
}
|
||||
}];
|
||||
didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
_application = nil;
|
||||
}
|
||||
}];
|
||||
_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;
|
||||
}
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) {
|
||||
@@ -99,15 +112,69 @@
|
||||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
if (_didLaunchToken) {
|
||||
[center removeObserver:_didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
if (_didTerminateToken) {
|
||||
[center removeObserver:_didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) ensurePermission {
|
||||
// Skip this check if running on a version of macOS before 10.14. In that case, we don't require
|
||||
// user permission to send Apple Events. Also skip it if compiling on an earlier version.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
|
||||
if (@available(macOS 10.14, *)) {
|
||||
id<BGMMusicPlayer> musicPlayer = _musicPlayer;
|
||||
|
||||
if (!musicPlayer.selected) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not selected. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!musicPlayer.running) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not running. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// AEDeterminePermissionToAutomateTarget will block if it has to show a dialog to the user
|
||||
// to ask for permission, so dispatch this to make sure it doesn't run on the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
NSAppleEventDescriptor* musicPlayerEventDescriptor =
|
||||
[NSAppleEventDescriptor
|
||||
descriptorWithBundleIdentifier:(NSString*)musicPlayer.bundleID];
|
||||
|
||||
OSStatus status =
|
||||
AEDeterminePermissionToAutomateTarget(musicPlayerEventDescriptor.aeDesc,
|
||||
typeWildCard,
|
||||
typeWildCard,
|
||||
true);
|
||||
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: "
|
||||
"Apple Events permission status for %s: %d",
|
||||
musicPlayer.name.UTF8String,
|
||||
status);
|
||||
|
||||
if (status != noErr) {
|
||||
// TODO: If they deny permission, we should grey-out the auto-pause menu item and
|
||||
// add something to the UI that indicates the problem. Maybe a warning icon
|
||||
// that shows an explanation when you hover your mouse over it. (We can't just
|
||||
// ask them again later because the API doesn't support it. They can only fix
|
||||
// it in System Preferences.)
|
||||
NSLog(@"BGMScriptingBridge::ensurePermission: Permission denied for %@. status=%d",
|
||||
musicPlayer.name,
|
||||
status);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: Not macOS 10.14+. Nothing to do.");
|
||||
}
|
||||
#endif /* MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 */
|
||||
}
|
||||
|
||||
#pragma mark SBApplicationDelegate
|
||||
|
||||
#pragma clang diagnostic push
|
||||
@@ -120,7 +187,7 @@
|
||||
NSString* vars = [NSString stringWithFormat:@"event='%4.4s' error=%@ application=%@",
|
||||
(char*)&(event->descriptorType), error, self.application];
|
||||
DebugMsg("BGMScriptingBridge::eventDidFail: Apple event sent to %s failed. %s",
|
||||
bundleID.UTF8String,
|
||||
_musicPlayer.bundleID.UTF8String,
|
||||
vars.UTF8String);
|
||||
#else
|
||||
#pragma unused (event, error)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMSpotify.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
// Spotify's AppleScript API looks to have been designed to match iTunes', so this file is basically
|
||||
// just s/iTunes/Spotify/ on BGMiTunes.m
|
||||
@@ -33,9 +33,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -44,12 +42,12 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen. (Command line tool.)
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"EC2A907F-8515-4687-9570-1BF63176E6D8"]
|
||||
name:@"Spotify"
|
||||
bundleID:@"com.spotify.client"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -59,6 +57,11 @@
|
||||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.spotify is nil (i.e. Spotify isn't running).
|
||||
return self.spotify.running;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMSwinsian.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Superclass/Protocol Import
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@interface BGMSwinsian : BGMMusicPlayerBase<BGMMusicPlayer>
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMSwinsian.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMSwinsian.h"
|
||||
|
||||
// Auto-generated Scripting Bridge header
|
||||
#import "Swinsian.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMSwinsian {
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (instancetype) init {
|
||||
// If you're copying this class, replace the ID string with a new one generated by uuidgen (the
|
||||
// command line tool).
|
||||
NSUUID* musicPlayerID = [BGMMusicPlayerBase makeID:@"B74D18F6-DFF7-4D88-B719-429CFF98CFFA"];
|
||||
|
||||
if ((self = [super initWithMusicPlayerID:musicPlayerID
|
||||
name:@"Swinsian"
|
||||
bundleID:@"com.swinsian.Swinsian"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (SwinsianApplication* __nullable) swinsian {
|
||||
return (SwinsianApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.swinsian is nil (i.e. Swinsian isn't running).
|
||||
return self.swinsian.running;
|
||||
}
|
||||
|
||||
// isPlaying and isPaused check self.running first just in case Swinsian is closed but self.swinsian
|
||||
// hasn't become nil yet. In that case, reading self.swinsian.playerState could make Scripting
|
||||
// Bridge open Swinsian.
|
||||
|
||||
- (BOOL) isPlaying {
|
||||
return self.running && (self.swinsian.playerState == SwinsianPlayerStatePlaying);
|
||||
}
|
||||
|
||||
- (BOOL) isPaused {
|
||||
return self.running && (self.swinsian.playerState == SwinsianPlayerStatePaused);
|
||||
}
|
||||
|
||||
- (BOOL) pause {
|
||||
// isPlaying checks isRunning, so we don't need to check it here and waste an Apple event.
|
||||
BOOL wasPlaying = self.playing;
|
||||
|
||||
if (wasPlaying) {
|
||||
DebugMsg("BGMSwinsian::pause: Pausing Swinsian");
|
||||
[self.swinsian pause];
|
||||
}
|
||||
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
- (BOOL) unpause {
|
||||
// isPaused checks isRunning, so we don't need to check it here and waste an Apple event.
|
||||
BOOL wasPaused = self.paused;
|
||||
|
||||
if (wasPaused) {
|
||||
DebugMsg("BGMSwinsian::unpause: Unpausing Swinsian");
|
||||
[self.swinsian play];
|
||||
}
|
||||
|
||||
return wasPaused;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMVLC.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Portions copyright (C) 2012 Peter Ljunglöf. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -31,9 +31,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -42,11 +40,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"]
|
||||
name:@"VLC"
|
||||
bundleID:@"org.videolan.vlc"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -56,6 +54,11 @@
|
||||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vlc.running;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMVOX.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,9 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -41,11 +39,11 @@
|
||||
BGMScriptingBridge* scriptingBridge;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"26498C5D-C18B-4689-8B41-9DA91A78FFAD"]
|
||||
name:@"VOX"
|
||||
bundleID:@"com.coppertino.Vox"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -55,6 +53,11 @@
|
||||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vox.running;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMiTunes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -30,9 +30,7 @@
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -47,11 +45,11 @@
|
||||
return (NSUUID*)musicPlayerID;
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super initWithMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
name:@"iTunes"
|
||||
bundleID:@"com.apple.iTunes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString* __nonnull)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -61,6 +59,11 @@
|
||||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.iTunes.running;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Swinsian.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef /Applications/Swinsian.app | sdp -fh --basename Swinsian
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@class SwinsianItem, SwinsianColor, SwinsianWindow, SwinsianApplication, SwinsianPlaylist, SwinsianLibrary, SwinsianTrack, SwinsianLibraryTrack, SwinsianIPodTrack, SwinsianQueue, SwinsianSmartPlaylist, SwinsianNormalPlaylist, SwinsianPlaylistFolder, SwinsianAudioDevice;
|
||||
|
||||
enum SwinsianSaveOptions {
|
||||
SwinsianSaveOptionsYes = 'yes ' /* Save the file. */,
|
||||
SwinsianSaveOptionsNo = 'no ' /* Do not save the file. */,
|
||||
SwinsianSaveOptionsAsk = 'ask ' /* Ask the user whether or not to save the file. */
|
||||
};
|
||||
typedef enum SwinsianSaveOptions SwinsianSaveOptions;
|
||||
|
||||
enum SwinsianPlayerState {
|
||||
SwinsianPlayerStateStopped = 'kPSS',
|
||||
SwinsianPlayerStatePlaying = 'kPSP',
|
||||
SwinsianPlayerStatePaused = 'kPSp'
|
||||
};
|
||||
typedef enum SwinsianPlayerState SwinsianPlayerState;
|
||||
|
||||
@protocol SwinsianGenericMethods
|
||||
|
||||
- (void) closeSaving:(SwinsianSaveOptions)saving savingIn:(NSURL *)savingIn; // Close an object.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void) duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location.
|
||||
- (BOOL) exists; // Verify if an object exists.
|
||||
- (void) moveTo:(SBObject *)to; // Move object(s) to a new location.
|
||||
- (void) saveIn:(NSURL *)in_ as:(NSString *)as; // Save an object.
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Standard Suite
|
||||
*/
|
||||
|
||||
// A scriptable object.
|
||||
@interface SwinsianItem : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy) NSDictionary *properties; // All of the object's properties.
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A color.
|
||||
@interface SwinsianColor : SBObject <SwinsianGenericMethods>
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// A window.
|
||||
@interface SwinsianWindow : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy) NSString *name; // The full title of the window.
|
||||
- (NSNumber *) id; // The unique identifier of the window.
|
||||
@property NSRect bounds; // The bounding rectangle of the window.
|
||||
@property (readonly) BOOL closeable; // Whether the window has a close box.
|
||||
@property (readonly) BOOL titled; // Whether the window has a title bar.
|
||||
@property (copy) NSNumber *index; // The index of the window in the back-to-front window ordering.
|
||||
@property (readonly) BOOL floating; // Whether the window floats.
|
||||
@property (readonly) BOOL miniaturizable; // Whether the window can be miniaturized.
|
||||
@property BOOL miniaturized; // Whether the window is currently miniaturized.
|
||||
@property (readonly) BOOL modal; // Whether the window is the application's current modal window.
|
||||
@property (readonly) BOOL resizable; // Whether the window can be resized.
|
||||
@property BOOL visible; // Whether the window is currently visible.
|
||||
@property (readonly) BOOL zoomable; // Whether the window can be zoomed.
|
||||
@property BOOL zoomed; // Whether the window is currently zoomed.
|
||||
@property (copy, readonly) NSArray<SwinsianTrack *> *selection; // Currently seleted tracks
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Swinsian Suite
|
||||
*/
|
||||
|
||||
// The application
|
||||
@interface SwinsianApplication : SBApplication
|
||||
|
||||
- (SBElementArray<SwinsianWindow *> *) windows;
|
||||
- (SBElementArray<SwinsianPlaylist *> *) playlists;
|
||||
- (SBElementArray<SwinsianSmartPlaylist *> *) smartPlaylists;
|
||||
- (SBElementArray<SwinsianNormalPlaylist *> *) normalPlaylists;
|
||||
- (SBElementArray<SwinsianLibrary *> *) libraries;
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
- (SBElementArray<SwinsianAudioDevice *> *) audioDevices;
|
||||
|
||||
@property (copy, readonly) NSString *name; // The name of the application.
|
||||
@property (readonly) BOOL frontmost; // Is this the frontmost (active) application?
|
||||
@property (copy, readonly) NSString *version; // The version of the application.
|
||||
@property NSInteger playerPosition; // the player’s position within the currently playing track in seconds.
|
||||
@property (copy, readonly) SwinsianTrack *currentTrack; // the currently playing track
|
||||
@property (copy) NSNumber *soundVolume; // the volume. (0 minimum, 100 maximum)
|
||||
@property (readonly) SwinsianPlayerState playerState; // are we stopped, paused or still playing?
|
||||
@property (copy, readonly) SwinsianQueue *playbackQueue; // the currently queued tracks
|
||||
@property (copy) SwinsianAudioDevice *outputDevice; // current audio output device
|
||||
|
||||
- (void) open:(NSURL *)x; // Open an object.
|
||||
- (void) print:(NSURL *)x; // Print an object.
|
||||
- (void) quitSaving:(SwinsianSaveOptions)saving; // Quit an application.
|
||||
- (void) play; // begin playing the current playlist
|
||||
- (void) pause; // pause playback
|
||||
- (void) nextTrack; // skip to the next track in the current playlist
|
||||
- (void) stop; // stop playback
|
||||
- (NSArray<SwinsianTrack *> *) searchPlaylist:(SwinsianPlaylist *)playlist for:(NSString *)for_; // search a playlist for tracks matching a string
|
||||
- (void) previousTrack; // skip back to the previous track
|
||||
- (void) playpause; // toggle play/pause
|
||||
- (void) addTracks:(NSArray<SwinsianTrack *> *)tracks to:(SwinsianNormalPlaylist *)to; // add a track to a playlist
|
||||
- (void) notify; // show currently playing track notification
|
||||
- (void) rescanTags:(NSArray<SwinsianTrack *> *)x; // rescan tags on tracks
|
||||
- (NSArray<SwinsianTrack *> *) findTrack:(NSString *)x; // Finds tracks for the given path
|
||||
- (void) removeTracks:(NSArray<SwinsianTrack *> *)tracks from:(SwinsianNormalPlaylist *)from; // remove tracks from a playlist
|
||||
|
||||
@end
|
||||
|
||||
// generic playlist type, subcasses include smart playlist and normal playlist
|
||||
@interface SwinsianPlaylist : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
@property (copy) NSString *name; // the name of the playlist
|
||||
@property (readonly) BOOL smart; // is this a smart playlist
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianLibrary : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a music track
|
||||
@interface SwinsianTrack : SwinsianItem
|
||||
|
||||
@property (copy) NSString *album; // the album of the track
|
||||
@property (copy) NSString *artist; // the artist
|
||||
@property (copy) NSString *composer; // the composer
|
||||
@property (copy) NSString *genre; // the genre
|
||||
@property (copy, readonly) NSString *time; // the length of the track in text format as MM:SS
|
||||
@property NSInteger year; // the year the track was recorded
|
||||
@property (copy, readonly) NSDate *dateAdded; // the date the track was added to the library
|
||||
@property (readonly) double duration; // the length of the track in seconds
|
||||
@property (copy, readonly) NSString *location; // location on disk
|
||||
@property (readonly) BOOL iPodTrack; // TRUE if the track is on an iPod
|
||||
@property (copy) NSString *name; // the title of the track (same as title)
|
||||
@property (readonly) NSInteger bitRate; // the bitrate of the track
|
||||
@property (copy, readonly) NSString *kind; // a text description of the type of file the track is
|
||||
@property (copy) NSNumber *rating; // Track rating. 0-5
|
||||
@property NSInteger trackNumber; // the Track number
|
||||
@property (readonly) NSInteger fileSize; // file size in bytes
|
||||
@property (copy, readonly) NSImage *albumArt; // the album artwork
|
||||
@property (copy, readonly) NSString *artFormat; // the data format for this piece of artwork. text that will be "PNG" or "JPEG". getting the album art property first will mean this information has been retrieved already, otherwise the tags for the file will have to be re-read
|
||||
@property (copy) NSNumber *discNumber; // the disc number
|
||||
@property (copy) NSNumber *discCount; // the total number of discs in the album
|
||||
- (NSString *) id; // uuid
|
||||
@property (copy) NSString *albumArtist; // the album artist
|
||||
@property (copy, readonly) NSString *albumArtistOrArtist; // the album artist of the track, or is none is set, the artist
|
||||
@property BOOL compilation; // compilation flag
|
||||
@property (copy) NSString *title; // track title (the same as name)
|
||||
@property (copy) NSString *comment; // the comment
|
||||
@property (copy, readonly) NSDate *dateCreated; // the date created
|
||||
@property (readonly) NSInteger channels; // audio channel count
|
||||
@property (readonly) NSInteger sampleRate; // audio sample rate
|
||||
@property (readonly) NSInteger bitDepth; // the audio bit depth
|
||||
@property (copy) NSDate *lastPlayed; // date track was last played
|
||||
@property (copy) NSString *lyrics; // track lyrics
|
||||
@property (copy, readonly) NSString *path; // POSIX style path
|
||||
@property (copy) NSString *grouping; // grouping
|
||||
@property (copy) NSString *publisher; // the publisher
|
||||
@property (copy) NSString *conductor; // the conductor
|
||||
@property (copy) NSString *objectDescription; // the description
|
||||
@property (copy, readonly) NSString *encoder; // the encoder
|
||||
@property (copy, readonly) NSString *copyright; // the copyright
|
||||
@property (copy) NSString *catalogNumber; // the catalog number
|
||||
@property (copy, readonly) NSDate *dateModified; // the date modified
|
||||
@property NSInteger playCount; // the play count
|
||||
@property (copy) NSNumber *trackCount; // the total number of tracks in the album
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianLibraryTrack : SwinsianTrack
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface SwinsianIPodTrack : SwinsianTrack
|
||||
|
||||
@property (copy, readonly) NSString *iPodName; // the name of the iPod this track is on
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// The playback queue
|
||||
@interface SwinsianQueue : SwinsianItem
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a smart playlist
|
||||
@interface SwinsianSmartPlaylist : SwinsianPlaylist
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// a normal, non-smart, playlist
|
||||
@interface SwinsianNormalPlaylist : SwinsianPlaylist
|
||||
|
||||
- (SBElementArray<SwinsianTrack *> *) tracks;
|
||||
|
||||
- (NSString *) id; // uuid
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// folder of playlists
|
||||
@interface SwinsianPlaylistFolder : SwinsianPlaylist
|
||||
|
||||
- (SBElementArray<SwinsianPlaylist *> *) playlists;
|
||||
|
||||
- (NSString *) id; // uuid
|
||||
|
||||
|
||||
@end
|
||||
|
||||
// an audio output device
|
||||
@interface SwinsianAudioDevice : SBObject <SwinsianGenericMethods>
|
||||
|
||||
@property (copy, readonly) NSString *name; // device name
|
||||
- (NSString *) id; // uuid
|
||||
- (void) setId: (NSString *) id;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
#import "BGM_Types.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
|
||||
@@ -93,10 +91,11 @@ static NSInteger const kProjectWebsiteLabelTag = 3;
|
||||
websiteLabel.allowsEditingTextAttributes = YES;
|
||||
|
||||
NSString* projectURL = [NSString stringWithUTF8String:kBGMProjectURL];
|
||||
NSFont* linkFont = websiteLabel.font ? websiteLabel.font : [NSFont labelFontOfSize:0.0];
|
||||
websiteLabel.attributedStringValue =
|
||||
[[NSAttributedString alloc] initWithString:projectURL
|
||||
attributes:@{ NSLinkAttributeName: projectURL,
|
||||
NSFontAttributeName: websiteLabel.font }];
|
||||
NSFontAttributeName: linkFont }];
|
||||
|
||||
// Load the text of the license into the text view
|
||||
NSString* __nullable licensePath = [bundle pathForResource:@"LICENSE" ofType:nil];
|
||||
@@ -112,7 +111,7 @@ static NSInteger const kProjectWebsiteLabelTag = 3;
|
||||
licenseStr = @"Error: could not open license file.";
|
||||
}
|
||||
|
||||
licenseView.string = licenseStr;
|
||||
licenseView.string = (NSString*)licenseStr;
|
||||
|
||||
NSFont* __nullable font = [NSFont fontWithName:@"Andale Mono" size:0.0];
|
||||
if (font) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMOutputDevicePrefs.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -187,7 +187,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
item.toolTip = toolTip;
|
||||
item.target = self;
|
||||
item.indentationLevel = 1;
|
||||
item.representedObject = @{ @"deviceID": [NSNumber numberWithUnsignedInt:device.GetObjectID()],
|
||||
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
|
||||
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
|
||||
|
||||
return item;
|
||||
@@ -216,10 +216,10 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
menuItem.toolTip ?
|
||||
[NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] :
|
||||
menuItem.title;
|
||||
|
||||
// Dispatched because it usually blocks. (Note that we're using QOS_CLASS_USER_INITIATED
|
||||
// rather than QOS_CLASS_USER_INTERACTIVE.)
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
|
||||
// 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), ^{
|
||||
[self changeToOutputDevice:newDeviceID
|
||||
newDataSource:newDataSourceID
|
||||
deviceName:deviceName];
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
|
||||
- (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key {
|
||||
#pragma unused (sender)
|
||||
DebugMsg("BGMAppDelegate:application:delegateHandlesKey: Key queried: '%s'", [key UTF8String]);
|
||||
|
||||
if (![key isEqual:@"_keyWindow"]) {
|
||||
DebugMsg("BGMAppDelegate:application:delegateHandlesKey: Key queried: '%s'",
|
||||
[key UTF8String]);
|
||||
}
|
||||
|
||||
return [@[@"selectedOutputDevice", @"outputDevices"] containsObject:key];
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
// Start BGMApp.
|
||||
return NSApplicationMain(argc, argv);
|
||||
}
|
||||
|
||||
|
||||
+64
-1
@@ -14,7 +14,7 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMAppUITests.m
|
||||
// BGMAppUITests.mm
|
||||
// BGMAppUITests
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
@@ -25,6 +25,7 @@
|
||||
// Local Includes
|
||||
#import "BGM_TestUtils.h"
|
||||
#import "BGM_Types.h"
|
||||
#import "BGMBackgroundMusicDevice.h"
|
||||
|
||||
// Scripting Bridge Includes
|
||||
#import "BGMApp.h"
|
||||
@@ -169,5 +170,67 @@
|
||||
XCTAssert(menuItems[@"Auto-pause iTunes"].exists);
|
||||
}
|
||||
|
||||
- (void) testOutputVolumeSlider {
|
||||
const AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
|
||||
const UInt32 channel = kMasterChannel;
|
||||
|
||||
[icon click];
|
||||
|
||||
XCUIElement* slider = menuItems.sliders[@"Output Volume"];
|
||||
|
||||
// Try to slide the slider all the way to the right.
|
||||
[slider adjustToNormalizedSliderPosition:1.0f];
|
||||
|
||||
// For whatever reason, XCTest usually doesn't quite make it to the position you ask for. So
|
||||
// just check that it got close enough.
|
||||
XCTAssertGreaterThan(slider.normalizedSliderPosition, 0.9f);
|
||||
|
||||
// BGMDevice's volume should be set to its max, or as close as XCTest was able to get the
|
||||
// slider. Probably shouldn't be comparing floats for equality like this, but it's working fine
|
||||
// so far.
|
||||
BGMBackgroundMusicDevice bgmDevice;
|
||||
XCTAssertEqual(slider.normalizedSliderPosition,
|
||||
bgmDevice.GetVolumeControlScalarValue(scope, channel));
|
||||
|
||||
// Try to slide the slider all the way to the left.
|
||||
[slider adjustToNormalizedSliderPosition:0.0f];
|
||||
|
||||
// BGMDevice's volume should be set to the new value of the slider.
|
||||
XCTAssertLessThan(slider.normalizedSliderPosition, 0.1f);
|
||||
XCTAssertEqual(slider.normalizedSliderPosition,
|
||||
bgmDevice.GetVolumeControlScalarValue(scope, channel));
|
||||
|
||||
// Try to slide the slider to 75%.
|
||||
[slider adjustToNormalizedSliderPosition:0.75f];
|
||||
|
||||
// BGMDevice's volume should be set to the new value of the slider, about 75% of its max.
|
||||
XCTAssertEqual(slider.normalizedSliderPosition,
|
||||
bgmDevice.GetVolumeControlScalarValue(scope, channel));
|
||||
|
||||
// BGMDevice should be unmuted.
|
||||
XCTAssertEqual(false, bgmDevice.GetMuteControlValue(scope, channel));
|
||||
|
||||
// Set BGMDevice's volume to its min.
|
||||
bgmDevice.SetVolumeControlScalarValue(scope, channel, 0.0f);
|
||||
|
||||
// The slider should be set to its min value. Use a wait for this check because the change
|
||||
// happens asynchronously.
|
||||
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"normalizedSliderPosition == 0"]
|
||||
evaluatedWithObject:slider
|
||||
handler:nil];
|
||||
[self waitForExpectationsWithTimeout:10.0 handler:nil];
|
||||
|
||||
XCTAssertEqual(0.0f, slider.normalizedSliderPosition);
|
||||
|
||||
// Click the slider without changing it to simulate the user setting the slider to zero.
|
||||
[slider adjustToNormalizedSliderPosition:0.0f];
|
||||
|
||||
// BGMDevice's volume should still be set to its min.
|
||||
XCTAssertEqual(0.0f, bgmDevice.GetVolumeControlScalarValue(scope, channel));
|
||||
|
||||
// BGMDevice should now be muted.
|
||||
XCTAssertEqual(true, bgmDevice.GetMuteControlValue(scope, channel));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGMMusicPlayersUnitTests.mm
|
||||
// BGMAppUnitTests
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Unit include
|
||||
@@ -33,26 +33,13 @@
|
||||
#import "BGMDecibel.h"
|
||||
#import "BGMSpotify.h"
|
||||
|
||||
// PublicUtility includes
|
||||
#import "CAHALAudioDevice.h" // Mocked
|
||||
|
||||
// System includes
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
@interface BGMMockAudioDeviceManager : BGMAudioDeviceManager
|
||||
@end
|
||||
|
||||
@implementation BGMMockAudioDeviceManager
|
||||
|
||||
+ (CAHALAudioDevice) bgmDevice {
|
||||
static CAHALAudioDevice device(CFSTR("MockBGMDevice"));
|
||||
return device;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ----
|
||||
// Note that the PublicUtility classes that we use to communicate with the HAL, CAHALAudioObject and
|
||||
// CAHALAudioSystemObject, are also mocked. The unit tests are compiled with mock implementations:
|
||||
// Mock_CAHALAudioObject.cpp and Mock_CAHALAudioSystemObject.cpp.
|
||||
|
||||
@interface BGMMockUserDefaults : BGMUserDefaults
|
||||
|
||||
@@ -83,13 +70,54 @@
|
||||
|
||||
@end
|
||||
|
||||
// ----
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
class BGMMockBackgroundMusicDevice
|
||||
:
|
||||
public BGMBackgroundMusicDevice
|
||||
{
|
||||
|
||||
public:
|
||||
CFStringRef GetMusicPlayerBundleID() const;
|
||||
void SetMusicPlayerBundleID(CFStringRef inBundleID);
|
||||
|
||||
private:
|
||||
CFStringRef mMusicPlayerBundleID = CFSTR("");
|
||||
|
||||
};
|
||||
|
||||
CFStringRef BGMMockBackgroundMusicDevice::GetMusicPlayerBundleID() const
|
||||
{
|
||||
return mMusicPlayerBundleID;
|
||||
}
|
||||
|
||||
void BGMMockBackgroundMusicDevice::SetMusicPlayerBundleID(CFStringRef inBundleID)
|
||||
{
|
||||
mMusicPlayerBundleID = inBundleID;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
@interface BGMMockAudioDeviceManager : BGMAudioDeviceManager
|
||||
@end
|
||||
|
||||
@implementation BGMMockAudioDeviceManager {
|
||||
BGMBackgroundMusicDevice bgmDevice;
|
||||
}
|
||||
|
||||
- (BGMBackgroundMusicDevice) bgmDevice {
|
||||
return bgmDevice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
@interface BGMMusicPlayersUnitTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation BGMMusicPlayersUnitTests {
|
||||
BGMMockAudioDeviceManager* devices;
|
||||
BGMAudioDeviceManager* devices;
|
||||
BGMMockUserDefaults* defaults;
|
||||
|
||||
NSUUID* spotifyID;
|
||||
@@ -98,7 +126,7 @@
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
|
||||
|
||||
devices = [BGMMockAudioDeviceManager new];
|
||||
defaults = [BGMMockUserDefaults new];
|
||||
|
||||
@@ -108,8 +136,6 @@
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
[self resetDevice];
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
@@ -187,8 +213,7 @@
|
||||
// When it doesn't find a selected music player in user defaults, it should check BGMDevice's music
|
||||
// player properties.
|
||||
|
||||
[devices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress,
|
||||
CFSTR("org.videolan.vlc"));
|
||||
[devices bgmDevice].SetMusicPlayerBundleID(CFSTR("org.videolan.vlc"));
|
||||
|
||||
BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices
|
||||
userDefaults:defaults];
|
||||
@@ -203,7 +228,7 @@
|
||||
|
||||
- (void) resetDevice {
|
||||
// Reset the mock BGMDevice.
|
||||
[devices bgmDevice].SetPropertyData_CFString(kBGMMusicPlayerBundleIDAddress, NULL);
|
||||
[devices bgmDevice].SetMusicPlayerBundleID(CFSTR(""));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
|
||||
#pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
|
||||
// The value of the music player bundle ID property. Tests should set this back to NULL when they finish. (Has
|
||||
// The value of the music player bundle ID property. Tests should set this back to "" when they finish. (Has
|
||||
// to be static because we can't add to the real class's interface.)
|
||||
static CFStringRef __nullable playerBundleID = NULL;
|
||||
static CFStringRef playerBundleID = CFSTR("");
|
||||
|
||||
CAHALAudioObject::CAHALAudioObject(AudioObjectID inObjectID)
|
||||
:
|
||||
@@ -46,6 +46,11 @@ CAHALAudioObject::~CAHALAudioObject()
|
||||
{
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioObject::GetObjectID() const
|
||||
{
|
||||
return mObjectID;
|
||||
}
|
||||
|
||||
void CAHALAudioObject::GetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32& ioDataSize, void* outData) const
|
||||
{
|
||||
if(inAddress.mSelector == kAudioDeviceCustomPropertyMusicPlayerBundleID)
|
||||
@@ -64,11 +69,6 @@ void CAHALAudioObject::SetPropertyData(const AudioObjectPropertyAddress& inAddre
|
||||
|
||||
#pragma mark Unimplemented methods
|
||||
|
||||
AudioObjectID CAHALAudioObject::GetObjectID() const
|
||||
{
|
||||
return kAudioObjectUnknown;
|
||||
}
|
||||
|
||||
void CAHALAudioObject::SetObjectID(AudioObjectID inObjectID)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// Mock_CAHALAudioSystemObject.cpp
|
||||
// BGMAppUnitTests
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
|
||||
|
||||
CAHALAudioSystemObject::CAHALAudioSystemObject()
|
||||
:
|
||||
CAHALAudioObject(kAudioObjectSystemObject)
|
||||
{
|
||||
}
|
||||
|
||||
CAHALAudioSystemObject::~CAHALAudioSystemObject()
|
||||
{
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioSystemObject::GetAudioDeviceForUID(CFStringRef inUID) const
|
||||
{
|
||||
AudioObjectID id = kAudioObjectUnknown;
|
||||
|
||||
// Generate a deterministic and random-ish ID from the UID string. Ideally we would ensure the
|
||||
// IDs are unique, but this is probably fine.
|
||||
for(int i = 0; i < CFStringGetLength(inUID); i++)
|
||||
{
|
||||
id += 37 * CFStringGetCharacterAtIndex(inUID, i);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
#pragma mark Unimplemented methods
|
||||
|
||||
#pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
|
||||
UInt32 CAHALAudioSystemObject::GetNumberAudioDevices() const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioSystemObject::GetAudioDevices(UInt32& ioNumberAudioDevices, AudioObjectID* outAudioDevices) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioSystemObject::GetAudioDeviceAtIndex(UInt32 inIndex) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioSystemObject::LogBasicDeviceInfo()
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioSystemObject::GetDefaultAudioDevice(bool inIsInput, bool inIsSystem) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
void CAHALAudioSystemObject::SetDefaultAudioDevice(bool inIsInput, bool inIsSystem, AudioObjectID inNewDefaultDevice)
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
AudioObjectID CAHALAudioSystemObject::GetAudioPlugInForBundleID(CFStringRef inUID) const
|
||||
{
|
||||
Throw(new CAException(kAudio_UnimplementedError));
|
||||
}
|
||||
|
||||
+69
-15
@@ -14,36 +14,40 @@
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGMXPCHelperService.m
|
||||
// BGMXPCHelperService.mm
|
||||
// BGMXPCHelper
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMXPCHelperService.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMXPCListenerDelegate.h"
|
||||
#import "BGMBackgroundMusicDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static const int DELAY_BEFORE_CLEANING_UP_FOR_BGMAPP_SECS = 1;
|
||||
|
||||
static NSXPCListenerEndpoint* __nullable sBGMAppEndpoint = nil;
|
||||
static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
|
||||
@implementation BGMXPCHelperService {
|
||||
NSXPCConnection* connection;
|
||||
AudioObjectID outputDeviceToMakeDefaultOnAbnormalTermination;
|
||||
}
|
||||
|
||||
- (id) initWithConnection:_connection {
|
||||
if ((self = [super init])) {
|
||||
connection = _connection;
|
||||
outputDeviceToMakeDefaultOnAbnormalTermination = kAudioObjectUnknown;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -113,6 +117,45 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Called after the connection from BGMApp is invalidated. (If it's only been interrupted, launchd
|
||||
// might restore it, so we wait for it to be invalidated.)
|
||||
- (void) cleanUpForBGMApp {
|
||||
// Wait a bit to see if BGMApp reconnects. Then, if it doesn't, check to see if BGMDevice has
|
||||
// been left as the default output device. That would probably mean BGMApp crashed or was force
|
||||
// quit or something like that, so we try to restore the user's output device from BGMXPCHelper.
|
||||
int64_t delay = DELAY_BEFORE_CLEANING_UP_FOR_BGMAPP_SECS * NSEC_PER_SEC;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
|
||||
dispatch_get_main_queue(),
|
||||
^{
|
||||
[self unsetBGMDeviceAsDefault];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) unsetBGMDeviceAsDefault {
|
||||
// Check that BGMApp hasn't reconnected.
|
||||
if (sBGMAppConnection) {
|
||||
DebugMsg("BGMXPCHelperService::unsetBGMDeviceAsDefault: BGMApp connected. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
AudioObjectID outputDevice = outputDeviceToMakeDefaultOnAbnormalTermination;
|
||||
|
||||
if (outputDevice == kAudioObjectUnknown) {
|
||||
// We could set the default device arbitrarily, but it's probably not worth the effort.
|
||||
DebugMsg("BGMXPCHelperService::unsetBGMDeviceAsDefault: No device to set. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If BGMDevice has been left as the default device, change it to the real output device.
|
||||
BGMLogAndSwallowExceptions("BGMXPCHelperService::unsetBGMDeviceAsDefault", ([&] {
|
||||
NSLog(@"BGMXPCHelperService::unsetBGMDeviceAsDefault: Changing default device to %u",
|
||||
outputDevice);
|
||||
|
||||
BGMBackgroundMusicDevice().UnsetAsOSDefault(outputDevice);
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma mark Exported Methods
|
||||
|
||||
- (void) registerAsBGMAppWithListenerEndpoint:(NSXPCListenerEndpoint*)endpoint reply:(void (^)(void))reply {
|
||||
@@ -128,13 +171,18 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
[sBGMAppConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]];
|
||||
|
||||
// Set the stored connection back to nil when BGMApp closes, dies or invalidates the connection.
|
||||
void(^cleanUp)(void) = ^{
|
||||
sBGMAppConnection.interruptionHandler = ^{
|
||||
@synchronized([self class]) {
|
||||
sBGMAppConnection = nil;
|
||||
}
|
||||
};
|
||||
sBGMAppConnection.interruptionHandler = cleanUp;
|
||||
sBGMAppConnection.invalidationHandler = cleanUp;
|
||||
|
||||
sBGMAppConnection.invalidationHandler = ^{
|
||||
@synchronized([self class]) {
|
||||
sBGMAppConnection = nil;
|
||||
[self cleanUpForBGMApp];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reply();
|
||||
@@ -155,25 +203,25 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) waitForBGMAppToStartOutputDeviceWithReply:(void (^)(NSError*))reply {
|
||||
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI {
|
||||
[self debugWarnIfCalledByBGMApp];
|
||||
|
||||
// If this reply string isn't set before the end of this method, it's a bug
|
||||
__block NSError* replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_InternalError
|
||||
description:@"Reply not set in waitForBGMAppToStartOutputDeviceWithReply"];
|
||||
description:@"Reply not set in startBGMAppPlayThroughSyncWithReply"];
|
||||
|
||||
// I couldn't find the Obj-C equivalent of xpc_connection_send_message_with_reply_sync so just wait on this
|
||||
// semaphore until we get a reply from BGMApp (or timeout)
|
||||
// semaphore until we get a reply from BGMApp (or timeout). Note that ARC handles dispatch semaphores.
|
||||
dispatch_semaphore_t bgmAppReplySemaphore = dispatch_semaphore_create(0);
|
||||
|
||||
DebugMsg("BGMXPCHelperService::waitForBGMAppToStartOutputDeviceWithReply: Waiting for BGMApp to start IO on the output device");
|
||||
DebugMsg("BGMXPCHelperService::startBGMAppPlayThroughSyncWithReply: Waiting for BGMApp to start IO on the output device");
|
||||
|
||||
// Send the message to BGMApp
|
||||
[BGMXPCHelperService withBGMAppRemoteProxy:^(id remoteObjectProxy) {
|
||||
[remoteObjectProxy waitForOutputDeviceToStartWithReply:^(NSError* bgmAppReply) {
|
||||
[remoteObjectProxy startPlayThroughSyncWithReply:^(NSError* bgmAppReply) {
|
||||
replyToBGMDriver = bgmAppReply;
|
||||
dispatch_semaphore_signal(bgmAppReplySemaphore);
|
||||
}];
|
||||
} forUISoundsDevice:isUI];
|
||||
} errorHandler:^(NSError* error) {
|
||||
replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_MessageFailure
|
||||
description:[error localizedDescription]
|
||||
@@ -190,11 +238,17 @@ static NSXPCConnection* __nullable sBGMAppConnection = nil;
|
||||
}
|
||||
|
||||
// Return the reply to BGMDriver
|
||||
DebugMsg("BGMXPCHelperService::waitForBGMAppToStartOutputDeviceWithReply: Reply to BGMDriver: %s",
|
||||
DebugMsg("BGMXPCHelperService::startBGMAppPlayThroughSyncWithReply: Reply to BGMDriver: %s",
|
||||
[[replyToBGMDriver localizedDescription] UTF8String]);
|
||||
reply(replyToBGMDriver);
|
||||
}
|
||||
|
||||
- (void) setOutputDeviceToMakeDefaultOnAbnormalTermination:(AudioObjectID)deviceID {
|
||||
outputDeviceToMakeDefaultOnAbnormalTermination = deviceID;
|
||||
DebugMsg("BGMXPCHelperService::setOutputDeviceToMakeDefaultOnAbnormalTermination: ID set to %u",
|
||||
deviceID);
|
||||
}
|
||||
|
||||
#pragma mark Debug Utils
|
||||
|
||||
- (void) debugWarnIfCalledByBGMApp {
|
||||
@@ -27,8 +27,6 @@
|
||||
#import "BGMXPCHelperService.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
// System Includes
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.1</string>
|
||||
<string>0.2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -48,8 +48,6 @@
|
||||
#import "BGMXPCListenerDelegate.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#undef CoreAudio_ThreadStampMessages
|
||||
#define CoreAudio_ThreadStampMessages 0 // Requires C++
|
||||
#include "CADebugMacros.h"
|
||||
|
||||
// System Includes
|
||||
|
||||
@@ -57,19 +57,20 @@
|
||||
|
||||
- (void) testStartOutputDeviceWithoutBGMAppConnected {
|
||||
dispatch_semaphore_t replySemaphore = dispatch_semaphore_create(0);
|
||||
|
||||
|
||||
// Unregister BGMXPCHelper's connection to BGMApp in case BGMApp didn't shutdown cleanly the last time it ran.
|
||||
[[connection remoteObjectProxy] unregisterAsBGMApp];
|
||||
|
||||
[[connection remoteObjectProxy] waitForBGMAppToStartOutputDeviceWithReply:^(NSError* reply) {
|
||||
[[connection remoteObjectProxy] startBGMAppPlayThroughSyncWithReply:^(NSError* reply) {
|
||||
XCTAssertEqual([reply code],
|
||||
kBGMXPC_MessageFailure,
|
||||
@"Check that BGMApp isn't running, which would cause this failure");
|
||||
|
||||
dispatch_semaphore_signal(replySemaphore);
|
||||
}];
|
||||
|
||||
if (0 != dispatch_semaphore_wait(replySemaphore, dispatch_time(DISPATCH_TIME_NOW, kStartIOTimeoutNsec))) {
|
||||
} forUISoundsDevice:NO];
|
||||
|
||||
// Very long timeout to make it less likely to fail on Travis CI when there's high contention.
|
||||
if (0 != dispatch_semaphore_wait(replySemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * 60 * NSEC_PER_SEC))) {
|
||||
XCTFail(@"Timed out waiting for BGMXPCHelper");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,18 @@
|
||||
// CADebugger
|
||||
//=============================================================================
|
||||
|
||||
// BGM edit: Added extern "C" so CADebugger (and headers that include it) can be used in Obj-C.
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if TARGET_API_MAC_OSX
|
||||
extern bool CAIsDebuggerAttached(void);
|
||||
#endif
|
||||
extern void CADebuggerStop(void);
|
||||
extern void CADebuggerStop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,12 +7,22 @@
|
||||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
1C30A69F1C1E98F000C05AA5 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3841BBCEFE8000E2DD1 /* CAMutex.cpp */; };
|
||||
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210C1C4A163A00A0C8C6 /* 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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -71,6 +81,10 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
19FE7431C588F36F4F1E70BB /* BGM_MuteControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_MuteControl.h; sourceTree = "<group>"; };
|
||||
19FE7B8CE9148B3D8D7517C6 /* BGM_Control.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_Control.h; sourceTree = "<group>"; };
|
||||
19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_Control.cpp; sourceTree = "<group>"; };
|
||||
19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_MuteControl.cpp; sourceTree = "<group>"; };
|
||||
1C0CB6A61C4E06C00084C15A /* CAAtomicStack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAtomicStack.h; path = PublicUtility/CAAtomicStack.h; sourceTree = "<group>"; };
|
||||
1C0CB6A71C4E06F70084C15A /* CAAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAtomic.h; path = PublicUtility/CAAtomic.h; sourceTree = "<group>"; };
|
||||
1C0CB6A91C50A3AF0084C15A /* CAAutoDisposer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAutoDisposer.h; path = PublicUtility/CAAutoDisposer.h; sourceTree = "<group>"; };
|
||||
@@ -89,6 +103,11 @@
|
||||
1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
|
||||
1C3821101C4A18DE00A0C8C6 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
|
||||
1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_DeviceTests.mm; sourceTree = "<group>"; };
|
||||
1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_AudibleState.cpp; sourceTree = "<group>"; };
|
||||
1C7010741F05ED5100D8CCDC /* BGM_AudibleState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_AudibleState.h; sourceTree = "<group>"; };
|
||||
1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_VolumeControl.cpp; sourceTree = "<group>"; };
|
||||
1C7010781F07A0BA00D8CCDC /* BGM_VolumeControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_VolumeControl.h; sourceTree = "<group>"; };
|
||||
1C780FEE1FEE78E800497FAD /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
|
||||
1C8034DA1BDD073B00668E00 /* BGMDriverTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMDriverTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_ClientsTests.mm; sourceTree = "<group>"; };
|
||||
1C8034DE1BDD073B00668E00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -151,6 +170,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C780FF41FF275F300497FAD /* Accelerate.framework in Frameworks */,
|
||||
1CBB322C1BDD3A3000C9BD55 /* CoreAudio.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -159,6 +179,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C780FEF1FEE78E800497FAD /* Accelerate.framework in Frameworks */,
|
||||
2743C9E61D7EF8E00089613B /* libPublicUtility.a in Frameworks */,
|
||||
2795973E1C9847CF00A002FB /* Foundation.framework in Frameworks */,
|
||||
1CB8B3761BBBD924000E2DD1 /* CoreAudio.framework in Frameworks */,
|
||||
@@ -235,10 +256,18 @@
|
||||
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */,
|
||||
1CB8B37F1BBCCF87000E2DD1 /* BGM_Device.h */,
|
||||
1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */,
|
||||
1C7010741F05ED5100D8CCDC /* BGM_AudibleState.h */,
|
||||
1C7010731F05ED5100D8CCDC /* BGM_AudibleState.cpp */,
|
||||
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */,
|
||||
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */,
|
||||
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */,
|
||||
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */,
|
||||
19FE7B8CE9148B3D8D7517C6 /* BGM_Control.h */,
|
||||
19FE7BC3396C4E50D21E1BC8 /* BGM_Control.cpp */,
|
||||
1C7010781F07A0BA00D8CCDC /* BGM_VolumeControl.h */,
|
||||
1C7010771F07A0BA00D8CCDC /* BGM_VolumeControl.cpp */,
|
||||
19FE7431C588F36F4F1E70BB /* BGM_MuteControl.h */,
|
||||
19FE7E6DC2A1B61211D74782 /* BGM_MuteControl.cpp */,
|
||||
1C0CB6AF1C642C600084C15A /* DeviceClients */,
|
||||
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */,
|
||||
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */,
|
||||
@@ -300,6 +329,7 @@
|
||||
2743CA241D86E2E80089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C780FEE1FEE78E800497FAD /* Accelerate.framework */,
|
||||
1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */,
|
||||
1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */,
|
||||
2795973D1C9847CF00A002FB /* Foundation.framework */,
|
||||
@@ -390,7 +420,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
CLASSPREFIX = BGM_;
|
||||
LastUpgradeCheck = 0700;
|
||||
LastUpgradeCheck = 0900;
|
||||
ORGANIZATIONNAME = "Background Music contributors";
|
||||
TargetAttributes = {
|
||||
1C8034D91BDD073B00668E00 = {
|
||||
@@ -453,6 +483,7 @@
|
||||
27E6B5F01E01966A00EC0AAB /* BGM_Utils.cpp in Sources */,
|
||||
277170101CA0CFC300AB34B4 /* BGM_PlugInInterface.cpp in Sources */,
|
||||
277170111CA0CFC300AB34B4 /* CACFNumber.cpp in Sources */,
|
||||
1C7010761F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */,
|
||||
27D643C31C9FBE1600737F6E /* BGM_XPCHelper.m in Sources */,
|
||||
27379B821C76D62D0084A24C /* CADebugMacros.cpp in Sources */,
|
||||
27379B831C76D62D0084A24C /* CADebugPrintf.cpp in Sources */,
|
||||
@@ -465,6 +496,7 @@
|
||||
277EE65E1C728C9D0037F1EE /* BGM_PlugIn.cpp in Sources */,
|
||||
277EE65F1C728C9D0037F1EE /* CAPThread.cpp in Sources */,
|
||||
277EE65A1C728C630037F1EE /* BGM_Client.cpp in Sources */,
|
||||
1C70107A1F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */,
|
||||
277EE65B1C728C630037F1EE /* BGM_ClientMap.cpp in Sources */,
|
||||
277EE65C1C728C630037F1EE /* BGM_Clients.cpp in Sources */,
|
||||
277EE65D1C728C630037F1EE /* BGM_TaskQueue.cpp in Sources */,
|
||||
@@ -476,6 +508,8 @@
|
||||
1CC1DF8D1BE5705700FB8FE4 /* CACFDictionary.cpp in Sources */,
|
||||
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */,
|
||||
1C8034DD1BDD073B00668E00 /* BGM_ClientsTests.mm in Sources */,
|
||||
19FE761291BF07AEA278F25C /* BGM_MuteControl.cpp in Sources */,
|
||||
19FE742AEBE30B21C4CF9285 /* BGM_Control.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -484,6 +518,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */,
|
||||
1C7010751F05ED5100D8CCDC /* BGM_AudibleState.cpp in Sources */,
|
||||
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */,
|
||||
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */,
|
||||
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */,
|
||||
@@ -491,12 +526,15 @@
|
||||
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */,
|
||||
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */,
|
||||
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */,
|
||||
1C7010791F07A0BA00D8CCDC /* BGM_VolumeControl.cpp in Sources */,
|
||||
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */,
|
||||
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */,
|
||||
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */,
|
||||
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */,
|
||||
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */,
|
||||
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */,
|
||||
19FE766482B57D852CCF6F0A /* BGM_MuteControl.cpp in Sources */,
|
||||
19FE77D40F15EA060B462D83 /* BGM_Control.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -579,6 +617,8 @@
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;
|
||||
@@ -586,6 +626,8 @@
|
||||
CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
@@ -641,6 +683,8 @@
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_ASSIGN_ENUM = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES;
|
||||
@@ -648,6 +692,8 @@
|
||||
CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
@@ -760,6 +806,7 @@
|
||||
2743C9C81D7EF84B0089613B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
@@ -788,6 +835,7 @@
|
||||
2743C9C91D7EF84B0089613B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -56,6 +58,7 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
debugAsWhichUser = "root"
|
||||
language = ""
|
||||
launchStyle = "1"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -78,7 +81,7 @@
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "ASAN_OPTIONS"
|
||||
value = "detect_odr_violation=0"
|
||||
value = "detect_odr_violation=1:use_odr_indicator=1:detect_stack_use_after_return=1:detect_invalid_pointer_pairs=2:check_initialization_order=1:log_to_syslog=1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0820"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -36,6 +38,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -51,6 +54,13 @@
|
||||
ReferencedContainer = "container:BGMDriver.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "ASAN_OPTIONS"
|
||||
value = "detect_odr_violation=1:use_odr_indicator=1:detect_stack_use_after_return=1:detect_invalid_pointer_pairs=2:check_initialization_order=1:log_to_syslog=1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
|
||||
@@ -241,8 +241,8 @@ void BGM_AbstractDevice::GetPropertyData(AudioObjectID inObjectID,
|
||||
case kAudioDevicePropertyZeroTimeStampPeriod:
|
||||
case kAudioDevicePropertyNominalSampleRate:
|
||||
case kAudioDevicePropertyAvailableNominalSampleRates:
|
||||
// Crash debug builds if a concrete device delegates a required property that can't be
|
||||
// handled here or in BGM_Object (the parent of this class).
|
||||
// Should be unreachable. Reaching this point would mean a concrete device has delegated
|
||||
// a required property that can't be handled by this class or its parent, BGM_Object.
|
||||
//
|
||||
// See BGM_Device for info about these properties.
|
||||
//
|
||||
@@ -250,7 +250,8 @@ void BGM_AbstractDevice::GetPropertyData(AudioObjectID inObjectID,
|
||||
BGMAssert(false,
|
||||
"BGM_AbstractDevice::GetPropertyData: Property %u not handled in subclass",
|
||||
inAddress.mSelector);
|
||||
|
||||
// Throw in release builds.
|
||||
Throw(CAException(kAudioHardwareIllegalOperationError));
|
||||
|
||||
case kAudioDevicePropertyTransportType:
|
||||
// This value represents how the device is attached to the system. This can be
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_AudibleState.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016 Josh Junon
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_AudibleState.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wsign-conversion"
|
||||
#include "CAAtomic.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// STL Includes
|
||||
#include <algorithm> // For std::min and std::max.
|
||||
|
||||
|
||||
// TODO: This is just the first value I tried.
|
||||
static const Float32 kSampleVolumeMarginRaw = 0.0001f;
|
||||
|
||||
BGM_AudibleState::BGM_AudibleState()
|
||||
:
|
||||
mState(kBGMDeviceIsSilent),
|
||||
mSampleTimes({0, 0, 0, 0})
|
||||
{
|
||||
}
|
||||
|
||||
BGMDeviceAudibleState BGM_AudibleState::GetState() const noexcept
|
||||
{
|
||||
CAMemoryBarrier(); // Probably unnecessary.
|
||||
return mState;
|
||||
}
|
||||
|
||||
void BGM_AudibleState::Reset() noexcept
|
||||
{
|
||||
mState = kBGMDeviceIsSilent;
|
||||
|
||||
mSampleTimes.latestSilent = 0;
|
||||
mSampleTimes.latestAudibleNonMusic = 0;
|
||||
mSampleTimes.latestSilentMusic = 0;
|
||||
mSampleTimes.latestAudibleMusic = 0;
|
||||
}
|
||||
|
||||
void BGM_AudibleState::UpdateWithClientIO(bool inClientIsMusicPlayer,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
Float64 inOutputSampleTime,
|
||||
const Float32* inBuffer)
|
||||
{
|
||||
// Update the sample times of the most recent audible music, silent music and audible non-music
|
||||
// samples we've received.
|
||||
|
||||
Float64 endFrameSampleTime = inOutputSampleTime + inIOBufferFrameSize - 1;
|
||||
|
||||
if(inClientIsMusicPlayer)
|
||||
{
|
||||
if(BufferIsAudible(inIOBufferFrameSize, inBuffer))
|
||||
{
|
||||
mSampleTimes.latestAudibleMusic = std::max(mSampleTimes.latestAudibleMusic,
|
||||
endFrameSampleTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
mSampleTimes.latestSilentMusic = std::max(mSampleTimes.latestSilentMusic,
|
||||
endFrameSampleTime);
|
||||
}
|
||||
}
|
||||
else if(endFrameSampleTime > mSampleTimes.latestAudibleNonMusic && // Don't bother checking the
|
||||
// buffer if it won't change
|
||||
// anything.
|
||||
BufferIsAudible(inIOBufferFrameSize, inBuffer))
|
||||
{
|
||||
mSampleTimes.latestAudibleNonMusic = std::max(mSampleTimes.latestAudibleNonMusic,
|
||||
endFrameSampleTime);
|
||||
}
|
||||
}
|
||||
|
||||
bool BGM_AudibleState::UpdateWithMixedIO(UInt32 inIOBufferFrameSize,
|
||||
Float64 inOutputSampleTime,
|
||||
const Float32* inBuffer)
|
||||
{
|
||||
// Update the sample time of the most recent silent sample we've received. (The music player
|
||||
// client is not considered separate for the latest silent sample.)
|
||||
|
||||
bool audible = BufferIsAudible(inIOBufferFrameSize, inBuffer);
|
||||
|
||||
// The sample time of the last frame we're looking at.
|
||||
Float64 endFrameSampleTime = inOutputSampleTime + inIOBufferFrameSize - 1;
|
||||
|
||||
if(!audible)
|
||||
{
|
||||
mSampleTimes.latestSilent = std::max(mSampleTimes.latestSilent, endFrameSampleTime);
|
||||
}
|
||||
|
||||
return RecalculateState(endFrameSampleTime);
|
||||
}
|
||||
|
||||
bool BGM_AudibleState::RecalculateState(Float64 inEndFrameSampleTime)
|
||||
{
|
||||
Float64 sinceLatestSilent = inEndFrameSampleTime - mSampleTimes.latestSilent;
|
||||
Float64 sinceLatestMusicSilent = inEndFrameSampleTime - mSampleTimes.latestSilentMusic;
|
||||
Float64 sinceLatestAudible = inEndFrameSampleTime - mSampleTimes.latestAudibleNonMusic;
|
||||
Float64 sinceLatestMusicAudible = inEndFrameSampleTime - mSampleTimes.latestAudibleMusic;
|
||||
|
||||
bool didChangeState = false;
|
||||
|
||||
// Update mState
|
||||
|
||||
// Change from silent/silentExceptMusic to audible
|
||||
if(mState != kBGMDeviceIsAudible &&
|
||||
sinceLatestSilent >= kDeviceAudibleStateMinChangedFramesForUpdate &&
|
||||
// Check that non-music audio is currently playing
|
||||
sinceLatestAudible <= 0 && mSampleTimes.latestAudibleNonMusic != 0)
|
||||
{
|
||||
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
|
||||
"kAudioDeviceCustomPropertyDeviceAudibleState to audible");
|
||||
mState = kBGMDeviceIsAudible;
|
||||
CAMemoryBarrier();
|
||||
didChangeState = true;
|
||||
}
|
||||
// Change from silent to silentExceptMusic
|
||||
else if(((mState == kBGMDeviceIsSilent &&
|
||||
sinceLatestMusicSilent >= kDeviceAudibleStateMinChangedFramesForUpdate) ||
|
||||
// ...or from audible to silentExceptMusic
|
||||
(mState == kBGMDeviceIsAudible &&
|
||||
sinceLatestAudible >= kDeviceAudibleStateMinChangedFramesForUpdate &&
|
||||
sinceLatestMusicSilent >= kDeviceAudibleStateMinChangedFramesForUpdate)) &&
|
||||
// In case we haven't seen any music samples yet (either audible or silent), check that
|
||||
// music is currently playing
|
||||
sinceLatestMusicAudible <= 0 && mSampleTimes.latestAudibleMusic != 0)
|
||||
{
|
||||
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
|
||||
"kAudioDeviceCustomPropertyDeviceAudibleState to silent except music");
|
||||
mState = kBGMDeviceIsSilentExceptMusic;
|
||||
CAMemoryBarrier();
|
||||
didChangeState = true;
|
||||
}
|
||||
// Change from audible/silentExceptMusic to silent
|
||||
else if(mState != kBGMDeviceIsSilent &&
|
||||
sinceLatestAudible >= kDeviceAudibleStateMinChangedFramesForUpdate &&
|
||||
sinceLatestMusicAudible >= kDeviceAudibleStateMinChangedFramesForUpdate)
|
||||
{
|
||||
DebugMsg("BGM_AudibleState::RecalculateState: Changing "
|
||||
"kAudioDeviceCustomPropertyDeviceAudibleState to silent");
|
||||
mState = kBGMDeviceIsSilent;
|
||||
CAMemoryBarrier();
|
||||
didChangeState = true;
|
||||
}
|
||||
|
||||
return didChangeState;
|
||||
}
|
||||
|
||||
// static
|
||||
bool BGM_AudibleState::BufferIsAudible(UInt32 inIOBufferFrameSize, const Float32* inBuffer)
|
||||
{
|
||||
// Check each frame to see if any are audible. This could be much more accurate, but seems to
|
||||
// work well enough for now.
|
||||
//
|
||||
// The trade off here is between pausing the music player at the wrong time and unpausing it at
|
||||
// the wrong time. If a short sound (e.g. a UI alert) plays but has a long, barely-audible tail,
|
||||
// we might not detect the silence quickly enough and pause the music player. Similarly, if
|
||||
// we've paused the music player and there's a period of near-silence in the new audio, we might
|
||||
// unpause the music and briefly interrupt the new audio.
|
||||
//
|
||||
// A fairly long period of silence before unpausing the music player isn't a big problem, which
|
||||
// means BGMApp can wait much longer before unpausing than before pausing. So this function errs
|
||||
// toward considering the buffer silent, which helps BGMApp ignore short sounds.
|
||||
if(inIOBufferFrameSize > 0)
|
||||
{
|
||||
// Bounds for the left channel samples.
|
||||
Float32 firstSampleLLower = inBuffer[0] - kSampleVolumeMarginRaw;
|
||||
Float32 firstSampleLUpper = inBuffer[0] + kSampleVolumeMarginRaw;
|
||||
// Bounds for the right channel samples.
|
||||
Float32 firstSampleRLower = inBuffer[1] - kSampleVolumeMarginRaw;
|
||||
Float32 firstSampleRUpper = inBuffer[1] + kSampleVolumeMarginRaw;
|
||||
|
||||
for(UInt32 i = 0; i < inIOBufferFrameSize * 2; i += 2)
|
||||
{
|
||||
bool audibleL =
|
||||
(inBuffer[i] < firstSampleLLower) || (inBuffer[i] > firstSampleLUpper);
|
||||
bool audibleR =
|
||||
(inBuffer[i + 1] < firstSampleRLower) || (inBuffer[i + 1] > firstSampleRUpper);
|
||||
|
||||
if(audibleL || audibleR)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_AudibleState.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Inspects a stream of audio data and reports whether it's silent, silent except for the user's
|
||||
// music player, or audible.
|
||||
//
|
||||
// See kAudioDeviceCustomPropertyDeviceAudibleState and the BGMDeviceAudibleState enum in
|
||||
// BGM_Types.h for more info.
|
||||
//
|
||||
// Not thread-safe.
|
||||
//
|
||||
|
||||
#ifndef BGMDriver__BGM_AudibleState
|
||||
#define BGMDriver__BGM_AudibleState
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_Types.h"
|
||||
|
||||
// System Includes
|
||||
#include <MacTypes.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_AudibleState
|
||||
{
|
||||
|
||||
public:
|
||||
BGM_AudibleState();
|
||||
|
||||
/*!
|
||||
@return The current audible state of the device, to be used as the value of the
|
||||
kAudioDeviceCustomPropertyDeviceAudibleState property.
|
||||
*/
|
||||
BGMDeviceAudibleState GetState() const noexcept;
|
||||
|
||||
/*! Set the audible state back to kBGMDeviceIsSilent and ignore all previous IO. */
|
||||
void Reset() noexcept;
|
||||
|
||||
/*!
|
||||
Read an audio buffer sent by a single device client (i.e. a process playing audio) and update
|
||||
the audible state. The update will only affect the return value of GetState after the next
|
||||
call to UpdateWithMixedIO, when all IO for the cycle has been read.
|
||||
|
||||
Real-time safe. Not thread safe.
|
||||
*/
|
||||
void UpdateWithClientIO(bool inClientIsMusicPlayer,
|
||||
UInt32 inIOBufferFrameSize,
|
||||
Float64 inOutputSampleTime,
|
||||
const Float32* inBuffer);
|
||||
/*!
|
||||
Read a fully mixed audio buffer and update the audible state. All client (unmixed) buffers for
|
||||
the same cycle must be read with UpdateWithClientIO before calling this function.
|
||||
|
||||
Real-time safe. Not thread safe.
|
||||
|
||||
@return True if the audible state changed.
|
||||
*/
|
||||
bool UpdateWithMixedIO(UInt32 inIOBufferFrameSize,
|
||||
Float64 inOutputSampleTime,
|
||||
const Float32* inBuffer);
|
||||
|
||||
private:
|
||||
bool RecalculateState(Float64 inEndFrameSampleTime);
|
||||
|
||||
static bool BufferIsAudible(UInt32 inIOBufferFrameSize,
|
||||
const Float32* inBuffer);
|
||||
|
||||
private:
|
||||
BGMDeviceAudibleState mState;
|
||||
|
||||
struct
|
||||
{
|
||||
Float64 latestAudibleNonMusic;
|
||||
Float64 latestSilent;
|
||||
Float64 latestAudibleMusic;
|
||||
Float64 latestSilentMusic;
|
||||
} mSampleTimes;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMDriver__BGM_AudibleState */
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_Control.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_Control.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAException.h"
|
||||
|
||||
// System Includes
|
||||
#include <CoreAudio/AudioHardwareBase.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
BGM_Control::BGM_Control(AudioObjectID inObjectID,
|
||||
AudioClassID inClassID,
|
||||
AudioClassID inBaseClassID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope,
|
||||
AudioObjectPropertyElement inElement)
|
||||
:
|
||||
BGM_Object(inObjectID, inClassID, inBaseClassID, inOwnerObjectID),
|
||||
mScope(inScope),
|
||||
mElement(inElement)
|
||||
{
|
||||
}
|
||||
|
||||
bool BGM_Control::HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioControlPropertyScope:
|
||||
case kAudioControlPropertyElement:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::HasProperty(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
bool BGM_Control::IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioControlPropertyScope:
|
||||
case kAudioControlPropertyElement:
|
||||
theAnswer = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
UInt32 BGM_Control::GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
UInt32 theAnswer = 0;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioControlPropertyScope:
|
||||
theAnswer = sizeof(AudioObjectPropertyScope);
|
||||
break;
|
||||
|
||||
case kAudioControlPropertyElement:
|
||||
theAnswer = sizeof(AudioObjectPropertyElement);
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
void BGM_Control::GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioControlPropertyScope:
|
||||
// This property returns the scope that the control is attached to.
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectPropertyScope),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_Control::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioControlPropertyScope for the control");
|
||||
*reinterpret_cast<AudioObjectPropertyScope*>(outData) = mScope;
|
||||
outDataSize = sizeof(AudioObjectPropertyScope);
|
||||
break;
|
||||
|
||||
case kAudioControlPropertyElement:
|
||||
// This property returns the element that the control is attached to.
|
||||
ThrowIf(inDataSize < sizeof(AudioObjectPropertyElement),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_Control::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioControlPropertyElement for the control");
|
||||
*reinterpret_cast<AudioObjectPropertyElement*>(outData) = mElement;
|
||||
outDataSize = sizeof(AudioObjectPropertyElement);
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Object::GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void BGM_Control::CheckObjectID(AudioObjectID inObjectID) const
|
||||
{
|
||||
ThrowIf(inObjectID == kAudioObjectUnknown || inObjectID != GetObjectID(),
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_Control::CheckObjectID: wrong audio object ID for the control");
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_Control.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
// An AudioObject that represents a user-controllable aspect of a device or stream, such as volume
|
||||
// or balance.
|
||||
//
|
||||
|
||||
#ifndef BGMDriver__BGM_Control
|
||||
#define BGMDriver__BGM_Control
|
||||
|
||||
// Superclass Includes
|
||||
#include "BGM_Object.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_Control
|
||||
:
|
||||
public BGM_Object
|
||||
{
|
||||
|
||||
protected:
|
||||
BGM_Control(AudioObjectID inObjectID,
|
||||
AudioClassID inClassID,
|
||||
AudioClassID inBaseClassID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope =
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
AudioObjectPropertyElement inElement =
|
||||
kAudioObjectPropertyElementMaster);
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
public:
|
||||
virtual bool HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual bool IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const;
|
||||
virtual void GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const;
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
protected:
|
||||
void CheckObjectID(AudioObjectID inObjectID) const;
|
||||
|
||||
protected:
|
||||
const AudioObjectPropertyScope mScope;
|
||||
const AudioObjectPropertyElement mElement;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMDriver__BGM_Control */
|
||||
|
||||
+458
-768
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,10 @@
|
||||
#include "BGM_WrappedAudioEngine.h"
|
||||
#include "BGM_Clients.h"
|
||||
#include "BGM_TaskQueue.h"
|
||||
#include "BGM_AudibleState.h"
|
||||
#include "BGM_Stream.h"
|
||||
#include "BGM_VolumeControl.h"
|
||||
#include "BGM_MuteControl.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAMutex.h"
|
||||
@@ -55,12 +58,20 @@ class BGM_Device
|
||||
|
||||
public:
|
||||
static BGM_Device& GetInstance();
|
||||
static BGM_Device& GetUISoundsInstance();
|
||||
|
||||
private:
|
||||
static void StaticInitializer();
|
||||
|
||||
protected:
|
||||
BGM_Device();
|
||||
BGM_Device(AudioObjectID inObjectID,
|
||||
const CFStringRef __nonnull inDeviceName,
|
||||
const CFStringRef __nonnull inDeviceUID,
|
||||
const CFStringRef __nonnull inDeviceModelUID,
|
||||
AudioObjectID inInputStreamID,
|
||||
AudioObjectID inOutputStreamID,
|
||||
AudioObjectID inOutputVolumeControlID,
|
||||
AudioObjectID inOutputMuteControlID);
|
||||
virtual ~BGM_Device();
|
||||
|
||||
virtual void Activate();
|
||||
@@ -87,15 +98,6 @@ private:
|
||||
void Device_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
|
||||
void Device_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
|
||||
|
||||
#pragma mark Control Property Operations
|
||||
|
||||
private:
|
||||
bool Control_HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
|
||||
bool Control_IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
|
||||
UInt32 Control_GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData) const;
|
||||
void Control_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
|
||||
void Control_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
public:
|
||||
@@ -113,10 +115,6 @@ private:
|
||||
void ReadInputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, void* __nonnull outBuffer);
|
||||
void WriteOutputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, const void* __nonnull inBuffer);
|
||||
void ApplyClientRelativeVolume(UInt32 inClientID, UInt32 inIOBufferFrameSize, void* __nonnull inBuffer) const;
|
||||
bool BufferIsAudible(UInt32 inIOBufferFrameSize, const void* __nonnull inBuffer);
|
||||
void UpdateAudibleStateSampleTimes_PreMix(UInt32 inClientID, UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
|
||||
void UpdateAudibleStateSampleTimes_PostMix(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
|
||||
void UpdateDeviceAudibleState(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime);
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
@@ -133,6 +131,22 @@ public:
|
||||
void RequestSampleRate(Float64 inRequestedSampleRate);
|
||||
|
||||
private:
|
||||
/*!
|
||||
@return The Audio Object that has the ID inObjectID and belongs to this device.
|
||||
@throws CAException if there is no such Audio Object.
|
||||
*/
|
||||
const BGM_Object& GetOwnedObjectByID(AudioObjectID inObjectID) const;
|
||||
BGM_Object& GetOwnedObjectByID(AudioObjectID inObjectID);
|
||||
|
||||
/*! @return The number of Audio Objects belonging to this device, e.g. streams and controls. */
|
||||
UInt32 GetNumberOfSubObjects() const;
|
||||
/*! @return The number of Audio Objects with output scope belonging to this device. */
|
||||
UInt32 GetNumberOfOutputSubObjects() const;
|
||||
/*!
|
||||
@return The number of control Audio Objects with output scope belonging to this device, e.g.
|
||||
output volume and mute controls.
|
||||
*/
|
||||
UInt32 GetNumberOfOutputControls() const;
|
||||
/*!
|
||||
Enable or disable the device's volume and/or mute controls.
|
||||
|
||||
@@ -148,19 +162,16 @@ private:
|
||||
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
|
||||
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
|
||||
|
||||
@param inNewSampleRate The sample rate.
|
||||
@param force If true, set the sample rate on the device even if it's currently set to
|
||||
inNewSampleRate.
|
||||
@throws CAException if inNewSampleRate < 1 or if applying the sample rate to one of the streams
|
||||
fails.
|
||||
*/
|
||||
void SetSampleRate(Float64 inNewSampleRate);
|
||||
void SetSampleRate(Float64 inNewSampleRate, bool force = false);
|
||||
|
||||
/*! @return True if inObjectID is the ID of one of this device's streams. */
|
||||
bool IsStreamID(AudioObjectID inObjectID) const noexcept;
|
||||
/*!
|
||||
@return The stream that has the ID inObjectID and belongs to this device.
|
||||
@throws CAException if there is no such stream (i.e. if inObjectID is neither
|
||||
kObjectID_Stream_Input nor kObjectID_Stream_Output.)
|
||||
*/
|
||||
const BGM_Stream& GetStreamByID(AudioObjectID inObjectID) const;
|
||||
inline bool IsStreamID(AudioObjectID inObjectID) const noexcept;
|
||||
|
||||
#pragma mark Hardware Accessors
|
||||
|
||||
@@ -172,15 +183,11 @@ private:
|
||||
Float64 _HW_GetSampleRate() const;
|
||||
kern_return_t _HW_SetSampleRate(Float64 inNewSampleRate);
|
||||
UInt32 _HW_GetRingBufferFrameSize() const;
|
||||
SInt32 _HW_GetVolumeControlValue(AudioObjectID inObjectID) const;
|
||||
kern_return_t _HW_SetVolumeControlValue(AudioObjectID inObjectID, SInt32 inNewControlValue);
|
||||
UInt32 _HW_GetMuteControlValue(AudioObjectID inObjectID) const;
|
||||
kern_return_t _HW_SetMuteControlValue(AudioObjectID inObjectID, UInt32 inValue);
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
public:
|
||||
CFStringRef __nonnull CopyDeviceUID() const { return CFSTR(kBGMDeviceUID); }
|
||||
CFStringRef __nonnull CopyDeviceUID() const { return mDeviceUID; }
|
||||
void AddClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
|
||||
void RemoveClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
|
||||
/*!
|
||||
@@ -194,29 +201,33 @@ public:
|
||||
private:
|
||||
static pthread_once_t sStaticInitializer;
|
||||
static BGM_Device* __nonnull sInstance;
|
||||
static BGM_Device* __nonnull sUISoundsInstance;
|
||||
|
||||
#define kDeviceName "Background Music Device"
|
||||
#define kDeviceName "Background Music"
|
||||
#define kDeviceName_UISounds "Background Music (UI Sounds)"
|
||||
#define kDeviceManufacturerName "Background Music contributors"
|
||||
|
||||
|
||||
const CFStringRef __nonnull mDeviceName;
|
||||
const CFStringRef __nonnull mDeviceUID;
|
||||
const CFStringRef __nonnull mDeviceModelUID;
|
||||
|
||||
enum
|
||||
{
|
||||
kNumberOfSubObjects = 4,
|
||||
// The number of global/output sub-objects varies because the controls can be disabled.
|
||||
kNumberOfInputSubObjects = 1,
|
||||
kNumberOfOutputSubObjects = 3,
|
||||
|
||||
|
||||
kNumberOfStreams = 2,
|
||||
kNumberOfInputStreams = 1,
|
||||
kNumberOfOutputStreams = 1
|
||||
};
|
||||
|
||||
|
||||
CAMutex mStateMutex;
|
||||
CAMutex mIOMutex;
|
||||
|
||||
UInt64 __unused mSampleRateShadow; // Currently unused.
|
||||
const Float64 kSampleRateDefault = 44100.0;
|
||||
// Before we can change sample rate, the host has to stop the device. The new sample rate is
|
||||
// stored here while it does.
|
||||
Float64 mPendingSampleRate;
|
||||
Float64 mPendingSampleRate = kSampleRateDefault;
|
||||
|
||||
BGM_WrappedAudioEngine* __nullable mWrappedAudioEngine;
|
||||
|
||||
@@ -236,15 +247,8 @@ private:
|
||||
|
||||
BGM_Stream mInputStream;
|
||||
BGM_Stream mOutputStream;
|
||||
|
||||
SInt32 mDeviceAudibleState;
|
||||
struct
|
||||
{
|
||||
Float64 latestAudibleNonMusic;
|
||||
Float64 latestSilent;
|
||||
Float64 latestAudibleMusic;
|
||||
Float64 latestSilentMusic;
|
||||
} mAudibleStateSampleTimes;
|
||||
|
||||
BGM_AudibleState mAudibleState;
|
||||
|
||||
enum class ChangeAction : UInt64
|
||||
{
|
||||
@@ -252,25 +256,10 @@ private:
|
||||
SetEnabledControls
|
||||
};
|
||||
|
||||
// This volume range will be used when the BGMDevice isn't wrapping another device (or we fail to
|
||||
// get the range of the wrapped device for some reason).
|
||||
#define kDefaultMinRawVolumeValue 0
|
||||
#define kDefaultMaxRawVolumeValue 96
|
||||
#define kDefaultMinDbVolumeValue -96.0f
|
||||
#define kDefaultMaxDbVolumeValue 0.0f
|
||||
|
||||
bool mOutputVolumeControlEnabled = true;
|
||||
bool mOutputMuteControlEnabled = true;
|
||||
BGM_VolumeControl mVolumeControl;
|
||||
BGM_MuteControl mMuteControl;
|
||||
bool mPendingOutputVolumeControlEnabled = true;
|
||||
bool mPendingOutputMuteControlEnabled = true;
|
||||
|
||||
SInt32 mOutputMasterVolumeControlRawValueShadow;
|
||||
SInt32 mOutputMasterMinRawVolumeShadow;
|
||||
SInt32 mOutputMasterMaxRawVolumeShadow;
|
||||
Float32 mOutputMasterMinDbVolumeShadow;
|
||||
Float32 mOutputMasterMaxDbVolumeShadow;
|
||||
CAVolumeCurve mVolumeCurve;
|
||||
UInt32 mOutputMuteValueShadow;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_MuteControl.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_MuteControl.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_PlugIn.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAException.h"
|
||||
#include "CADispatchQueue.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGM_MuteControl::BGM_MuteControl(AudioObjectID inObjectID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope,
|
||||
AudioObjectPropertyElement inElement)
|
||||
:
|
||||
BGM_Control(inObjectID,
|
||||
kAudioMuteControlClassID,
|
||||
kAudioBooleanControlClassID,
|
||||
inOwnerObjectID,
|
||||
inScope,
|
||||
inElement),
|
||||
mMutex("Mute Control"),
|
||||
mMuted(false)
|
||||
{
|
||||
}
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
bool BGM_MuteControl::HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioBooleanControlPropertyValue:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::HasProperty(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
bool BGM_MuteControl::IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioBooleanControlPropertyValue:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
UInt32 BGM_MuteControl::GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
UInt32 theAnswer = 0;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioBooleanControlPropertyValue:
|
||||
theAnswer = sizeof(UInt32);
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
void BGM_MuteControl::GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioBooleanControlPropertyValue:
|
||||
// This returns the mute value of the control.
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_MuteControl::GetPropertyData: not enough space for the return value "
|
||||
"of kAudioBooleanControlPropertyValue for the mute control");
|
||||
|
||||
CAMutex::Locker theLocker(mMutex);
|
||||
|
||||
// Non-zero for true, which means audio is being muted.
|
||||
*reinterpret_cast<UInt32*>(outData) = mMuted ? 1 : 0;
|
||||
outDataSize = sizeof(UInt32);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Control::GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void BGM_MuteControl::SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData)
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioBooleanControlPropertyValue:
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(UInt32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_MuteControl::SetPropertyData: wrong size for the data for "
|
||||
"kAudioBooleanControlPropertyValue");
|
||||
|
||||
CAMutex::Locker theLocker(mMutex);
|
||||
|
||||
// Non-zero for true, meaning audio will be muted.
|
||||
bool theNewMuted = (*reinterpret_cast<const UInt32*>(inData) != 0);
|
||||
|
||||
if(mMuted != theNewMuted)
|
||||
{
|
||||
mMuted = theNewMuted;
|
||||
|
||||
// Send notifications.
|
||||
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
|
||||
AudioObjectPropertyAddress theChangedProperty[1];
|
||||
theChangedProperty[0] = {
|
||||
kAudioBooleanControlPropertyValue, mScope, mElement
|
||||
};
|
||||
|
||||
BGM_PlugIn::Host_PropertiesChanged(inObjectID, 1, theChangedProperty);
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Control::SetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
inData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_MuteControl.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef BGMDriver__BGM_MuteControl
|
||||
#define BGMDriver__BGM_MuteControl
|
||||
|
||||
// Superclass Includes
|
||||
#include "BGM_Control.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAMutex.h"
|
||||
|
||||
// System Includes
|
||||
#include <MacTypes.h>
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_MuteControl
|
||||
:
|
||||
public BGM_Control
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
BGM_MuteControl(AudioObjectID inObjectID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope =
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
AudioObjectPropertyElement inElement =
|
||||
kAudioObjectPropertyElementMaster);
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
bool HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
|
||||
bool IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
|
||||
UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const;
|
||||
|
||||
void GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const;
|
||||
|
||||
void SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData);
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
private:
|
||||
CAMutex mMutex;
|
||||
bool mMuted;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMDriver__BGM_MuteControl */
|
||||
|
||||
@@ -31,9 +31,7 @@
|
||||
#include "CAException.h"
|
||||
#include "CAPropertyAddress.h"
|
||||
#include "CADispatchQueue.h"
|
||||
|
||||
// System Includes
|
||||
#include <mach/mach_time.h> // For mach_absolute_time
|
||||
#include "CAHostTimeBase.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
@@ -89,12 +87,8 @@ void BGM_NullDevice::Activate()
|
||||
// Call the super-class, which just marks the object as active.
|
||||
BGM_AbstractDevice::Activate();
|
||||
|
||||
// Calculate the host ticks per frame for the clock.
|
||||
struct mach_timebase_info theTimeBaseInfo;
|
||||
mach_timebase_info(&theTimeBaseInfo);
|
||||
Float64 theHostClockFrequency = theTimeBaseInfo.denom / theTimeBaseInfo.numer;
|
||||
theHostClockFrequency *= 1000000000.0;
|
||||
mHostTicksPerFrame = theHostClockFrequency / kSampleRate;
|
||||
// Calculate the number of host clock ticks per frame for this device's clock.
|
||||
mHostTicksPerFrame = CAHostTimeBase::GetFrequency() / kSampleRate;
|
||||
|
||||
SendDeviceIsAlivePropertyNotifications();
|
||||
}
|
||||
@@ -381,6 +375,20 @@ void BGM_NullDevice::SetPropertyData(AudioObjectID inObjectID,
|
||||
inDataSize,
|
||||
inData);
|
||||
}
|
||||
else if(inObjectID == GetObjectID())
|
||||
{
|
||||
BGM_AbstractDevice::SetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
inData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Throw(CAException(kAudioHardwareBadObjectError));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark IO Operations
|
||||
@@ -396,7 +404,7 @@ void BGM_NullDevice::StartIO(UInt32 inClientID)
|
||||
{
|
||||
// Reset the clock.
|
||||
mNumberTimeStamps = 0;
|
||||
mAnchorHostTime = mach_absolute_time();
|
||||
mAnchorHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
|
||||
// Send notifications.
|
||||
DebugMsg("BGM_NullDevice::StartIO: Sending kAudioDevicePropertyDeviceIsRunning");
|
||||
@@ -446,7 +454,7 @@ void BGM_NullDevice::GetZeroTimeStamp(Float64& outSampleTime,
|
||||
// clockless devices don't need to, but if the device doesn't have
|
||||
// kAudioDevicePropertyZeroTimeStampPeriod the HAL seems to reject it. So we give it a simple
|
||||
// clock similar to the loopback clock in BGM_Device.
|
||||
UInt64 theCurrentHostTime = mach_absolute_time();
|
||||
UInt64 theCurrentHostTime = CAHostTimeBase::GetTheCurrentTime();
|
||||
|
||||
// Calculate the next host time.
|
||||
Float64 theHostTicksPerPeriod = mHostTicksPerFrame * static_cast<Float64>(kZeroTimeStampPeriod);
|
||||
|
||||
@@ -101,7 +101,6 @@ bool BGM_Object::IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID,
|
||||
|
||||
default:
|
||||
Throw(CAException(kAudioHardwareUnknownPropertyError));
|
||||
break;
|
||||
};
|
||||
return theAnswer;
|
||||
}
|
||||
@@ -128,7 +127,6 @@ UInt32 BGM_Object::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
|
||||
|
||||
default:
|
||||
Throw(CAException(kAudioHardwareUnknownPropertyError));
|
||||
break;
|
||||
};
|
||||
return theAnswer;
|
||||
}
|
||||
@@ -170,7 +168,6 @@ void BGM_Object::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
|
||||
|
||||
default:
|
||||
Throw(CAException(kAudioHardwareUnknownPropertyError));
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,7 +179,6 @@ void BGM_Object::SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
|
||||
{
|
||||
default:
|
||||
Throw(CAException(kAudioHardwareUnknownPropertyError));
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,9 @@ UInt32 BGM_PlugIn::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
|
||||
|
||||
case kAudioObjectPropertyOwnedObjects:
|
||||
case kAudioPlugInPropertyDeviceList:
|
||||
theAnswer = (BGM_NullDevice::GetInstance().IsActive() ? 2 : 1) * sizeof(AudioObjectID);
|
||||
// The plug-in owns the main BGM_Device, the instance of BGM_Device that handles UI
|
||||
// sounds and, if it's enabled, the null device.
|
||||
theAnswer = (BGM_NullDevice::GetInstance().IsActive() ? 3 : 2) * sizeof(AudioObjectID);
|
||||
break;
|
||||
|
||||
case kAudioPlugInPropertyTranslateUIDToDevice:
|
||||
@@ -181,12 +183,32 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
|
||||
case kAudioPlugInPropertyDeviceList:
|
||||
{
|
||||
AudioObjectID* theReturnedDeviceList = reinterpret_cast<AudioObjectID*>(outData);
|
||||
if((inDataSize >= 2 * sizeof(AudioObjectID)) && BGM_NullDevice::GetInstance().IsActive())
|
||||
if(inDataSize >= 3 * sizeof(AudioObjectID))
|
||||
{
|
||||
if(BGM_NullDevice::GetInstance().IsActive())
|
||||
{
|
||||
theReturnedDeviceList[0] = kObjectID_Device;
|
||||
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
|
||||
theReturnedDeviceList[2] = kObjectID_Device_Null;
|
||||
|
||||
// say how much we returned
|
||||
outDataSize = 3 * sizeof(AudioObjectID);
|
||||
}
|
||||
else
|
||||
{
|
||||
theReturnedDeviceList[0] = kObjectID_Device;
|
||||
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
|
||||
|
||||
// say how much we returned
|
||||
outDataSize = 2 * sizeof(AudioObjectID);
|
||||
}
|
||||
}
|
||||
else if(inDataSize >= 2 * sizeof(AudioObjectID))
|
||||
{
|
||||
theReturnedDeviceList[0] = kObjectID_Device;
|
||||
theReturnedDeviceList[1] = kObjectID_Device_Null;
|
||||
|
||||
// say how much we returned
|
||||
theReturnedDeviceList[1] = kObjectID_Device_UI_Sounds;
|
||||
|
||||
// say how much we returned
|
||||
outDataSize = 2 * sizeof(AudioObjectID);
|
||||
}
|
||||
else if(inDataSize >= sizeof(AudioObjectID))
|
||||
@@ -218,6 +240,12 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
|
||||
"kAudioPlugInPropertyTranslateUIDToDevice");
|
||||
*outID = kObjectID_Device;
|
||||
}
|
||||
else if(CFEqual(theUID, BGM_Device::GetUISoundsInstance().CopyDeviceUID()))
|
||||
{
|
||||
DebugMsg("BGM_PlugIn::GetPropertyData: Returning BGMUISoundsDevice for "
|
||||
"kAudioPlugInPropertyTranslateUIDToDevice");
|
||||
*outID = kObjectID_Device_UI_Sounds;
|
||||
}
|
||||
else if(BGM_NullDevice::GetInstance().IsActive() &&
|
||||
CFEqual(theUID, BGM_NullDevice::GetInstance().CopyDeviceUID()))
|
||||
{
|
||||
|
||||
@@ -98,6 +98,7 @@ static AudioServerPlugInDriverInterface* gAudioServerPlugInDriverInterfacePtr =
|
||||
static AudioServerPlugInDriverRef gAudioServerPlugInDriverRef = &gAudioServerPlugInDriverInterfacePtr;
|
||||
static UInt32 gAudioServerPlugInDriverRefCount = 1;
|
||||
|
||||
// TODO: This name is a bit misleading because the devices are actually owned by the plug-in.
|
||||
static BGM_Object& BGM_LookUpOwnerObject(AudioObjectID inObjectID)
|
||||
{
|
||||
switch(inObjectID)
|
||||
@@ -112,6 +113,12 @@ static BGM_Object& BGM_LookUpOwnerObject(AudioObjectID inObjectID)
|
||||
case kObjectID_Mute_Output_Master:
|
||||
return BGM_Device::GetInstance();
|
||||
|
||||
case kObjectID_Device_UI_Sounds:
|
||||
case kObjectID_Stream_Input_UI_Sounds:
|
||||
case kObjectID_Stream_Output_UI_Sounds:
|
||||
case kObjectID_Volume_Output_Master_UI_Sounds:
|
||||
return BGM_Device::GetUISoundsInstance();
|
||||
|
||||
case kObjectID_Device_Null:
|
||||
case kObjectID_Stream_Null:
|
||||
return BGM_NullDevice::GetInstance();
|
||||
@@ -128,6 +135,9 @@ static BGM_AbstractDevice& BGM_LookUpDevice(AudioObjectID inObjectID)
|
||||
case kObjectID_Device:
|
||||
return BGM_Device::GetInstance();
|
||||
|
||||
case kObjectID_Device_UI_Sounds:
|
||||
return BGM_Device::GetUISoundsInstance();
|
||||
|
||||
case kObjectID_Device_Null:
|
||||
return BGM_NullDevice::GetInstance();
|
||||
}
|
||||
@@ -276,6 +286,7 @@ static OSStatus BGM_Initialize(AudioServerPlugInDriverRef inDriver, AudioServerP
|
||||
|
||||
// Init/activate the devices.
|
||||
BGM_Device::GetInstance();
|
||||
BGM_Device::GetUISoundsInstance();
|
||||
BGM_NullDevice::GetInstance();
|
||||
}
|
||||
catch(const CAException& inException)
|
||||
@@ -325,7 +336,7 @@ static OSStatus BGM_AddDeviceClient(AudioServerPlugInDriverRef inDriver, AudioOb
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_AddDeviceClient: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_AddDeviceClient: unknown device");
|
||||
|
||||
@@ -361,7 +372,7 @@ static OSStatus BGM_RemoveDeviceClient(AudioServerPlugInDriverRef inDriver, Audi
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_RemoveDeviceClient: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_RemoveDeviceClient: unknown device");
|
||||
|
||||
@@ -404,7 +415,7 @@ static OSStatus BGM_PerformDeviceConfigurationChange(AudioServerPlugInDriverRef
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_PerformDeviceConfigurationChange: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_PerformDeviceConfigurationChange: unknown device");
|
||||
|
||||
@@ -436,7 +447,7 @@ static OSStatus BGM_AbortDeviceConfigurationChange(AudioServerPlugInDriverRef in
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_PerformDeviceConfigurationChange: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_PerformDeviceConfigurationChange: unknown device");
|
||||
|
||||
@@ -667,7 +678,7 @@ static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_StartIO: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_StartIO: unknown device");
|
||||
|
||||
@@ -701,7 +712,7 @@ static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
|
||||
CAException(kAudioHardwareBadObjectError),
|
||||
"BGM_StopIO: bad driver reference");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_StopIO: unknown device");
|
||||
|
||||
@@ -751,7 +762,7 @@ static OSStatus BGM_GetZeroTimeStamp(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIfNULL(outSeed,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_GetZeroTimeStamp: no place to put the seed");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_GetZeroTimeStamp: unknown device");
|
||||
|
||||
@@ -794,7 +805,7 @@ static OSStatus BGM_WillDoIOOperation(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIfNULL(outWillDoInPlace,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_WillDoIOOperation: no place to put the in-place return value");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_WillDoIOOperation: unknown device");
|
||||
|
||||
@@ -839,7 +850,7 @@ static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIfNULL(inIOCycleInfo,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_BeginIOOperation: no cycle info");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_BeginIOOperation: unknown device");
|
||||
|
||||
@@ -887,7 +898,7 @@ static OSStatus BGM_DoIOOperation(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIfNULL(inIOCycleInfo,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_EndIOOperation: no cycle info");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_EndIOOperation: unknown device");
|
||||
|
||||
@@ -935,7 +946,7 @@ static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver,
|
||||
ThrowIfNULL(inIOCycleInfo,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_EndIOOperation: no cycle info");
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
|
||||
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_UI_Sounds && inDeviceObjectID != kObjectID_Device_Null,
|
||||
CAException(kAudioHardwareBadDeviceError),
|
||||
"BGM_EndIOOperation: unknown device");
|
||||
|
||||
|
||||
@@ -98,14 +98,16 @@ BGM_TaskQueue::BGM_TaskQueue()
|
||||
BGM_TaskQueue::~BGM_TaskQueue()
|
||||
{
|
||||
// Join the worker threads
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ true);
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ false);
|
||||
|
||||
BGMLogAndSwallowExceptionsMsg("BGM_TaskQueue::~BGM_TaskQueue", "QueueSync", ([&] {
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ true);
|
||||
QueueSync(kBGMTaskStopWorkerThread, /* inRunOnRealtimeThread = */ false);
|
||||
}));
|
||||
|
||||
// Destroy the semaphores
|
||||
auto destroySemaphore = [] (semaphore_t inSemaphore) {
|
||||
kern_return_t theError = semaphore_destroy(mach_task_self(), inSemaphore);
|
||||
|
||||
BGM_Utils::ThrowIfMachError("BGM_TaskQueue::~BGM_TaskQueue", "semaphore_destroy", theError);
|
||||
BGM_Utils::LogIfMachError("BGM_TaskQueue::~BGM_TaskQueue", "semaphore_destroy", theError);
|
||||
};
|
||||
|
||||
destroySemaphore(mRealTimeThreadWorkQueuedSemaphore);
|
||||
@@ -157,10 +159,12 @@ void BGM_TaskQueue::QueueSync_SwapClientShadowMaps(BGM_ClientMap* inClientMap
|
||||
QueueSync(kBGMTaskSwapClientShadowMaps, /* inRunOnRealtimeThread = */ true, reinterpret_cast<UInt64>(inClientMap));
|
||||
}
|
||||
|
||||
void BGM_TaskQueue::QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty)
|
||||
void BGM_TaskQueue::QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty, AudioObjectID inDeviceID)
|
||||
{
|
||||
DebugMsg("BGM_TaskQueue::QueueAsync_SendPropertyNotification: Queueing property notification. inProperty=%u", inProperty);
|
||||
BGM_Task theTask(kBGMTaskSendPropertyNotification, /* inIsSync = */ false, inProperty);
|
||||
DebugMsg("BGM_TaskQueue::QueueAsync_SendPropertyNotification: Queueing property notification. inProperty=%u inDeviceID=%u",
|
||||
inProperty,
|
||||
inDeviceID);
|
||||
BGM_Task theTask(kBGMTaskSendPropertyNotification, /* inIsSync = */ false, inProperty, inDeviceID);
|
||||
QueueOnNonRealtimeThread(theTask);
|
||||
}
|
||||
|
||||
@@ -468,7 +472,7 @@ bool BGM_TaskQueue::ProcessNonRealTimeThreadTask(BGM_Task* inTask)
|
||||
{
|
||||
AudioObjectPropertyAddress thePropertyAddress[] = {
|
||||
{ static_cast<UInt32>(inTask->GetArg1()), kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster } };
|
||||
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, 1, thePropertyAddress);
|
||||
BGM_PlugIn::Host_PropertiesChanged(static_cast<AudioObjectID>(inTask->GetArg2()), 1, thePropertyAddress);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ public:
|
||||
|
||||
// Sends a property changed notification to the BGMDevice host. Assumes the scope and element are kAudioObjectPropertyScopeGlobal and
|
||||
// kAudioObjectPropertyElementMaster because currently those are the only ones we use.
|
||||
void QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty);
|
||||
void QueueAsync_SendPropertyNotification(AudioObjectPropertySelector inProperty, AudioObjectID inDeviceID);
|
||||
|
||||
// Set/unset a client's is-doing-IO flag
|
||||
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_VolumeControl.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_VolumeControl.h"
|
||||
|
||||
// Local Includes
|
||||
#include "BGM_PlugIn.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAException.h"
|
||||
#include "CADebugMacros.h"
|
||||
#include "CADispatchQueue.h"
|
||||
#include "BGM_Utils.h"
|
||||
|
||||
// STL Includes
|
||||
#include <algorithm>
|
||||
|
||||
// System Includes
|
||||
#include <CoreAudio/AudioHardwareBase.h>
|
||||
#include <Accelerate/Accelerate.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGM_VolumeControl::BGM_VolumeControl(AudioObjectID inObjectID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope,
|
||||
AudioObjectPropertyElement inElement)
|
||||
:
|
||||
BGM_Control(inObjectID,
|
||||
kAudioVolumeControlClassID,
|
||||
kAudioLevelControlClassID,
|
||||
inOwnerObjectID,
|
||||
inScope,
|
||||
inElement),
|
||||
mMutex("Volume Control"),
|
||||
mVolumeRaw(kDefaultMinRawVolume),
|
||||
mAmplitudeGain(0.0f),
|
||||
mMinVolumeRaw(kDefaultMinRawVolume),
|
||||
mMaxVolumeRaw(kDefaultMaxRawVolume),
|
||||
mMinVolumeDb(kDefaultMinDbVolume),
|
||||
mMaxVolumeDb(kDefaultMaxDbVolume),
|
||||
mWillApplyVolumeToAudio(false)
|
||||
{
|
||||
// Setup the volume curve with the one range
|
||||
mVolumeCurve.AddRange(mMinVolumeRaw, mMaxVolumeRaw, mMinVolumeDb, mMaxVolumeDb);
|
||||
}
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
bool BGM_VolumeControl::HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioLevelControlPropertyScalarValue:
|
||||
case kAudioLevelControlPropertyDecibelValue:
|
||||
case kAudioLevelControlPropertyDecibelRange:
|
||||
case kAudioLevelControlPropertyConvertScalarToDecibels:
|
||||
case kAudioLevelControlPropertyConvertDecibelsToScalar:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::HasProperty(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
bool BGM_VolumeControl::IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
bool theAnswer = false;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioLevelControlPropertyDecibelRange:
|
||||
case kAudioLevelControlPropertyConvertScalarToDecibels:
|
||||
case kAudioLevelControlPropertyConvertDecibelsToScalar:
|
||||
theAnswer = false;
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyScalarValue:
|
||||
case kAudioLevelControlPropertyDecibelValue:
|
||||
theAnswer = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::IsPropertySettable(inObjectID, inClientPID, inAddress);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
UInt32 BGM_VolumeControl::GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
UInt32 theAnswer = 0;
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioLevelControlPropertyScalarValue:
|
||||
theAnswer = sizeof(Float32);
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyDecibelValue:
|
||||
theAnswer = sizeof(Float32);
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyDecibelRange:
|
||||
theAnswer = sizeof(AudioValueRange);
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyConvertScalarToDecibels:
|
||||
theAnswer = sizeof(Float32);
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyConvertDecibelsToScalar:
|
||||
theAnswer = sizeof(Float32);
|
||||
break;
|
||||
|
||||
default:
|
||||
theAnswer = BGM_Control::GetPropertyDataSize(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData);
|
||||
break;
|
||||
};
|
||||
|
||||
return theAnswer;
|
||||
}
|
||||
|
||||
void BGM_VolumeControl::GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioLevelControlPropertyScalarValue:
|
||||
// This returns the value of the control in the normalized range of 0 to 1.
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
|
||||
"of kAudioLevelControlPropertyScalarValue for the volume control");
|
||||
|
||||
CAMutex::Locker theLocker(mMutex);
|
||||
|
||||
*reinterpret_cast<Float32*>(outData) = mVolumeCurve.ConvertRawToScalar(mVolumeRaw);
|
||||
outDataSize = sizeof(Float32);
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyDecibelValue:
|
||||
// This returns the dB value of the control.
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
|
||||
"of kAudioLevelControlPropertyDecibelValue for the volume control");
|
||||
|
||||
CAMutex::Locker theLocker(mMutex);
|
||||
|
||||
*reinterpret_cast<Float32*>(outData) = mVolumeCurve.ConvertRawToDB(mVolumeRaw);
|
||||
outDataSize = sizeof(Float32);
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyDecibelRange:
|
||||
// This returns the dB range of the control.
|
||||
ThrowIf(inDataSize < sizeof(AudioValueRange),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::GetPropertyData: not enough space for the return value of "
|
||||
"kAudioLevelControlPropertyDecibelRange for the volume control");
|
||||
reinterpret_cast<AudioValueRange*>(outData)->mMinimum = mVolumeCurve.GetMinimumDB();
|
||||
reinterpret_cast<AudioValueRange*>(outData)->mMaximum = mVolumeCurve.GetMaximumDB();
|
||||
outDataSize = sizeof(AudioValueRange);
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyConvertScalarToDecibels:
|
||||
// This takes the scalar value in outData and converts it to dB.
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
|
||||
"of kAudioLevelControlPropertyConvertScalarToDecibels for the volume "
|
||||
"control");
|
||||
|
||||
// clamp the value to be between 0 and 1
|
||||
Float32 theVolumeValue = *reinterpret_cast<Float32*>(outData);
|
||||
theVolumeValue = std::min(1.0f, std::max(0.0f, theVolumeValue));
|
||||
|
||||
// do the conversion
|
||||
*reinterpret_cast<Float32*>(outData) =
|
||||
mVolumeCurve.ConvertScalarToDB(theVolumeValue);
|
||||
|
||||
// report how much we wrote
|
||||
outDataSize = sizeof(Float32);
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyConvertDecibelsToScalar:
|
||||
// This takes the dB value in outData and converts it to scalar.
|
||||
{
|
||||
ThrowIf(inDataSize < sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::GetPropertyData: not enough space for the return value "
|
||||
"of kAudioLevelControlPropertyConvertDecibelsToScalar for the volume "
|
||||
"control");
|
||||
|
||||
// clamp the value to be between mMinVolumeDb and mMaxVolumeDb
|
||||
Float32 theVolumeValue = *reinterpret_cast<Float32*>(outData);
|
||||
theVolumeValue = std::min(mMaxVolumeDb, std::max(mMinVolumeDb, theVolumeValue));
|
||||
|
||||
// do the conversion
|
||||
*reinterpret_cast<Float32*>(outData) =
|
||||
mVolumeCurve.ConvertDBToScalar(theVolumeValue);
|
||||
|
||||
// report how much we wrote
|
||||
outDataSize = sizeof(Float32);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Control::GetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
outDataSize,
|
||||
outData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void BGM_VolumeControl::SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData)
|
||||
{
|
||||
CheckObjectID(inObjectID);
|
||||
|
||||
switch(inAddress.mSelector)
|
||||
{
|
||||
case kAudioLevelControlPropertyScalarValue:
|
||||
{
|
||||
ThrowIf(inDataSize != sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::SetPropertyData: wrong size for the data for "
|
||||
"kAudioLevelControlPropertyScalarValue");
|
||||
|
||||
// Read the new scalar volume.
|
||||
Float32 theNewVolumeScalar = *reinterpret_cast<const Float32*>(inData);
|
||||
SetVolumeScalar(theNewVolumeScalar);
|
||||
}
|
||||
break;
|
||||
|
||||
case kAudioLevelControlPropertyDecibelValue:
|
||||
{
|
||||
ThrowIf(inDataSize != sizeof(Float32),
|
||||
CAException(kAudioHardwareBadPropertySizeError),
|
||||
"BGM_VolumeControl::SetPropertyData: wrong size for the data for "
|
||||
"kAudioLevelControlPropertyDecibelValue");
|
||||
|
||||
// Read the new volume in dB.
|
||||
Float32 theNewVolumeDb = *reinterpret_cast<const Float32*>(inData);
|
||||
SetVolumeDb(theNewVolumeDb);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
BGM_Control::SetPropertyData(inObjectID,
|
||||
inClientPID,
|
||||
inAddress,
|
||||
inQualifierDataSize,
|
||||
inQualifierData,
|
||||
inDataSize,
|
||||
inData);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
void BGM_VolumeControl::SetVolumeScalar(Float32 inNewVolumeScalar)
|
||||
{
|
||||
// For the scalar volume, we clamp the new value to [0, 1]. Note that if this value changes, it
|
||||
// implies that the dB value changes too.
|
||||
inNewVolumeScalar = std::min(1.0f, std::max(0.0f, inNewVolumeScalar));
|
||||
|
||||
// Store the new volume.
|
||||
SInt32 theNewVolumeRaw = mVolumeCurve.ConvertScalarToRaw(inNewVolumeScalar);
|
||||
SetVolumeRaw(theNewVolumeRaw);
|
||||
}
|
||||
|
||||
void BGM_VolumeControl::SetVolumeDb(Float32 inNewVolumeDb)
|
||||
{
|
||||
// For the dB value, we first convert it to a raw value since that is how the value is tracked.
|
||||
// Note that if this value changes, it implies that the scalar value changes as well.
|
||||
|
||||
// Clamp the new volume.
|
||||
inNewVolumeDb = std::min(mMaxVolumeDb, std::max(mMinVolumeDb, inNewVolumeDb));
|
||||
|
||||
// Store the new volume.
|
||||
SInt32 theNewVolumeRaw = mVolumeCurve.ConvertDBToRaw(inNewVolumeDb);
|
||||
SetVolumeRaw(theNewVolumeRaw);
|
||||
}
|
||||
|
||||
void BGM_VolumeControl::SetWillApplyVolumeToAudio(bool inWillApplyVolumeToAudio)
|
||||
{
|
||||
mWillApplyVolumeToAudio = inWillApplyVolumeToAudio;
|
||||
}
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
bool BGM_VolumeControl::WillApplyVolumeToAudioRT() const
|
||||
{
|
||||
return mWillApplyVolumeToAudio;
|
||||
}
|
||||
|
||||
void BGM_VolumeControl::ApplyVolumeToAudioRT(Float32* ioBuffer, UInt32 inBufferFrameSize) const
|
||||
{
|
||||
ThrowIf(!mWillApplyVolumeToAudio,
|
||||
CAException(kAudioHardwareIllegalOperationError),
|
||||
"BGM_VolumeControl::ApplyVolumeToAudioRT: This control doesn't process audio data");
|
||||
|
||||
// Don't bother if the change is very unlikely to be perceptible.
|
||||
if((mAmplitudeGain < 0.99f) || (mAmplitudeGain > 1.01f))
|
||||
{
|
||||
// Apply the amount of gain/loss for the current volume to the audio signal by multiplying
|
||||
// each sample. This call to vDSP_vsmul is equivalent to
|
||||
//
|
||||
// for(UInt32 i = 0; i < inBufferFrameSize * 2; i++)
|
||||
// {
|
||||
// ioBuffer[i] *= mAmplitudeGain;
|
||||
// }
|
||||
//
|
||||
// but a bit faster on processors with newer SIMD instructions. However, it shouldn't take
|
||||
// more than a few microseconds either way. (Unless some of the samples were subnormal
|
||||
// numbers for some reason.)
|
||||
//
|
||||
// It would be a tiny bit faster still to not do this in-place, i.e. use separate input and
|
||||
// output buffers, but then we'd have to copy the data into the output buffer when the
|
||||
// volume is at 1.0. With our current use of this class, most people will leave the volume
|
||||
// at 1.0, so it wouldn't be worth it.
|
||||
vDSP_vsmul(ioBuffer, 1, &mAmplitudeGain, ioBuffer, 1, inBufferFrameSize * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
void BGM_VolumeControl::SetVolumeRaw(SInt32 inNewVolumeRaw)
|
||||
{
|
||||
CAMutex::Locker theLocker(mMutex);
|
||||
|
||||
// Make sure the new raw value is in the proper range.
|
||||
inNewVolumeRaw = std::min(std::max(mMinVolumeRaw, inNewVolumeRaw), mMaxVolumeRaw);
|
||||
|
||||
// Store the new volume.
|
||||
if(mVolumeRaw != inNewVolumeRaw)
|
||||
{
|
||||
mVolumeRaw = inNewVolumeRaw;
|
||||
|
||||
// CAVolumeCurve deals with volumes in three different scales: scalar, dB and raw. Raw
|
||||
// volumes are the number of steps along the dB curve, so dB and raw volumes are linearly
|
||||
// related.
|
||||
//
|
||||
// macOS uses the scalar volume to set the position of its volume sliders for the
|
||||
// device. We have to set the scalar volume to the position of our volume slider for a
|
||||
// device (more specifically, a linear mapping of it onto [0,1]) or macOS's volume sliders
|
||||
// or it will work differently to our own.
|
||||
//
|
||||
// When we set a new slider position as the device's scalar volume, we convert it to raw
|
||||
// with CAVolumeCurve::ConvertScalarToRaw, which will "undo the curve". However, we haven't
|
||||
// applied the curve at that point.
|
||||
//
|
||||
// So, to actually apply the curve, we use CAVolumeCurve::ConvertRawToScalar to get the
|
||||
// linear slider position back, map it onto the range of raw volumes and use
|
||||
// CAVolumeCurve::ConvertRawToScalar again to apply the curve.
|
||||
//
|
||||
// It might be that we should be using CAVolumeCurve with transfer functions x^n where
|
||||
// 0 < n < 1, but a lot more of the transfer functions it supports have n >= 1, including
|
||||
// the default one. So I'm a bit confused.
|
||||
//
|
||||
// TODO: I think this means the dB volume we report will be wrong. It also makes the code
|
||||
// pretty confusing.
|
||||
Float32 theSliderPosition = mVolumeCurve.ConvertRawToScalar(mVolumeRaw);
|
||||
|
||||
// TODO: This assumes the control should never boost the signal. (So, technically, it never
|
||||
// actually applies gain, only loss.)
|
||||
SInt32 theRawRange = mMaxVolumeRaw - mMinVolumeRaw;
|
||||
SInt32 theSliderPositionInRawSteps = static_cast<SInt32>(theSliderPosition * theRawRange);
|
||||
theSliderPositionInRawSteps += mMinVolumeRaw;
|
||||
|
||||
mAmplitudeGain = mVolumeCurve.ConvertRawToScalar(theSliderPositionInRawSteps);
|
||||
|
||||
BGMAssert((mAmplitudeGain >= 0.0f) && (mAmplitudeGain <= 1.0f), "Gain not in [0,1]");
|
||||
|
||||
// Send notifications.
|
||||
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
|
||||
AudioObjectPropertyAddress theChangedProperties[2];
|
||||
theChangedProperties[0] = { kAudioLevelControlPropertyScalarValue, mScope, mElement };
|
||||
theChangedProperties[1] = { kAudioLevelControlPropertyDecibelValue, mScope, mElement };
|
||||
|
||||
BGM_PlugIn::Host_PropertiesChanged(GetObjectID(), 2, theChangedProperties);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// This file is part of Background Music.
|
||||
//
|
||||
// Background Music is free software: you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation, either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// Background Music is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//
|
||||
// BGM_VolumeControl.h
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef BGMDriver__BGM_VolumeControl
|
||||
#define BGMDriver__BGM_VolumeControl
|
||||
|
||||
// Superclass Includes
|
||||
#include "BGM_Control.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAVolumeCurve.h"
|
||||
#include "CAMutex.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
class BGM_VolumeControl
|
||||
:
|
||||
public BGM_Control
|
||||
{
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
public:
|
||||
BGM_VolumeControl(AudioObjectID inObjectID,
|
||||
AudioObjectID inOwnerObjectID,
|
||||
AudioObjectPropertyScope inScope =
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
AudioObjectPropertyElement inElement =
|
||||
kAudioObjectPropertyElementMaster);
|
||||
|
||||
#pragma mark Property Operations
|
||||
|
||||
virtual bool HasProperty(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual bool IsPropertySettable(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress) const;
|
||||
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData) const;
|
||||
virtual void GetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
UInt32& outDataSize,
|
||||
void* outData) const;
|
||||
virtual void SetPropertyData(AudioObjectID inObjectID,
|
||||
pid_t inClientPID,
|
||||
const AudioObjectPropertyAddress& inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData);
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
/*!
|
||||
@return The curve used by this control to convert volume values from scalar into signal gain
|
||||
and/or decibels. A continuous 2D function.
|
||||
*/
|
||||
CAVolumeCurve& GetVolumeCurve() { return mVolumeCurve; }
|
||||
|
||||
/*!
|
||||
Set the volume of this control to a given position along its volume curve. (See
|
||||
GetVolumeCurve.)
|
||||
|
||||
Passing 1.0 sets the volume to the maximum and 0.0 sets it to the minimum. The gain/loss the
|
||||
control applies (and/or reports to apply) to the audio it controls is given by the y-position
|
||||
of the curve at the x-position inNewVolumeScalar.
|
||||
|
||||
In general, since the control's volume curve will be applied to the given value, it should be
|
||||
linearly related to a volume input by the user.
|
||||
|
||||
@param inNewVolumeScalar The volume to set. Will be clamped to [0.0, 1.0].
|
||||
*/
|
||||
void SetVolumeScalar(Float32 inNewVolumeScalar);
|
||||
/*!
|
||||
Set the volume of this control in decibels.
|
||||
|
||||
@param inNewVolumeDb The volume to set. Will be clamped to the minimum/maximum dB volumes of
|
||||
the control. See GetVolumeCurve.
|
||||
*/
|
||||
void SetVolumeDb(Float32 inNewVolumeDb);
|
||||
|
||||
/*!
|
||||
Set this volume control to apply its volume to audio data, which allows clients to call
|
||||
ApplyVolumeToAudioRT. When this is set true, WillApplyVolumeToAudioRT will return true. Set to
|
||||
false initially.
|
||||
*/
|
||||
void SetWillApplyVolumeToAudio(bool inWillApplyVolumeToAudio);
|
||||
|
||||
#pragma mark IO Operations
|
||||
|
||||
/*!
|
||||
@return True if clients should use ApplyVolumeToAudioRT to apply this volume control's volume
|
||||
to their audio data while doing IO.
|
||||
*/
|
||||
bool WillApplyVolumeToAudioRT() const;
|
||||
/*!
|
||||
Apply this volume control's volume to the samples in ioBuffer. That is, increase/decrease the
|
||||
volumes of the samples by the current volume of this control.
|
||||
|
||||
@param ioBuffer The audio sample buffer to process.
|
||||
@param inBufferFrameSize The number of sample frames in ioBuffer. The audio is assumed to be in
|
||||
stereo, i.e. two samples per frame. (Though, hopefully we'll support
|
||||
more at some point.)
|
||||
@throws CAException If SetWillApplyVolumeToAudio hasn't been used to set this control to apply
|
||||
its volume to audio data.
|
||||
*/
|
||||
void ApplyVolumeToAudioRT(Float32* ioBuffer, UInt32 inBufferFrameSize) const;
|
||||
|
||||
#pragma mark Implementation
|
||||
|
||||
protected:
|
||||
void SetVolumeRaw(SInt32 inNewVolumeRaw);
|
||||
|
||||
private:
|
||||
const SInt32 kDefaultMinRawVolume = 0;
|
||||
const SInt32 kDefaultMaxRawVolume = 96;
|
||||
const Float32 kDefaultMinDbVolume = -96.0f;
|
||||
const Float32 kDefaultMaxDbVolume = 0.0f;
|
||||
|
||||
CAMutex mMutex;
|
||||
|
||||
SInt32 mVolumeRaw;
|
||||
SInt32 mMinVolumeRaw;
|
||||
SInt32 mMaxVolumeRaw;
|
||||
Float32 mMinVolumeDb;
|
||||
Float32 mMaxVolumeDb;
|
||||
|
||||
CAVolumeCurve mVolumeCurve;
|
||||
// The gain (or loss) to apply to an audio signal to increase/decrease its volume by the current
|
||||
// volume of this control.
|
||||
Float32 mAmplitudeGain;
|
||||
|
||||
bool mWillApplyVolumeToAudio;
|
||||
|
||||
};
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
||||
#endif /* BGMDriver__BGM_VolumeControl */
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef __BGMDriver__BGM_XPCHelper__
|
||||
#define __BGMDriver__BGM_XPCHelper__
|
||||
#ifndef BGMDriver__BGM_XPCHelper
|
||||
#define BGMDriver__BGM_XPCHelper
|
||||
|
||||
// System Includes
|
||||
#include <MacTypes.h>
|
||||
@@ -31,11 +31,11 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// On failure, returns one of the kBGMXPC_* error codes, or the error code received from BGMXPCHelper. Returns kBGMXPC_Success otherwise.
|
||||
UInt64 WaitForBGMAppToStartOutputDevice(void);
|
||||
UInt64 StartBGMAppPlayThroughSync(bool inIsForUISoundsDevice);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __BGMDriver__BGM_XPCHelper__ */
|
||||
#endif /* BGMDriver__BGM_XPCHelper */
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGM_XPCHelper.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -52,13 +52,13 @@ static NSXPCConnection* CreateXPCHelperConnection()
|
||||
theConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMXPCHelperXPCProtocol)];
|
||||
[theConnection resume];
|
||||
} else {
|
||||
@throw(@"BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: initWithMachServiceName returned nil");
|
||||
@throw(@"BGM_XPCHelper::CreateXPCHelperConnection: initWithMachServiceName returned nil");
|
||||
}
|
||||
|
||||
return theConnection;
|
||||
}
|
||||
|
||||
UInt64 WaitForBGMAppToStartOutputDevice()
|
||||
UInt64 StartBGMAppPlayThroughSync(bool inIsForUISoundsDevice)
|
||||
{
|
||||
__block UInt64 theAnswer = kBGMXPC_Success;
|
||||
|
||||
@@ -76,7 +76,7 @@ UInt64 WaitForBGMAppToStartOutputDevice()
|
||||
// Set the failure callbacks to signal the reply semaphore so we can return immediately if BGMXPCHelper can't be reached. (It
|
||||
// doesn't matter how many times we signal the reply semaphore because we create a new one each time.)
|
||||
void (^failureHandler)(void) = ^{
|
||||
DebugMsg("BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: Connection to BGMXPCHelper failed");
|
||||
DebugMsg("BGM_XPCHelper::StartBGMAppPlayThroughSync: Connection to BGMXPCHelper failed");
|
||||
|
||||
theAnswer = kBGMXPC_MessageFailure;
|
||||
dispatch_semaphore_signal(theReplySemaphore);
|
||||
@@ -87,18 +87,25 @@ UInt64 WaitForBGMAppToStartOutputDevice()
|
||||
// This remote call to BGMXPCHelper will send a reply when the output device is ready to receive IO. Note that, for security
|
||||
// reasons, we shouldn't trust the reply object.
|
||||
[[theConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
|
||||
#if !DEBUG
|
||||
#pragma unused (error)
|
||||
#endif
|
||||
DebugMsg("BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: Remote call error: %s",
|
||||
(void)error;
|
||||
DebugMsg("BGM_XPCHelper::StartBGMAppPlayThroughSync: Remote call error: %s",
|
||||
[[error debugDescription] UTF8String]);
|
||||
|
||||
failureHandler();
|
||||
}] waitForBGMAppToStartOutputDeviceWithReply:^(NSError* reply) {
|
||||
DebugMsg("BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: Got reply from BGMXPCHelper: \"%s\"",
|
||||
}] startBGMAppPlayThroughSyncWithReply:^(NSError* reply) {
|
||||
DebugMsg("BGM_XPCHelper::StartBGMAppPlayThroughSync: Got reply from BGMXPCHelper: \"%s\"",
|
||||
[[reply localizedDescription] UTF8String]);
|
||||
|
||||
theAnswer = (UInt64)[reply code];
|
||||
theAnswer = kBGMXPC_MessageFailure;
|
||||
|
||||
@try {
|
||||
if (reply)
|
||||
{
|
||||
theAnswer = (UInt64)[reply code];
|
||||
}
|
||||
} @catch(...) {
|
||||
NSLog(@"BGM_XPCHelper::StartBGMAppPlayThroughSync: Exception while reading reply code");
|
||||
}
|
||||
|
||||
// We only need the connection for one call, which was successful, so the losing the connection is no longer a problem.
|
||||
theConnection.interruptionHandler = nil;
|
||||
@@ -106,9 +113,9 @@ UInt64 WaitForBGMAppToStartOutputDevice()
|
||||
|
||||
// Tell the enclosing function it can return now.
|
||||
dispatch_semaphore_signal(theReplySemaphore);
|
||||
}];
|
||||
} forUISoundsDevice:inIsForUISoundsDevice];
|
||||
|
||||
DebugMsg("BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: Waiting for BGMApp to tell us the output device is ready for IO");
|
||||
DebugMsg("BGM_XPCHelper::StartBGMAppPlayThroughSync: Waiting for BGMApp to tell us the output device is ready for IO");
|
||||
|
||||
// Wait on the reply semaphore until we get the reply (or a connection failure).
|
||||
if (0 != dispatch_semaphore_wait(theReplySemaphore,
|
||||
@@ -117,7 +124,7 @@ UInt64 WaitForBGMAppToStartOutputDevice()
|
||||
//
|
||||
// TODO: It's possible that the output device is just taking a really long time to start. Is there some way we could check for
|
||||
// that, rather than timing out?
|
||||
NSLog(@"BGM_XPCHelper::WaitForBGMAppToStartOutputDevice: Timed out waiting for the Background Music app to start the output device");
|
||||
NSLog(@"BGM_XPCHelper::StartBGMAppPlayThroughSync: Timed out waiting for the Background Music app to start the output device");
|
||||
|
||||
theAnswer = kBGMXPC_Timeout;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public:
|
||||
bool mIsMusicPlayer = false;
|
||||
|
||||
// The client's volume relative to other clients. In the range [0.0, 4.0], defaults to 1.0 (unchanged).
|
||||
// mRelativeVolumeCurve is applied this this value when it's set.
|
||||
// mRelativeVolumeCurve is applied to this value when it's set.
|
||||
Float32 mRelativeVolume = 1.0;
|
||||
|
||||
// The client's pan position, in the range [-100, 100] where -100 is left and 100 is right
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
BGM_Clients::BGM_Clients(BGM_TaskQueue* inTaskQueue)
|
||||
BGM_Clients::BGM_Clients(AudioObjectID inOwnerDeviceID, BGM_TaskQueue* inTaskQueue)
|
||||
:
|
||||
mOwnerDeviceID(inOwnerDeviceID),
|
||||
mClientMap(inTaskQueue)
|
||||
{
|
||||
mRelativeVolumeCurve.AddRange(kAppRelativeVolumeMinRawValue,
|
||||
@@ -110,9 +111,10 @@ bool BGM_Clients::StartIONonRT(UInt32 inClientID)
|
||||
// Make sure we can start
|
||||
ThrowIf(mStartCount == UINT64_MAX, CAException(kAudioHardwareIllegalOperationError), "BGM_Clients::StartIO: failed to start because the ref count was maxxed out already");
|
||||
|
||||
DebugMsg("BGM_Clients::StartIO: Client %u (%s) starting IO",
|
||||
DebugMsg("BGM_Clients::StartIO: Client %u (%s, %d) starting IO",
|
||||
inClientID,
|
||||
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8));
|
||||
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8),
|
||||
theClient.mProcessID);
|
||||
|
||||
mClientMap.StartIONonRT(inClientID);
|
||||
|
||||
@@ -160,9 +162,10 @@ bool BGM_Clients::StopIONonRT(UInt32 inClientID)
|
||||
|
||||
if(theClient.mDoingIO)
|
||||
{
|
||||
DebugMsg("BGM_Clients::StopIO: Client %u (%s) stopping IO",
|
||||
DebugMsg("BGM_Clients::StopIO: Client %u (%s, %d) stopping IO",
|
||||
inClientID,
|
||||
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8));
|
||||
CFStringGetCStringPtr(theClient.mBundleID.GetCFString(), kCFStringEncodingUTF8),
|
||||
theClient.mProcessID);
|
||||
|
||||
mClientMap.StopIONonRT(inClientID);
|
||||
|
||||
@@ -228,7 +231,7 @@ void BGM_Clients::SendIORunningNotifications(bool sendIsRunningNotification,
|
||||
theNotificationCount++;
|
||||
}
|
||||
|
||||
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, theNotificationCount, theChangedProperties);
|
||||
BGM_PlugIn::Host_PropertiesChanged(mOwnerDeviceID, theNotificationCount, theChangedProperties);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -297,7 +300,7 @@ Float32 BGM_Clients::GetClientRelativeVolumeRT(UInt32 inClientID) const
|
||||
{
|
||||
BGM_Client theClient;
|
||||
bool didGetClient = mClientMap.GetClientRT(inClientID, &theClient);
|
||||
return (didGetClient ? theClient.mRelativeVolume : 1.0);
|
||||
return (didGetClient ? theClient.mRelativeVolume : 1.0f);
|
||||
}
|
||||
|
||||
SInt32 BGM_Clients::GetClientPanPositionRT(UInt32 inClientID) const
|
||||
|
||||
@@ -58,7 +58,7 @@ class BGM_Clients
|
||||
friend class BGM_ClientTasks;
|
||||
|
||||
public:
|
||||
BGM_Clients(BGM_TaskQueue* inTaskQueue);
|
||||
BGM_Clients(AudioObjectID inOwnerDeviceID, BGM_TaskQueue* inTaskQueue);
|
||||
~BGM_Clients() = default;
|
||||
// Disallow copying. (It could make sense to implement these in future, but we don't need them currently.)
|
||||
BGM_Clients(const BGM_Clients&) = delete;
|
||||
@@ -111,6 +111,7 @@ public:
|
||||
bool SetClientsRelativeVolumes(const CACFArray inAppVolumes);
|
||||
|
||||
private:
|
||||
AudioObjectID mOwnerDeviceID;
|
||||
BGM_ClientMap mClientMap;
|
||||
|
||||
// Counters for the number of clients that are doing IO. These are used to tell whether any clients
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.1</string>
|
||||
<string>0.2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -37,7 +37,7 @@
|
||||
</array>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016, 2017 Background Music contributors</string>
|
||||
<string>Copyright © 2016-2018 Background Music contributors</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
|
||||
@@ -57,7 +57,7 @@ static const AudioServerPlugInClientInfo client2Info = {
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
clients = new BGM_Clients(&taskQueue);
|
||||
clients = new BGM_Clients(kAudioObjectUnknown, &taskQueue);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
|
||||
@@ -32,8 +32,40 @@
|
||||
// PublicUtility Includes
|
||||
#include "CAException.h"
|
||||
|
||||
// STL Includes
|
||||
#include <stdexcept>
|
||||
|
||||
@interface BGM_DeviceTests : XCTestCase
|
||||
|
||||
// Subclass BGM_Device to add some test-only functions.
|
||||
class TestBGM_Device
|
||||
:
|
||||
public BGM_Device
|
||||
{
|
||||
|
||||
public:
|
||||
TestBGM_Device();
|
||||
~TestBGM_Device() = default;
|
||||
|
||||
};
|
||||
|
||||
TestBGM_Device::TestBGM_Device()
|
||||
:
|
||||
BGM_Device(kObjectID_Device,
|
||||
CFSTR(kDeviceName),
|
||||
CFSTR(kBGMDeviceUID),
|
||||
CFSTR(kBGMDeviceModelUID),
|
||||
kObjectID_Stream_Input,
|
||||
kObjectID_Stream_Output,
|
||||
kObjectID_Volume_Output_Master,
|
||||
kObjectID_Mute_Output_Master)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
|
||||
|
||||
@interface BGM_DeviceTests : XCTestCase {
|
||||
TestBGM_Device* testDevice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -42,29 +74,79 @@
|
||||
|
||||
- (void) setUp {
|
||||
[super setUp];
|
||||
testDevice = new TestBGM_Device();
|
||||
}
|
||||
|
||||
- (void) tearDown {
|
||||
// Reminder: add code here, above the super call
|
||||
delete testDevice;
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void) testDoIOOperation_writeMix_readInput {
|
||||
// The number of audio frames to send/receive in the IO operations.
|
||||
const int kFrameSize = 512;
|
||||
|
||||
// Choose a sample time that will make the data wrap around to the start of the device's
|
||||
// internal ring buffer.
|
||||
AudioServerPlugInIOCycleInfo cycleInfo {};
|
||||
cycleInfo.mOutputTime.mSampleTime = kLoopbackRingBufferFrameSize - 25.0;
|
||||
|
||||
// Generate the test input data.
|
||||
Float32 inputBuffer[kFrameSize * 2];
|
||||
|
||||
for(int i = 0; i < kFrameSize * 2; i++)
|
||||
{
|
||||
inputBuffer[i] = static_cast<Float32>(i);
|
||||
}
|
||||
|
||||
// Send a copy of the input buffer just in case DoIOOperation modifies the data for some reason.
|
||||
Float32 inputBufferCopy[kFrameSize * 2];
|
||||
memcpy(inputBufferCopy, inputBuffer, sizeof(inputBuffer));
|
||||
|
||||
// Send the input data to the device.
|
||||
testDevice->DoIOOperation(/* inStreamObjectID = */ kObjectID_Stream_Output,
|
||||
/* inClientID = */ 0,
|
||||
/* inOperationID = */ kAudioServerPlugInIOOperationWriteMix,
|
||||
/* inIOBufferFrameSize = */ kFrameSize,
|
||||
/* inIOCycleInfo = */ cycleInfo,
|
||||
/* ioMainBuffer = */ inputBuffer,
|
||||
/* ioSecondaryBuffer = */ nullptr);
|
||||
|
||||
// Request data from the same point in time so we get the same data back.
|
||||
cycleInfo.mInputTime.mSampleTime = kLoopbackRingBufferFrameSize - 25.0;
|
||||
|
||||
// Read the data back from the device.
|
||||
Float32 outputBuffer[kFrameSize * 2];
|
||||
|
||||
testDevice->DoIOOperation(/* inStreamObjectID = */ kObjectID_Stream_Output,
|
||||
/* inClientID = */ 0,
|
||||
/* inOperationID = */ kAudioServerPlugInIOOperationReadInput,
|
||||
/* inIOBufferFrameSize = */ kFrameSize,
|
||||
/* inIOCycleInfo = */ cycleInfo,
|
||||
/* ioMainBuffer = */ outputBuffer,
|
||||
/* ioSecondaryBuffer = */ nullptr);
|
||||
|
||||
// Check the output matches the input.
|
||||
for(int i = 0; i < kFrameSize * 2; i++)
|
||||
{
|
||||
XCTAssertEqual(inputBuffer[i], outputBuffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) testCustomPropertyMusicPlayerBundleID {
|
||||
BGM_Device& device = BGM_Device::GetInstance();
|
||||
|
||||
// Convenience wrappers
|
||||
auto getBundleID = [&](UInt32 inDataSize = sizeof(CFStringRef)){
|
||||
CFStringRef bundleID = NULL;
|
||||
CFStringRef bundleID = nullptr;
|
||||
UInt32 outDataSize;
|
||||
|
||||
device.GetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 3,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ NULL,
|
||||
/* inDataSize = */ inDataSize,
|
||||
/* outDataSize = */ outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(&bundleID));
|
||||
testDevice->GetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 3,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ nullptr,
|
||||
/* inDataSize = */ inDataSize,
|
||||
/* outDataSize = */ outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(&bundleID));
|
||||
|
||||
// This isn't technically required, but we're unlikely to ever want to return any more/less data from GetPropertyData.
|
||||
XCTAssertEqual(outDataSize, sizeof(CFStringRef));
|
||||
@@ -73,13 +155,13 @@
|
||||
};
|
||||
|
||||
auto setBundleID = [&](const CFStringRef* __nullable bundleID, UInt32 dataSize = sizeof(CFStringRef)){
|
||||
device.SetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 1234,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ NULL,
|
||||
/* inDataSize = */ dataSize,
|
||||
/* inData = */ reinterpret_cast<const void* __nonnull>(bundleID));
|
||||
testDevice->SetPropertyData(/* inObjectID = */ kObjectID_Device,
|
||||
/* inClientPID = */ 1234,
|
||||
/* inAddress = */ kBGMMusicPlayerBundleIDAddress,
|
||||
/* inQualifierDataSize = */ 0,
|
||||
/* inQualifierData = */ nullptr,
|
||||
/* inDataSize = */ dataSize,
|
||||
/* inData = */ reinterpret_cast<const void* __nonnull>(bundleID));
|
||||
};
|
||||
|
||||
// Should be set to the empty string by default.
|
||||
@@ -98,13 +180,14 @@
|
||||
XCTAssertEqualObjects(getBundleID(), @"");
|
||||
|
||||
// Arguments should be null-checked.
|
||||
BGMShouldThrow<BGM_RuntimeException>(self, [&](){
|
||||
BGMShouldThrow<std::runtime_error>(self, [&](){
|
||||
UInt32 outDataSize;
|
||||
device.GetPropertyData(kObjectID_Device, 0, kBGMMusicPlayerBundleIDAddress, 0, NULL, sizeof(CFStringRef),
|
||||
outDataSize, /* outData = */ reinterpret_cast<void* __nonnull>(NULL));
|
||||
testDevice->GetPropertyData(kObjectID_Device, 0, kBGMMusicPlayerBundleIDAddress, 0, nullptr,
|
||||
sizeof(CFStringRef), outDataSize,
|
||||
/* outData = */ reinterpret_cast<void* __nonnull>(NULL));
|
||||
});
|
||||
BGMShouldThrow<BGM_RuntimeException>(self, [&](){
|
||||
setBundleID(NULL);
|
||||
BGMShouldThrow<std::runtime_error>(self, [&](){
|
||||
setBundleID(nullptr);
|
||||
});
|
||||
|
||||
// Invalid data should be rejected.
|
||||
@@ -112,7 +195,7 @@
|
||||
setBundleID((CFStringRef*)&kCFNull);
|
||||
});
|
||||
BGMShouldThrow<CAException>(self, [&](){
|
||||
CFStringRef nullRef = NULL;
|
||||
CFStringRef nullRef = nullptr;
|
||||
setBundleID(&nullRef);
|
||||
});
|
||||
BGMShouldThrow<CAException>(self, [&](){
|
||||
|
||||
@@ -68,7 +68,7 @@ void LogError(const char *fmt, ...)
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
// vsyslog. Also added __ASSERT_STOP. Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
@@ -83,7 +83,7 @@ void LogError(const char *fmt, ...)
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
CADebuggerStop();
|
||||
__ASSERT_STOP;
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
@@ -94,7 +94,7 @@ void LogWarning(const char *fmt, ...)
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
// vsyslog. Also added __ASSERT_STOP. Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
@@ -109,7 +109,7 @@ void LogWarning(const char *fmt, ...)
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
//CADebuggerStop(); // TODO: Add a toggle for this to the project file (under "Preprocessor Macros"). Default to off.
|
||||
//__ASSERT_STOP; // TODO: Add a toggle for this to the project file (under "Preprocessor Macros"). Default to off.
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
|
||||
+16
-14
@@ -6,18 +6,18 @@ The codebase is split into two projects: BGMDriver, a [userspace](https://en.wik
|
||||
Audio](https://developer.apple.com/library/mac/documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html)
|
||||
[HAL](https://developer.apple.com/library/mac/documentation/DeviceDrivers/Conceptual/WritingAudioDrivers/AudioOnMacOSX/AudioOnMacOSX.html#//apple_ref/doc/uid/TP30000730-TPXREF104)
|
||||
[plugin](https://developer.apple.com/library/prerelease/content/samplecode/AudioDriverExamples/Listings/ReadMe_txt.html)
|
||||
that publishes the virtual audio device, and BGMApp, which handles the UI, passing audio from the virtual device to the
|
||||
real output device and a few other things. The virtual device is usually referred to as "BGMDevice" in the code. Any
|
||||
code shared between the two projects is kept in the `SharedSource` dir.
|
||||
that publishes the virtual audio device<sup id="a1">[1](#f1)</sup>, and BGMApp, which handles the UI, passing audio from
|
||||
the virtual device to the real output device and a few other things. The virtual device is usually referred to as
|
||||
"BGMDevice" in the code. Any code shared between the two projects is kept in the `SharedSource` dir.
|
||||
|
||||
## Summary
|
||||
|
||||
From the user's perspective, BGMDevice appears as one input device and one output device, both named "Background Music
|
||||
Device". They're shown in `System Preferences > Sound` along with the real audio devices.
|
||||
From the user's perspective, BGMDevice appears as one input device and one output device, both named "Background Music".
|
||||
They're shown in `System Preferences > Sound` along with the real audio devices.
|
||||
|
||||
When you start BGMApp, it sets BGMDevice as your system's default output device so the system (i.e. Core Audio) will
|
||||
start sending all<sup id="a1">[1](#f1)</sup> your audio data to BGMDriver. BGMDriver plays that audio on BGMDevice's
|
||||
input stream, and the user can record it by selecting "Background Music Device" in QuickTime the same way they'd select
|
||||
start sending all<sup id="a2">[2](#f2)</sup> your audio data to BGMDriver. BGMDriver plays that audio on BGMDevice's
|
||||
input stream, and the user can record it by selecting the Background Music device in QuickTime the same way they'd select
|
||||
a microphone.
|
||||
|
||||
So that you can still hear the audio, BGMApp starts listening to BGMDevice's input stream and playing the audio out of
|
||||
@@ -50,10 +50,10 @@ Nothing](http://www.rossbencina.com/code/real-time-audio-programming-101-time-wa
|
||||
|
||||
## BGMDriver
|
||||
|
||||
The BGMDriver project is an audio driver for a virtual audio device called Background Music Device, which we use to
|
||||
intercept the audio playing on the user's system. The driver processes the audio data to apply per-app volumes, see if
|
||||
the music player is playing, etc. and then writes the audio to BGMDevice's input stream. It's essentially a loopback
|
||||
device with a few extra features.
|
||||
The BGMDriver project is an audio driver for a virtual audio device named "Background Music", which we use to intercept
|
||||
the audio playing on the user's system. The driver processes the audio data to apply per-app volumes, see if the music
|
||||
player is playing, etc. and then writes the audio to BGMDevice's input stream. It's essentially a loopback device with a
|
||||
few extra features.
|
||||
|
||||
There are quite a few other open-source projects with drivers that do the same
|
||||
thing--[Soundflower](https://github.com/mattingalls/Soundflower) is probably the most well known--but as far as I know
|
||||
@@ -216,8 +216,10 @@ Scheme...`, select the Background Music scheme, and add the environment var in R
|
||||
|
||||
----
|
||||
|
||||
<b id="f1">[1]</b> All, unless you're playing audio through a program that's set to always use a specific device, or one
|
||||
that doesn't switch to the new default device right away. The latter would usually be a bug in that program and I doubt
|
||||
we could do anything about it. [↩](#a1)
|
||||
<b id="f1">[1]</b> It actually publishes two devices -- the main one and one for UI-related sounds, but you probably
|
||||
only need to know about the main one. [↩](#a1)
|
||||
|
||||
<b id="f2">[2]</b> All, unless you're playing audio through a program that's set to always use a specific device or,
|
||||
for some reason, doesn't switch to the new default device right away. [↩](#a2)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -13,9 +13,25 @@
|
||||
- No restart required to install
|
||||
- Runs entirely in userspace
|
||||
|
||||
**Version 0.1.0**, first release. Probably very buggy. Not very polished or well tested.
|
||||
## Download
|
||||
|
||||
**Requires OS X 10.10+**. Might work on 10.9, but I haven't tried it. Currently unable to build in Xcode 6.
|
||||
### Version 0.1.1
|
||||
|
||||
<a href="https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.1.1/BackgroundMusic-0.1.1.pkg"><img
|
||||
src="Images/README/pkg-icon.png" width="32" height="32" align="absmiddle" />
|
||||
BackgroundMusic-0.1.1.pkg</a> (473 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.
|
||||
|
||||
> <sub>MD5: e02988e6b32eafa88b99c4da33e7fe56</sub><br/>
|
||||
> <sub>SHA256: 7ce875bb00fdeb2b5b363aa92367b3fa096d18cb02a02c461d5df66307ab1088</sub><br/>
|
||||
> <sub>PGP:
|
||||
> [sig](https://github.com/kyleneideck/BackgroundMusic/releases/download/v0.1.1/BackgroundMusic-0.1.1.pkg.asc),
|
||||
> [key (0595DF814E41A6F69334C5E2CAA8D9B8E39EC18C)](https://bearisdriving.com/kyle-neideck.gpg)</sub>
|
||||
|
||||
We also have [snapshot builds](https://github.com/kyleneideck/BackgroundMusic/releases).
|
||||
|
||||
## Auto-pause music
|
||||
|
||||
@@ -37,21 +53,17 @@ their normal maximum volume.
|
||||
## Recording system audio
|
||||
|
||||
With Background Music running, open QuickTime Player and go `File > New Audio Recording...` (or movie/screen). Then
|
||||
click the arrow next to the record button that looks like `⌄` and select `Background Music Device` as the input device.
|
||||
click the arrow next to the record button that looks like `⌄` and select `Background Music` as the input device.
|
||||
|
||||
You should be able to record system audio and a microphone together by creating an [aggregate
|
||||
device](https://support.apple.com/en-us/HT202000) that combines your input device (usually Built-in Input) with
|
||||
Background Music Device. You can create the aggregate device using the Audio MIDI Setup utility from
|
||||
the Background Music device. You can create the aggregate device using the Audio MIDI Setup utility from
|
||||
`/Applications/Utilities`.
|
||||
|
||||
## Install
|
||||
## Install from source
|
||||
|
||||
No binaries yet (working on it) but building should take less than a minute.
|
||||
|
||||
<table>
|
||||
<tr><td>⚠️</td>
|
||||
<td>Unfortunately, <strong>it won't build if you don't have Xcode installed</strong> because xcodebuild doesn't work on its own anymore. If you don't mind a 4GB download, you can <a href="https://developer.apple.com/xcode/download/">get Xcode here</a>.</td>
|
||||
</tr></table>
|
||||
Building should take less than a minute, but you'll need [Xcode](https://developer.apple.com/xcode/download/) version
|
||||
8 or higher.
|
||||
|
||||
If you're comfortable with it, you can just paste the following at a Terminal prompt.
|
||||
|
||||
@@ -69,7 +81,7 @@ include a checksum), which makes sure we can't run a half-downloaded copy of bui
|
||||
/bin/bash BackgroundMusic-master/build_and_install.sh -w && rm -rf BackgroundMusic-master)
|
||||
```
|
||||
|
||||
Otherwise, to build and install:
|
||||
Otherwise, to build and install from source:
|
||||
|
||||
- Clone or [download](https://github.com/kyleneideck/BackgroundMusic/archive/master.zip) the project.
|
||||
- If the project is in a zip, unzip it.
|
||||
@@ -85,7 +97,9 @@ Wiki](https://github.com/kyleneideck/BackgroundMusic/wiki/Installation).
|
||||
|
||||
## Uninstall
|
||||
|
||||
- Run the `uninstall.sh` script to remove Background Music from your system.
|
||||
- Run the `uninstall.sh` script (using `Terminal.app`) to remove Background Music from your system. You should be able
|
||||
to find it in `/Applications/Background Music.app/Contents/Resources/uninstall.sh`, but if not you can [download the
|
||||
project](https://github.com/kyleneideck/BackgroundMusic/archive/master.zip) again.
|
||||
- Go to the Sound section in System Preferences and change your default output device at least once. (If you only have
|
||||
one device now, either use `Audio MIDI Setup.app` to create a temporary aggregate device, restart any audio apps that
|
||||
have stopped working or just restart your system.)
|
||||
@@ -97,9 +111,11 @@ consider submitting a bug report, too.)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If Background Music crashes and system audio stops working, open the Sound panel in System Preferences and change your
|
||||
system's default output device to something other than Background Music Device. If it already is, it might help to
|
||||
change the default device and then change it back again. Failing that, you might have to uninstall.
|
||||
If Background Music crashes and your audio stops working, open the Sound panel in System Preferences and change your
|
||||
system's default output device to something other than the Background Music device. If it already is, it might help to
|
||||
change the default device and then change it back again.
|
||||
|
||||
Failing that, you might have to uninstall. Consider filing a bug report if you do.
|
||||
|
||||
## Known issues
|
||||
|
||||
@@ -112,13 +128,13 @@ change the default device and then change it back again. Failing that, you might
|
||||
General tab of Skype's preferences.
|
||||
- Plugging in or unplugging headphones when Background Music isn't running can silence system audio. To fix it, go to
|
||||
the Sound section in System Preferences, click the Output tab and change your default output device to something other
|
||||
than Background Music Device. Alternatively, you may Option+Click on the Sound icon in the menu bar to select a
|
||||
than the Background Music device. Alternatively, you may Option+Click on the Sound icon in the menu bar to select a
|
||||
different output device.
|
||||
|
||||
This happens when macOS remembers that Background Music Device was your default audio device the last time you last
|
||||
used (or didn't use) headphones.
|
||||
- [A recent Chrome bug](https://bugs.chromium.org/p/chromium/issues/detail?id=557620) can stop Chrome from switching to
|
||||
Background Music Device after you open Background Music. Chrome's audio will still play, but Background Music won't be
|
||||
This happens when macOS remembers that the Background Music device was your default audio device the last time you
|
||||
last used (or didn't use) headphones.
|
||||
- [A Chrome bug](https://bugs.chromium.org/p/chromium/issues/detail?id=557620) can stop Chrome from switching to the
|
||||
Background Music device after you open Background Music. Chrome's audio will still play, but Background Music won't be
|
||||
aware of it.
|
||||
- Some apps play notification sounds that are only just long enough to trigger an auto-pause. The only workaround right
|
||||
now is to increase the `kPauseDelayNSec` constant in [BGMAutoPauseMusic.mm](/BGMApp/BGMApp/BGMAutoPauseMusic.mm).
|
||||
@@ -134,6 +150,7 @@ change the default device and then change it back again. Failing that, you might
|
||||
- [Soundflower](https://github.com/mattingalls/Soundflower) - "MacOS system extension that allows applications to pass
|
||||
audio to other applications."
|
||||
- [WavTap](https://github.com/pje/WavTap) - "globally capture whatever your mac is playing—-as simply as a screenshot"
|
||||
- [eqMac](http://www.bitgapp.com/eqmac/), [GitHub](https://github.com/nodeful/eqMac2) - "System-wide Audio Equalizer for the Mac"
|
||||
- [llaudio](https://github.com/mountainstorm/llaudio) - "An old piece of work to reverse engineer the Mac OSX
|
||||
user/kernel audio interface. Shows how to read audio straight out of the kernel as you would on Darwin (where most the
|
||||
OSX goodness is missing)"
|
||||
@@ -151,11 +168,10 @@ change the default device and then change it back again. Failing that, you might
|
||||
- [Sound Siphon](https://staticz.com/soundsiphon/), [Sound Control](https://staticz.com/soundcontrol/) - System/app audio recording, per-app volumes, system audio equaliser
|
||||
- [SoundBunny](https://www.prosofteng.com/soundbunny-mac-volume-control/) - "Control application volume independently."
|
||||
- [Boom 2](http://www.globaldelight.com/boom/index.php) - "The Best Volume Booster & Equalizer For Mac"
|
||||
- [eqMac](http://www.bitgapp.com/eqmac/) - "System-wide Audio Equalizer for Mac OSX"
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2016, 2017 [Background Music contributors](https://github.com/kyleneideck/BackgroundMusic/graphs/contributors).
|
||||
Copyright © 2016-2018 [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 @@
|
||||
// BGMXPCProtocols.h
|
||||
// SharedSource
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
@@ -45,7 +45,15 @@ static NSString* kBGMXPCHelperMachServiceName = @kBGMXPCHelperBundleID;
|
||||
//
|
||||
// If BGMApp can be reached, the error it returns will be passed the reply block. Otherwise, the reply block will be passed an error with
|
||||
// one of the kBGMXPC_* error codes. It may have an underlying error using one of the NSXPCConnection* error codes from FoundationErrors.h.
|
||||
- (void) waitForBGMAppToStartOutputDeviceWithReply:(void (^)(NSError*))reply;
|
||||
- (void) startBGMAppPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI;
|
||||
|
||||
// BGMXPCHelper will set the system's default output device to deviceID if it loses its connection
|
||||
// to BGMApp and BGMApp has left BGMDevice as the default device. It waits for a short time first to
|
||||
// give BGMApp a chance to fix the connection.
|
||||
//
|
||||
// This is so BGMDevice isn't left as the default device if BGMApp crashes or otherwise terminates
|
||||
// abnormally. If audio is played to BGMDevice and BGMApp isn't running, the user won't hear it.
|
||||
- (void) setOutputDeviceToMakeDefaultOnAbnormalTermination:(AudioObjectID)deviceID;
|
||||
|
||||
@end
|
||||
|
||||
@@ -53,7 +61,7 @@ static NSString* kBGMXPCHelperMachServiceName = @kBGMXPCHelperBundleID;
|
||||
// The protocol that BGMApp will vend as its XPC API.
|
||||
@protocol BGMAppXPCProtocol
|
||||
|
||||
- (void) waitForOutputDeviceToStartWithReply:(void (^)(NSError*))reply;
|
||||
- (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+57
-19
@@ -23,6 +23,12 @@
|
||||
#ifndef SharedSource__BGM_Types
|
||||
#define SharedSource__BGM_Types
|
||||
|
||||
// STL Includes
|
||||
#if defined(__cplusplus)
|
||||
#include <stdexcept>
|
||||
#endif
|
||||
|
||||
// System Includes
|
||||
#include <CoreAudio/AudioServerPlugIn.h>
|
||||
|
||||
|
||||
@@ -40,25 +46,39 @@ static const char* const kBGMIssueTrackerURL = "https://github.com/kyleneideck/B
|
||||
|
||||
#define kBGMDeviceUID "BGMDevice"
|
||||
#define kBGMDeviceModelUID "BGMDeviceModelUID"
|
||||
#define kBGMDeviceUID_UISounds "BGMDevice_UISounds"
|
||||
#define kBGMDeviceModelUID_UISounds "BGMDeviceModelUID_UISounds"
|
||||
#define kBGMNullDeviceUID "BGMNullDevice"
|
||||
#define kBGMNullDeviceModelUID "BGMNullDeviceModelUID"
|
||||
|
||||
// The object IDs for the audio objects this driver implements.
|
||||
//
|
||||
// BGMDevice always publishes this fixed set of objects (regardless of the wrapped device). We might need to
|
||||
// change that at some point, but so far it hasn't caused any problems and it makes the driver much simpler.
|
||||
// BGMDevice always publishes this fixed set of objects (except when BGMDevice's volume or mute
|
||||
// controls are disabled). We might need to change that at some point, but so far it hasn't caused
|
||||
// any problems and it makes the driver much simpler.
|
||||
enum
|
||||
{
|
||||
kObjectID_PlugIn = kAudioObjectPlugInObject,
|
||||
kObjectID_Device = 2, // Belongs to kObjectID_PlugIn
|
||||
kObjectID_Stream_Input = 3, // Belongs to kObjectID_Device
|
||||
kObjectID_Stream_Output = 4, // Belongs to kObjectID_Device
|
||||
kObjectID_Volume_Output_Master = 5, // Belongs to kObjectID_Device
|
||||
kObjectID_Mute_Output_Master = 6, // Belongs to kObjectID_Device
|
||||
kObjectID_Device_Null = 7, // Belongs to kObjectID_PlugIn
|
||||
kObjectID_Stream_Null = 8, // Belongs to kObjectID_Device_Null
|
||||
kObjectID_PlugIn = kAudioObjectPlugInObject,
|
||||
// BGMDevice
|
||||
kObjectID_Device = 2, // Belongs to kObjectID_PlugIn
|
||||
kObjectID_Stream_Input = 3, // Belongs to kObjectID_Device
|
||||
kObjectID_Stream_Output = 4, // Belongs to kObjectID_Device
|
||||
kObjectID_Volume_Output_Master = 5, // Belongs to kObjectID_Device
|
||||
kObjectID_Mute_Output_Master = 6, // Belongs to kObjectID_Device
|
||||
// Null Device
|
||||
kObjectID_Device_Null = 7, // Belongs to kObjectID_PlugIn
|
||||
kObjectID_Stream_Null = 8, // Belongs to kObjectID_Device_Null
|
||||
// BGMDevice for UI sounds
|
||||
kObjectID_Device_UI_Sounds = 9, // Belongs to kObjectID_PlugIn
|
||||
kObjectID_Stream_Input_UI_Sounds = 10, // Belongs to kObjectID_Device_UI_Sounds
|
||||
kObjectID_Stream_Output_UI_Sounds = 11, // Belongs to kObjectID_Device_UI_Sounds
|
||||
kObjectID_Volume_Output_Master_UI_Sounds = 12, // Belongs to kObjectID_Device_UI_Sounds
|
||||
};
|
||||
|
||||
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
|
||||
// master element."
|
||||
static const AudioObjectPropertyElement kMasterChannel = 0;
|
||||
|
||||
#pragma BGM Plug-in Custom Properties
|
||||
|
||||
enum
|
||||
@@ -104,9 +124,9 @@ enum
|
||||
};
|
||||
|
||||
// The number of silent/audible frames before BGMDriver will change kAudioDeviceCustomPropertyDeviceAudibleState
|
||||
#define kDeviceAudibleStateMinChangedFramesForUpdate (2 << 12)
|
||||
#define kDeviceAudibleStateMinChangedFramesForUpdate (2 << 11)
|
||||
|
||||
enum
|
||||
enum BGMDeviceAudibleState : SInt32
|
||||
{
|
||||
// kAudioDeviceCustomPropertyDeviceAudibleState values
|
||||
//
|
||||
@@ -201,7 +221,7 @@ enum {
|
||||
kBGMXPC_Timeout,
|
||||
kBGMXPC_BGMAppStateError,
|
||||
kBGMXPC_HardwareError,
|
||||
kBGMXPC_HardwareNotStartingError,
|
||||
kBGMXPC_ReturningEarlyError,
|
||||
kBGMXPC_InternalError
|
||||
};
|
||||
|
||||
@@ -209,12 +229,30 @@ enum {
|
||||
|
||||
#if defined(__cplusplus)
|
||||
|
||||
class BGM_InvalidClientException { };
|
||||
class BGM_InvalidClientPIDException { };
|
||||
class BGM_InvalidClientRelativeVolumeException { };
|
||||
class BGM_InvalidClientPanPositionException { };
|
||||
class BGM_DeviceNotSetException { };
|
||||
class BGM_RuntimeException { };
|
||||
class BGM_InvalidClientException : public std::runtime_error {
|
||||
public:
|
||||
BGM_InvalidClientException() : std::runtime_error("InvalidClient") { }
|
||||
};
|
||||
|
||||
class BGM_InvalidClientPIDException : public std::runtime_error {
|
||||
public:
|
||||
BGM_InvalidClientPIDException() : std::runtime_error("InvalidClientPID") { }
|
||||
};
|
||||
|
||||
class BGM_InvalidClientRelativeVolumeException : public std::runtime_error {
|
||||
public:
|
||||
BGM_InvalidClientRelativeVolumeException() : std::runtime_error("InvalidClientRelativeVolume") { }
|
||||
};
|
||||
|
||||
class BGM_InvalidClientPanPositionException : public std::runtime_error {
|
||||
public:
|
||||
BGM_InvalidClientPanPositionException() : std::runtime_error("InvalidClientPanPosition") { }
|
||||
};
|
||||
|
||||
class BGM_DeviceNotSetException : public std::runtime_error {
|
||||
public:
|
||||
BGM_DeviceNotSetException() : std::runtime_error("DeviceNotSet") { }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGM_Utils.cpp
|
||||
// SharedSource
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
@@ -29,10 +29,35 @@
|
||||
// System Includes
|
||||
#include <MacTypes.h>
|
||||
#include <mach/mach_error.h>
|
||||
#include <CoreFoundation/CoreFoundation.h> // For kCFCoreFoundationVersionNumber
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
dispatch_queue_t BGMGetDispatchQueue_PriorityUserInteractive()
|
||||
{
|
||||
long queueClass;
|
||||
|
||||
// Compile-time check that QOS_CLASS_USER_INTERACTIVE can be used. It was added in 10.10.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101000 // MAC_OS_X_VERSION_10_10
|
||||
// Runtime check for the same.
|
||||
if(floor(kCFCoreFoundationVersionNumber) > kCFCoreFoundationVersionNumber10_9)
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
queueClass = QOS_CLASS_USER_INTERACTIVE;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// Fallback for older versions.
|
||||
queueClass = DISPATCH_QUEUE_PRIORITY_HIGH;
|
||||
}
|
||||
|
||||
return dispatch_get_global_queue(queueClass, 0);
|
||||
}
|
||||
|
||||
namespace BGM_Utils
|
||||
{
|
||||
// Forward declarations
|
||||
@@ -93,13 +118,17 @@ namespace BGM_Utils
|
||||
void LogException(const char* __nullable fileName,
|
||||
int lineNumber,
|
||||
const char* callerName,
|
||||
CAException e)
|
||||
const CAException& e)
|
||||
{
|
||||
LogError("%s:%d:%s: CAException, error code: %d.",
|
||||
OSStatus err = e.GetError();
|
||||
const char err4CC[5] = CA4CCToCString(err);
|
||||
|
||||
LogError("%s:%d:%s: CAException, code: '%s' (%d).",
|
||||
(fileName ? fileName : ""),
|
||||
lineNumber,
|
||||
callerName,
|
||||
e.GetError());
|
||||
err4CC,
|
||||
err);
|
||||
}
|
||||
|
||||
void LogUnexpectedException(const char* __nullable fileName,
|
||||
@@ -148,17 +177,21 @@ namespace BGM_Utils
|
||||
{
|
||||
function();
|
||||
}
|
||||
catch(CAException e)
|
||||
catch(const CAException& e)
|
||||
{
|
||||
// TODO: Can/should we log a stack trace somewhere? (If so, also in the following catch
|
||||
// block.)
|
||||
// TODO: Log a warning instead of an error for expected exceptions?
|
||||
LogError("%s:%d:%s: %sCAException, error code: %d. %s%s %s %s ",
|
||||
OSStatus err = e.GetError();
|
||||
const char err4CC[5] = CA4CCToCString(err);
|
||||
|
||||
LogError("%s:%d:%s: %sCAException, code: '%s' (%d). %s%s %s %s ",
|
||||
(fileName ? fileName : ""),
|
||||
lineNumber,
|
||||
callerName,
|
||||
(expected ? "" : "Unexpected "),
|
||||
e.GetError(),
|
||||
err4CC,
|
||||
err,
|
||||
(message ? message : ""),
|
||||
(message ? "." : ""),
|
||||
(expected ? "If you think this might be a bug:"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// BGM_Utils.h
|
||||
// SharedSource
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
#ifndef SharedSource__BGM_Utils
|
||||
@@ -25,10 +25,11 @@
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CADebugMacros.h"
|
||||
#include "CAException.h"
|
||||
|
||||
#if defined(__cplusplus)
|
||||
|
||||
#include "CAException.h"
|
||||
|
||||
// STL Includes
|
||||
#include <functional>
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
|
||||
// System Includes
|
||||
#include <mach/error.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#pragma mark Macros
|
||||
|
||||
@@ -127,6 +129,10 @@
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
#pragma mark C Utility Functions
|
||||
|
||||
dispatch_queue_t BGMGetDispatchQueue_PriorityUserInteractive(void);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
|
||||
#pragma mark C++ Utility Functions
|
||||
@@ -138,7 +144,7 @@ namespace BGM_Utils
|
||||
template <typename T>
|
||||
inline T __nonnull NN(T __nullable v) {
|
||||
BGMAssertNonNull(v);
|
||||
return v;
|
||||
return static_cast<T __nonnull>(v);
|
||||
}
|
||||
|
||||
// Log (and swallow) errors returned by Mach functions. Returns false if there was an error.
|
||||
@@ -171,7 +177,7 @@ namespace BGM_Utils
|
||||
void LogException(const char* __nullable fileName,
|
||||
int lineNumber,
|
||||
const char* callerName,
|
||||
CAException e);
|
||||
const CAException& e);
|
||||
|
||||
void LogUnexpectedException(const char* __nullable fileName,
|
||||
int lineNumber,
|
||||
|
||||
@@ -70,7 +70,7 @@ There are also lots of other TODOs commented around the code.
|
||||
|
||||
- Should we hide the BGM device when BGMApp isn't running? This would fix the problem of our device being left as the
|
||||
default device if BGMApp doesn't shutdown properly (because of a crash, hard reset, etc.), which stops the system from
|
||||
playing audio. The problem with that is Background Music Device can still be used without BGMApp, to record
|
||||
playing audio. The problem with that is the Background Music device can still be used without BGMApp, to record
|
||||
system/apps' audio, so ideally the BGM device would be able to just unset itself as the default device when BGMApp
|
||||
isn't running. For now, I think we should just have `kAudioDevicePropertyDeviceCanBeDefaultDevice` become false.
|
||||
|
||||
|
||||
+70
-34
@@ -40,7 +40,7 @@ set -o errtrace
|
||||
cd "$( dirname "${BASH_SOURCE[0]}" )"
|
||||
|
||||
error_handler() {
|
||||
LAST_COMMAND="${BASH_COMMAND}" LAST_COMMAND_EXIT_STATUS=$?
|
||||
LAST_COMMAND="$3" LAST_COMMAND_EXIT_STATUS="$2"
|
||||
|
||||
# Log the error.
|
||||
echo "Failure in $0 at line $1. The last command was (probably)" >> ${LOG_FILE}
|
||||
@@ -86,7 +86,7 @@ enable_error_handling() {
|
||||
# TODO: The version of Bash that ships with OSX only gives you the line number of the
|
||||
# function the error occurred in -- not the line the error occurred on. There are a
|
||||
# few solutions suggested on various websites, but none of them work.
|
||||
trap 'error_handler ${LINENO}' ERR
|
||||
trap 'error_handler ${LINENO} $? "${BASH_COMMAND}"' ERR
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -145,11 +145,10 @@ if ! [[ -x "${XCODEBUILD}" ]]; then
|
||||
fi
|
||||
# This check is last because it takes 10 seconds or so if it fails.
|
||||
if ! [[ -x "${XCODEBUILD}" ]]; then
|
||||
XCODEBUILD=$(/usr/bin/xcrun --find xcodebuild &2>>${LOG_FILE} || true)
|
||||
XCODEBUILD="$(/usr/bin/xcrun --find xcodebuild 2>>${LOG_FILE} || true)"
|
||||
fi
|
||||
|
||||
# TODO: Update this when/if Xcode 6 is supported.
|
||||
RECOMMENDED_MIN_XCODE_VERSION=7
|
||||
RECOMMENDED_MIN_XCODE_VERSION=8
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [options]" >&2
|
||||
@@ -220,12 +219,11 @@ show_spinner() {
|
||||
# (wait returns 127 if the process has already exited.)
|
||||
if [[ ${EXIT_STATUS} -ne 0 ]] && [[ ${EXIT_STATUS} -ne 127 ]]; then
|
||||
ERROR_MSG="$1"
|
||||
if [[ ${DID_TIMEOUT} -eq 0 ]]; then
|
||||
ERROR_MSG+="\n\nFailed command:
|
||||
${PREV_COMMAND_STRING}"
|
||||
if [[ ${DID_TIMEOUT} -ne 0 ]]; then
|
||||
ERROR_MSG+="\n\nCommand timed out after ${TIMEOUT} seconds."
|
||||
fi
|
||||
|
||||
error_handler ${LINENO}
|
||||
error_handler ${LINENO} ${EXIT_STATUS} "${PREV_COMMAND_STRING}"
|
||||
|
||||
if [[ ${CONTINUE_ON_ERROR} -eq 0 ]]; then
|
||||
exit ${EXIT_STATUS}
|
||||
@@ -247,8 +245,11 @@ parse_options() {
|
||||
CONFIGURATION="Debug"
|
||||
;;
|
||||
b)
|
||||
# Just build; don't install.
|
||||
XCODEBUILD_ACTION="build"
|
||||
# The dirs xcodebuild will build in.
|
||||
# TODO: If these dirs were created by running this script without -b, they'll be
|
||||
# owned by root and xcodebuild will fail.
|
||||
APP_PATH="./BGMApp/build"
|
||||
DRIVER_PATH="./BGMDriver/build"
|
||||
;;
|
||||
@@ -525,6 +526,19 @@ log_debug_info() {
|
||||
LOG_DEBUG_INFO_TASK_PID=$!
|
||||
}
|
||||
|
||||
# Cleans the build products and intermediate files for a build scheme.
|
||||
#
|
||||
# Params:
|
||||
# - The Xcode build scheme to clean, e.g. "Background Music Device".
|
||||
clean() {
|
||||
if [[ "${CLEAN}" != "" ]]; then
|
||||
${SUDO} "${XCODEBUILD}" -scheme "$1" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
BUILD_DIR=./build \
|
||||
${CLEAN} >> ${LOG_FILE} 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Register our handler so we can print a message and clean up if there's an error.
|
||||
enable_error_handling
|
||||
|
||||
@@ -604,57 +618,78 @@ done
|
||||
|
||||
log_debug_info $*
|
||||
|
||||
# BGMDriver
|
||||
|
||||
if [[ "${XCODEBUILD_ACTION}" == "install" ]]; then
|
||||
SUDO="sudo"
|
||||
ACTIONING="Installing"
|
||||
else
|
||||
# No need to sudo if we're only building.
|
||||
SUDO=""
|
||||
ACTIONING="Building"
|
||||
fi
|
||||
|
||||
# Enable AddressSanitizer in debug builds to catch memory bugs. Allow ENABLE_ASAN to be set as an
|
||||
# environment variable by only setting it here if it isn't already set. (Used by package.sh.)
|
||||
if [[ "${CONFIGURATION}" == "Debug" ]]; then
|
||||
ENABLE_ASAN="${ENABLE_ASAN:-YES}"
|
||||
else
|
||||
ENABLE_ASAN="${ENABLE_ASAN:-NO}"
|
||||
fi
|
||||
|
||||
# Clean all projects. Done separately to workaround what I think is a bug in Xcode 10.0. If you just
|
||||
# add "clean" to the other xcodebuild commands, they seem to fail because of the DSTROOT="/" arg.
|
||||
if [[ "${CLEAN}" != "" ]]; then
|
||||
# Disable the -e shell option and error trap for build commands so we can handle errors
|
||||
# differently.
|
||||
(disable_error_handling
|
||||
clean "Background Music Device"
|
||||
clean "PublicUtility"
|
||||
clean "BGMXPCHelper"
|
||||
clean "Background Music"
|
||||
# Also delete the build dirs as files/dirs left in them can make the install step fail and,
|
||||
# if you're using Xcode 10, the commands above will have cleaned the DerivedData dir but not
|
||||
# the build dirs. I think this is a separate Xcode bug. See
|
||||
# <http://www.openradar.me/40906897>.
|
||||
${SUDO} /bin/rm -rf BGMDriver/build BGMApp/build >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
echo "Cleaning"
|
||||
show_spinner "Clean command failed. Try deleting the directories BGMDriver/build and \
|
||||
BGMApp/build manually and running '$0 -n' to skip the cleaning step."
|
||||
fi
|
||||
|
||||
# BGMDriver
|
||||
|
||||
echo "[1/3] ${ACTIONING} the virtual audio device $(bold_face ${DRIVER_DIR}) to" \
|
||||
"$(bold_face ${DRIVER_PATH})" \
|
||||
"$(bold_face ${DRIVER_PATH})" \
|
||||
| tee -a ${LOG_FILE}
|
||||
|
||||
# Disable the -e shell option and error trap for build commands so we can handle errors differently.
|
||||
(disable_error_handling
|
||||
# Build Apple's PublicUtility classes as a static library.
|
||||
${SUDO} "${XCODEBUILD}" -project BGMDriver/BGMDriver.xcodeproj \
|
||||
-target "PublicUtility" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} build >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
(disable_error_handling
|
||||
# Build and install BGMDriver
|
||||
# TODO: Should these use -scheme instead?
|
||||
${SUDO} "${XCODEBUILD}" -project BGMDriver/BGMDriver.xcodeproj \
|
||||
-target "Background Music Device" \
|
||||
# Build and install BGMDriver.
|
||||
${SUDO} "${XCODEBUILD}" -scheme "Background Music Device" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
-enableAddressSanitizer ${ENABLE_ASAN} \
|
||||
BUILD_DIR=./build \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
DSTROOT="/" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
# BGMXPCHelper
|
||||
|
||||
echo "[2/3] ${ACTIONING} $(bold_face ${XPC_HELPER_DIR}) to $(bold_face ${XPC_HELPER_PATH})" \
|
||||
| tee -a ${LOG_FILE}
|
||||
| tee -a ${LOG_FILE}
|
||||
|
||||
(disable_error_handling
|
||||
${SUDO} "${XCODEBUILD}" -project BGMApp/BGMApp.xcodeproj \
|
||||
-target BGMXPCHelper \
|
||||
${SUDO} "${XCODEBUILD}" -scheme BGMXPCHelper \
|
||||
-configuration ${CONFIGURATION} \
|
||||
-enableAddressSanitizer ${ENABLE_ASAN} \
|
||||
BUILD_DIR=./build \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
DSTROOT="/" \
|
||||
INSTALL_PATH="${XPC_HELPER_PATH}" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
@@ -664,13 +699,14 @@ echo "[3/3] ${ACTIONING} $(bold_face ${APP_DIR}) to $(bold_face ${APP_PATH})" \
|
||||
| tee -a ${LOG_FILE}
|
||||
|
||||
(disable_error_handling
|
||||
${SUDO} "${XCODEBUILD}" -project BGMApp/BGMApp.xcodeproj \
|
||||
-target "Background Music" \
|
||||
${SUDO} "${XCODEBUILD}" -scheme "Background Music" \
|
||||
-configuration ${CONFIGURATION} \
|
||||
-enableAddressSanitizer ${ENABLE_ASAN} \
|
||||
BUILD_DIR=./build \
|
||||
RUN_CLANG_STATIC_ANALYZER=0 \
|
||||
DSTROOT="/" \
|
||||
${XCODEBUILD_OPTIONS} \
|
||||
${CLEAN} "${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
"${XCODEBUILD_ACTION}" >> ${LOG_FILE} 2>&1) &
|
||||
|
||||
show_spinner "${BUILD_FAILED_ERROR_MSG}"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user